Created
May 29, 2026 15:56
-
-
Save Nezteb/4f440443b13b9801d10b8e3e3f6a8aa1 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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