Skip to content

Instantly share code, notes, and snippets.

@Nezteb
Created May 29, 2026 15:56
Show Gist options
  • Select an option

  • Save Nezteb/4f440443b13b9801d10b8e3e3f6a8aa1 to your computer and use it in GitHub Desktop.

Select an option

Save Nezteb/4f440443b13b9801d10b8e3e3f6a8aa1 to your computer and use it in GitHub Desktop.
defmodule Mix.Tasks.RecompileStats do
@moduledoc """
Minimal Mix task reimplementation of "files that cause the most recompiles" and "files that get recompiled most often" from:
- https://github.com/axelson/dep_viz
- https://depviz.jasonaxelson.com/
Usage: `mix recompile_stats`
"""
use Mix.Task
@impl Mix.Task
def run(_args) do
case System.cmd("mix", ["xref", "graph", "--format", "dot"], stderr_to_stdout: true) do
{_, 0} -> :ok
{output, code} -> Mix.raise("mix xref failed (exit #{code}):\n#{output}")
end
dot = "./xref_graph.dot" |> Path.expand() |> File.read!()
graph = dot |> Dotx.decode!() |> Dotx.flatten()
{edges, _graphs} = Dotx.to_edges(graph)
{cause_entries, dep_entries} = analyze_edges(edges)
print_top_10("Files that cause the most recompiles:", cause_entries)
print_top_10("Files that get recompiled most often:", dep_entries)
end
def analyze_edges(edges) do
adjacency =
Enum.reduce(edges, %{}, fn %{from: %{id: [from]}, to: %{id: [to]}, attrs: attrs}, acc ->
type = edge_type(attrs)
Map.update(acc, from, [{to, type}], &[{to, type} | &1])
end)
all_nodes =
Enum.reduce(adjacency, MapSet.new(Map.keys(adjacency)), fn {_, deps}, acc ->
Enum.reduce(deps, acc, fn {node, _}, acc -> MapSet.put(acc, node) end)
end)
|> MapSet.to_list()
dependencies_map =
Map.new(all_nodes, fn file ->
{file, find_all_dependency_types(adjacency, file)}
end)
cause_recompile_map =
Enum.reduce(dependencies_map, %{}, fn {file, deps}, acc ->
Enum.reduce(deps, acc, fn
{dep_file, :compile}, acc -> Map.update(acc, dep_file, [file], &[file | &1])
_, acc -> acc
end)
end)
cause_entries =
Enum.map(cause_recompile_map, fn {file, dependents} ->
{Enum.count(dependents, &(&1 != file)), file}
end)
dep_entries =
Enum.map(dependencies_map, fn {file, deps} ->
{Enum.count(deps, fn {dep_file, type} -> dep_file != file and type in [:compile, :export] end), file}
end)
{cause_entries, dep_entries}
end
defp find_all_dependency_types(graph, start) do
ctx = %{
matched: %{start => :compile},
state: :top_level,
self_id: start
}
do_find_all_dependency_types(graph, start, ctx)
end
defp do_find_all_dependency_types(graph, id, %{matched: matched, state: state, self_id: svid} = ctx) do
deps = Map.get(graph, id, [])
# Visit compile deps first so :compile wins when a node is reachable multiple ways.
sorted_deps =
Enum.sort_by(deps, fn
{_, :compile} -> 0
{_, :export} -> 1
{_, :runtime} -> 2
end)
{matched, _svid} =
Enum.reduce(sorted_deps, {matched, svid}, fn {dep_id, dep_type}, {matched, svid} ->
# Allow revisiting the original starting node when inside a compile chain,
# so that its runtime deps also get classified as compile deps.
needs_self_visit = state == :compile_chain and svid == dep_id
if Map.has_key?(matched, dep_id) and not needs_self_visit do
{matched, svid}
else
loop_svid = if needs_self_visit, do: nil, else: svid
next = next_traversal_state(state, dep_type)
next_svid = if next == :compile_chain, do: loop_svid, else: nil
matched =
matched
|> Map.put(dep_id, dep_result_type(state, dep_type))
|> then(&do_find_all_dependency_types(graph, dep_id, %{ctx | matched: &1, state: next, self_id: next_svid}))
{matched, loop_svid}
end
end)
matched
end
defp print_top_10(label, entries) do
IO.puts(label)
entries
|> Enum.sort(:desc)
|> Enum.take(10)
|> Enum.each(fn {count, file} -> IO.puts("- #{count}: #{file}") end)
IO.puts("")
end
defp edge_type(%{"label" => "(compile)"}), do: :compile
defp edge_type(%{"label" => "(export)"}), do: :export
defp edge_type(_), do: :runtime
defp next_traversal_state(:compile_chain, _), do: :compile_chain
defp next_traversal_state(:top_level, :compile), do: :compile_chain
defp next_traversal_state(_, _), do: :descending
defp dep_result_type(:compile_chain, _), do: :compile
defp dep_result_type(:top_level, :compile), do: :compile
defp dep_result_type(:top_level, :export), do: :export
defp dep_result_type(_, _), do: :runtime
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment