Powered by AppSignal & Oban Pro
Would you like to see your link here? Contact us

Archeometer - bonfire

reports/dev/static/livemd/bonfire.livemd

Archeometer - bonfire

# Dependencies

Mix.install([
  {:archeometer, "~> 0.5"},
  {:kino_vega_lite, "~> 0.1.11"}
])

# Utility functons

defmodule LiveBookUtils do
  def to_data_table(result) do
    Enum.zip(result.headers, Enum.zip(result.rows) |> Enum.map(&Tuple.to_list(&1)))
    |> Kino.DataTable.new()
  end

  def list_to_data_table(list, col_name) do
    Kino.DataTable.new(%{col_name => list})
  end
end

# Setup database

db = "/Users/me/Code/Bonfire/bonfire-app/archeometer_bonfire.db"
Application.put_env(:archeometer, :default_db, db)

# Application name and default namespace

app_name = "bonfire"
default_ns = "*"

Setup DSL

In order to use the Archeometer toolkit, we need some aliases and imports

alias Archeometer.Schema.{App, AppXRef, Module, Function, XRef, Behaviour}
alias Archeometer.Analysis.{DSM, Treemap, Clustering}
alias Archeometer.Analysis.Xref, as: XRefAnalysis
alias Archeometer.Analysis.Apps.Xref, as: AppXRefAnalysis
alias Archeometer.Graphs.Graphviz
alias Archeometer.Repo
import Archeometer.Query

Size

Top 10 largest modules, measured by LoC (Lines of Code).

Repo.all(
  from(m in Module,
    select: [
      name: m.name,
      num_lines: m.num_lines
    ],
    #where: m.app.name == ^app_name,
    order_by: [desc: num_lines]
  )
)
|> LiveBookUtils.to_data_table()

This treemap represents a hierarchical decomposition of modules. The area of each module representing its relative size.

Treemap.treemap(:size, app: app_name, namespace: default_ns)
|> Treemap.SVGRender.render()

Size

Complexity

Top 10 modules with the most complexity

Repo.all(
  from(m in Module,
    select: [
      name: m.name,
      aggregated_cc: sum(m.functions.cc),
      average_cc: round(sum(m.functions.cc) * 1.0 / count(m.functions.id), 2)
    ],
    group_by: m.name,
    order_by: [desc: average_cc],
   # where: m.app.name == ^app_name,
    limit: 10
  )
)
|> LiveBookUtils.to_data_table()

Top 10 functions with the most complexity.

Repo.all(
  from(f in Function,
    select: [
      module_name: f.module.name,
      fun_name: f.name,
      fun_arity: f.num_args,
      fun_cc: f.cc
    ],
    order_by: [desc: fun_cc],
    #where: f.module.app.name == ^app_name,
    limit: 10
  )
)
|> LiveBookUtils.to_data_table()

Core Modules

Modules with the most dependencies.

Repo.all(
  from(x in XRef,
    select: [
      name: x.caller.name,
      num_callees: count(x.callee.name)
    ],
    group_by: x.caller.id,
    order_by: [desc: num_callees],
    #where: x.caller.app.name == ^app_name,
    limit: 10
  )
)
|> LiveBookUtils.to_data_table()

Possibly the intersection between the biggest modules and those with the most dependencies gives us a hint about the core modules.

biggest_modules =
  Repo.all(
    from(m in Module,
      select: [
        name: m.name
      ],
      order_by: [desc: m.num_lines],
      #where: m.app.name == ^app_name,
      #limit: 10
    )
  )

most_dependencies_modules =
  Repo.all(
    from(x in XRef,
      select: [
        name: x.caller.name
      ],
      group_by: x.caller.id,
      order_by: [desc: count(x.callee.name)],
     # where: x.caller.app.name == ^app_name,
     # limit: 10
    )
  )

result = intersection(biggest_modules, most_dependencies_modules)
LiveBookUtils.to_data_table(result)

This is de dependency graph between the (hypothetically) core modules.

result.rows
|> Enum.map(&hd/1)
|> XRefAnalysis.gen_graph("svg")

Dependency Cycles

Colored squares respresent groups of modules with cyclic dependencies between them (open image in another tab if it is too small).

{:ok, dsm, modules} = DSM.gen_dsm(app_name, default_ns, db, true)
triangularized_dsm = DSM.triangularize(dsm)
dsm_svg = DSM.SVGRender.render(triangularized_dsm, modules)

To get the largest group of cyclic dependencies, you could use the following code.

largest_cycle = DSM.largest_cyclic_deps_group(app_name, db)
LiveBookUtils.list_to_data_table(largest_cycle, "name")
XRefAnalysis.gen_graph(largest_cycle, "svg", db)

Building Blocks

Modules most used within the application.

Repo.all(
  from(x in XRef,
    select: [
      name: x.callee.name,
      num_callers: count(x.caller.name)
    ],
    group_by: x.callee.name,
    order_by: [desc: num_callers],
    #where: x.callee.app.name == ^app_name,
    limit: 10
  )
)
|> LiveBookUtils.to_data_table()

Public API

Modules with the most public functions.

Repo.all(
  from(f in Function,
    select: [
      name: f.module.name,
      num_funs: count(f.id)
    ],
    #where: f.module.app.name == ^app_name,
    where: f.type == "def",
    group_by: f.module.id,
    order_by: [desc: num_funs]
  )
)
|> LiveBookUtils.to_data_table()

Ecto

These are the Ecto schemas defined in the application.

result =
  Repo.all(
    from(m in Module,
      #where: m.app.name == ^app_name,
      where: m.has_ecto_schema,
      select: [name: m.name],
      order_by: [asc: name]
    )
  )

LiveBookUtils.to_data_table(result)

This is the dependency graph of the Ecto schemas. (If detail is too small, you can right-click and open it in other tab).

result.rows
|> Enum.map(&hd/1)
|> XRefAnalysis.gen_graph("svg")

OTP

These are the modules implementing OTP behaviours.

otp_behaviours = ["Application", "Agent", "GenServer", "GenEvent", "Supervisor"]

Repo.all(
  from(b in Behaviour,
    select: [
      otp_behaviour: b.name,
      module: b.module.name
    ],
    where: b.name in ^otp_behaviours,
    #where: b.module.app.name == ^app_name,
    order_by: otp_behaviour
  )
)
|> LiveBookUtils.to_data_table()

Test Coverage

Modules with the least coverage.

Repo.all(
  from(m in Module,
    select: [
      name: m.name,
      num_lines: m.num_lines,
      coverage: round(m.coverage, 2),
      uncovered_lines: round(m.num_lines * (1 - m.coverage), 0)
    ],
    order_by: [desc: uncovered_lines],
    #where: m.app.name == ^app_name,
    where: m.coverage < 0.5
  )
)
|> LiveBookUtils.to_data_table()