defmodule Archeometer.Analysis.Treemap do
@moduledoc """
Functions for generating different TreeMaps.
"""
defmodule Node do
@moduledoc false
defstruct name: nil, nodes: [], kind: :group, val: 0, pct: 0
end
def treemap(:size, app, namespace, db_name, skip_tests) do
case __MODULE__.Data.get_data(:size, app, namespace, db_name, skip_tests) do
[] ->
{:error, :no_modules_matched}
modules ->
treemap(modules)
end
end
def treemap(modules, 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()
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