defmodule Archeometer.Analysis.Treemap do
@moduledoc """
Functions for generating different TreeMaps.
"""
defmodule Node do
@moduledoc """
Represents a Node within a Treemap.
"""
defstruct name: nil, nodes: [], kind: :group, val: 0, pct: 0, metric: :none
end
@doc """
Generates a treemap with the given options.
Available metrics:
- size:
- functions:
- issues:
Available options:
- app: Application where the modules to consider belong
(default: :none, it means to include all applications)
- namespace: Namespace of the desired modules (default: "*")
- db: Path to Database (default: archeometer_project_name.db)
- skip_tests: If skip testing modules or not (default: true)
## Examples
iex> treemap(:size,
...> app: "jissai",
...> namespace: "Jissai.Reports.Attendance",
...> db: "test/resources/db/archeometer_jissai.db"
...> )
%Archeometer.Analysis.Treemap.Node{
kind: :group,
metric: :size,
name: "Jissai",
nodes: [
%Archeometer.Analysis.Treemap.Node{
kind: :group,
name: "Jissai.Reports",
nodes: [
%Archeometer.Analysis.Treemap.Node{
kind: :leaf,
name: "Jissai.Reports.Attendance",
nodes: [],
pct: 100.0,
val: 21
}
],
pct: 100.0,
val: 21
}
],
pct: 100,
val: 21
}
Then it can be rendered as SVG with Treemap.SVGRender.render/1
"""
def treemap(metric, opts \\ []) when is_atom(metric) do
cond do
Keyword.drop(opts, [:app, :namespace, :db, :skip_tests]) == [] ->
app = Keyword.get(opts, :app, :none)
namespace = Keyword.get(opts, :namespace, "*")
db_name = Keyword.get(opts, :db, Archeometer.Repo.default_db_name())
skip_tests = Keyword.get(opts, :skip_tests, true)
treemap(metric, app, namespace, db_name, skip_tests)
Keyword.drop(opts, [:modules, :ns_sep]) == [] ->
modules = Keyword.get(opts, :modules)
ns_sep = Keyword.get(opts, :ns_sep, ".")
do_treemap(modules, metric, ns_sep)
true ->
{:error, :incompatible_options}
end
end
defp treemap(metric, app, namespace, db_name, skip_tests) do
case __MODULE__.Data.get_data(metric, app, namespace, db_name, skip_tests) do
[] ->
{:error, :no_modules_matched}
{:error, error} ->
{:error, error}
modules ->
do_treemap(modules, metric)
end
end
defp do_treemap(modules, metric, ns_sep \\ ".") do
all_groups = all_groups(modules, ns_sep)
all_groups
|> get_roots(ns_sep)
|> init_tree()
|> add_groups(all_groups, ns_sep)
|> add_modules(modules, ns_sep)
|> aggregate_metric()
|> distribute_pct()
|> Map.put(:metric, metric)
end
defp aggregate_metric(%Node{kind: :group, nodes: nodes} = t) do
new_nodes = Enum.map(nodes, &aggregate_metric/1)
new_val = Enum.reduce(new_nodes, 0, fn node, acc -> node.val + acc end)
%{t | nodes: new_nodes, val: new_val}
end
defp aggregate_metric(%Node{kind: :leaf, val: _val} = t), do: t
defp distribute_pct(%Node{kind: :group, pct: pct, val: val, nodes: nodes} = t) do
new_nodes =
Enum.map(nodes, fn node ->
new_node = %{node | pct: node.val * pct / val}
distribute_pct(new_node)
end)
%{t | nodes: new_nodes}
end
defp distribute_pct(%Node{} = t), do: t
def parents(module, ns_sep) do
case String.split(module, ns_sep) |> Enum.reverse() do
[_] ->
[]
[_ | tail] ->
groups(tail, ns_sep)
end
end
defp groups([x], _ns_sep), do: [x]
defp groups([_h | t] = l, ns_sep),
do: [l |> Enum.reverse() |> Enum.join(ns_sep) | groups(t, ns_sep)]
defp all_groups(modules, ns_sep) do
modules
|> Enum.flat_map(fn {_id, name, _loc} -> parents(name, ns_sep) end)
|> Enum.uniq()
|> Enum.sort()
|> List.delete([])
end
defp get_roots(groups, ns_sep) do
Enum.filter(groups, fn g -> String.split(g, ns_sep) |> length() == 1 end)
end
defp init_tree([name]), do: %Node{name: name, nodes: [], pct: 100}
defp init_tree(names) do
nodes = Enum.map(names, fn name -> %Node{name: name, nodes: []} end)
%Node{name: "*", nodes: nodes, pct: 100}
end
def add_groups(%Node{} = t, [], _ns_sep), do: t
def add_groups(%Node{name: root_name, nodes: nodes} = t, [group_name | others], ns_sep) do
case {subgroup?(root_name, group_name, ns_sep), find_match(nodes, group_name, ns_sep)} do
{false, _match} ->
add_groups(t, others, ns_sep)
{true, nil} ->
case find_same(nodes, group_name) do
nil ->
new_nodes =
[%Node{name: group_name} | nodes]
|> Enum.sort_by(fn n -> n.name end)
new_t = %{t | nodes: new_nodes}
add_groups(new_t, others, ns_sep)
_same ->
add_groups(t, others, ns_sep)
end
{true, match} ->
new_nodes =
[add_groups(match, [group_name], ns_sep) | List.delete(nodes, match)]
|> Enum.sort_by(fn n -> n.name end)
new_t = %{t | nodes: new_nodes}
add_groups(new_t, others, ns_sep)
end
end
defp subgroup?("*", _group, _ns_sep), do: true
defp subgroup?(root_name, group, ns_sep) do
String.starts_with?(group, root_name <> ns_sep)
end
defp find_match(nodes, name, ns_sep) do
Enum.find(nodes, fn node -> String.starts_with?(name, node.name <> ns_sep) end)
end
defp find_same(nodes, name) do
Enum.find(nodes, fn node -> name == node.name end)
end
def add_modules(%Node{} = t, [], _ns_sep), do: t
def add_modules(
%Node{name: node_name, nodes: nodes} = t,
[{_id, module, val} = curr | others],
ns_sep
) do
new_t =
case {same_name?(node_name, module), direct_child?(module, node_name, ns_sep)} do
{true, _} ->
new_nodes =
[leaf(module, val) | nodes]
|> Enum.sort_by(fn n -> n.name end)
%{t | nodes: new_nodes}
{false, true} ->
new_nodes =
case find_same(nodes, module) do
nil ->
[leaf(module, val) | nodes]
|> Enum.sort_by(fn n -> n.name end)
match ->
[add_modules(match, [curr], ns_sep) | List.delete(nodes, match)]
|> Enum.sort_by(fn n -> n.name end)
end
%{t | nodes: new_nodes}
{false, false} ->
new_nodes = Enum.map(nodes, fn node -> add_modules(node, [curr], ns_sep) end)
%{t | nodes: new_nodes}
end
add_modules(new_t, others, ns_sep)
end
defp leaf(name, val) do
%Node{name: name, kind: :leaf, val: val}
end
defp direct_child?(module, root_name, ns_sep) do
String.split(module, ns_sep) |> Enum.drop(-1) |> Enum.join(ns_sep) == root_name
end
defp same_name?(module, node_name) do
module == node_name
end
end