Ansel 0.0
A darktable fork - bloat + design vision
Loading...
Searching...
No Matches
include_report.py
Go to the documentation of this file.
1#!/usr/bin/env python3
2"""Aggregate include-hygiene report: a single self-contained HTML page with SVG charts.
3
4This deliberately does NOT draw per-file include graphs -- doc/Doxyfile already has
5HAVE_DOT with INCLUDE_GRAPH, INCLUDED_BY_GRAPH and DIRECTORY_GRAPH, and produces
6interactive SVG that is better for drilling into one file than anything here.
7
8What Doxygen does not give you, and this does:
9 * RANKING -- which headers cost the most, sorted, with numbers you can diff
10 * BLAST RADIUS -- how many TUs recompile when header X is touched, quantified
11 * LAYERING -- inversions counted per directory pair, not just drawn
12 * TREND -- the same metrics at two git revisions, side by side
13 * UNUSED -- candidate removable includes (see tools/include_unused.py)
14
15Note on the Doxygen graphs: DOT_GRAPH_MAX_NODES is 100 in doc/Doxyfile, so exactly the
16god-headers worth looking at render truncated. Raise it (or set MAX_DOT_GRAPH_DEPTH)
17before drilling into one of the headers this report ranks at the top.
18
19No third-party dependencies: charts are hand-emitted SVG so the tool runs anywhere the
20rest of tools/ runs, including CI.
21
22Usage:
23 python3 tools/include_report.py # writes include-report.html
24 python3 tools/include_report.py -o /tmp/r.html
25 python3 tools/include_report.py --unused unused.json # fold in include_unused.py --json
26"""
27import html
28import os
29import sys
30from collections import defaultdict
31
32sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
33import include_graph as ig # noqa: E402 (same directory, shared graph construction)
34
35PALETTE = ['#4c8bf5', '#e8710a', '#12a37a', '#c5221f', '#a142f4', '#f9ab00']
36
37
38def build():
39 files = ig.collect()
40 known = set(files)
41 graph = defaultdict(set)
42 for p, text in files.items():
43 for inc in ig.INCLUDE_RE.findall(text):
44 t = ig.resolve(inc, p, known)
45 if t and t != p:
46 graph[p].add(t)
47 return files, graph
48
49
50def closures(files, graph):
51 memo = {}
52
53 def closure(n):
54 if n in memo:
55 return memo[n]
56 out, stack = set(), [n]
57 while stack:
58 cur = stack.pop()
59 for w in graph.get(cur, ()):
60 if w not in out:
61 out.add(w)
62 stack.append(w)
63 memo[n] = out
64 return out
65 return closure
66
67
68def bar_chart(rows, title, unit, colour=PALETTE[0], width=900, row_h=22):
69 """rows: list of (label, value). Emits a horizontal bar chart as inline SVG."""
70 if not rows:
71 return '<p>nothing to show</p>'
72 top = max(v for _, v in rows) or 1
73 label_w = 380
74 bar_w = width - label_w - 90
75 height = row_h * len(rows) + 34
76 out = ['<svg viewBox="0 0 %d %d" width="100%%" role="img" aria-label="%s">'
77 % (width, height, html.escape(title))]
78 out.append('<text x="0" y="16" class="ct">%s</text>' % html.escape(title))
79 for i, (label, value) in enumerate(rows):
80 y = 34 + i * row_h
81 w = max(1, int(bar_w * value / top))
82 out.append('<text x="0" y="%d" class="lb">%s</text>'
83 % (y + 12, html.escape(label[-58:])))
84 out.append('<rect x="%d" y="%d" width="%d" height="%d" rx="2" fill="%s"/>'
85 % (label_w, y + 2, w, row_h - 6, colour))
86 out.append('<text x="%d" y="%d" class="vl">%s %s</text>'
87 % (label_w + w + 6, y + 12, f'{value:,}', unit))
88 out.append('</svg>')
89 return '\n'.join(out)
90
91
92def matrix(viol, width=900):
93 """Directory-pair inversion counts as a heat table."""
94 if not viol:
95 return '<p>no layering inversions</p>'
96 pairs = sorted(viol.items(), key=lambda kv: -len(kv[1]))
97 top = len(pairs[0][1]) or 1
98 cells = []
99 for (a, b), items in pairs:
100 n = len(items)
101 intensity = 0.15 + 0.85 * (n / top)
102 cells.append(
103 '<div class="cell" style="background:rgba(197,34,31,%.2f)">'
104 '<span class="pair">%s → %s</span><span class="n">%d</span></div>'
105 % (intensity, html.escape(a), html.escape(b), n))
106 return '<div class="grid">%s</div>' % ''.join(cells)
107
108
109def main():
110 out_path = 'include-report.html'
111 if '-o' in sys.argv:
112 out_path = sys.argv[sys.argv.index('-o') + 1]
113
114 files, graph = build()
115 closure = closures(files, graph)
116 headers = [f for f in files if f.endswith(('.h', '.hpp'))]
117 tus = [f for f in files if f.endswith(('.c', '.cc', '.cpp'))]
118
119 lines = {}
120 for f in files:
121 lines[f] = files[f].count('\n') + 1
122
123 # blast radius: how many TUs recompile when this header changes
124 blast = defaultdict(int)
125 blast_lines = defaultdict(int)
126 for t in tus:
127 for h in closure(t):
128 blast[h] += 1
129 for h in headers:
130 blast_lines[h] = blast[h] * lines.get(h, 0)
131
132 comps = [c for c in ig.tarjan(graph, list(files)) if len(c) > 1]
133
134 viol = defaultdict(list)
135 for a, targets in graph.items():
136 la = ig.layer_of(a)
137 if la is None:
138 continue
139 for b in targets:
140 lb = ig.layer_of(b)
141 if lb is None:
142 continue
143 if lb > la:
144 viol[(a.split(os.sep)[1], b.split(os.sep)[1])].append((a, b))
145
146 weighted_title = "Weighted by the header's own size (fan-in x lines)"
147 top_blast = sorted(((h, blast[h]) for h in headers), key=lambda kv: -kv[1])[:20]
148 top_cost = sorted(((h, blast_lines[h]) for h in headers), key=lambda kv: -kv[1])[:20]
149 top_closure = sorted(((h, len(closure(h))) for h in headers), key=lambda kv: -kv[1])[:20]
150 heavy_tu = sorted(((t, sum(lines.get(h, 0) for h in closure(t))) for t in tus),
151 key=lambda kv: -kv[1])[:20]
152
153 unused_section = ''
154 if '--unused' in sys.argv:
155 import json
156 with open(sys.argv[sys.argv.index('--unused') + 1], encoding='utf-8') as fh:
157 data = json.load(fh)
158 per_header = defaultdict(int)
159 per_dir = defaultdict(int)
160 for path, cands in data.items():
161 per_dir[path.split(os.sep)[1]] += len(cands)
162 for c in cands:
163 per_header[c['include']] += 1
164 total = sum(len(v) for v in data.values())
165 unused_section = (
166 '<h2>Candidate unneeded includes</h2>'
167 '<p class="note">%d candidates across %d files, from '
168 '<code>tools/include_unused.py</code>. These are <em>questions</em>: a static '
169 'pass cannot tell "not used" from "used to reach a transitive dependency". '
170 'Measured precision on a verified sample was ~87%%; always confirm with '
171 '<code>--verify</code> before removing.</p>%s%s'
172 % (total, len(data),
173 bar_chart(sorted(per_header.items(), key=lambda kv: -kv[1])[:20],
174 'Most often included without using any of its names', 'files',
175 PALETTE[3]),
176 bar_chart(sorted(per_dir.items(), key=lambda kv: -kv[1]),
177 'Candidates by directory', 'includes', PALETTE[5])))
178
179 cycles_html = ('<p class="ok">The include graph is a DAG — 0 cycles.</p>'
180 if not comps else
181 '<ul>%s</ul>' % ''.join(
182 '<li>cycle of %d: %s</li>'
183 % (len(c), html.escape(', '.join(sorted(c))))
184 for c in sorted(comps, key=len, reverse=True)))
185
186 doc = f"""<title>Ansel include hygiene</title>
187<style>
188 :root {{ color-scheme: light dark; }}
189 body {{ font: 15px/1.55 system-ui, sans-serif; margin: 0 auto; padding: 2rem 1.25rem;
190 max-width: 1000px; }}
191 h1 {{ font-size: 1.6rem; margin: 0 0 .25rem; }}
192 h2 {{ font-size: 1.15rem; margin: 2.5rem 0 .5rem; padding-bottom: .3rem;
193 border-bottom: 1px solid rgba(128,128,128,.35); }}
194 .sub {{ opacity: .7; margin-top: 0; }}
195 .note {{ opacity: .8; font-size: .92rem; }}
196 .ok {{ color: #12a37a; font-weight: 600; }}
197 svg {{ display: block; margin: 1rem 0 1.75rem; max-width: 100%; height: auto; overflow: visible; }}
198 .ct {{ font: 600 13px system-ui, sans-serif; fill: currentColor; }}
199 .lb {{ font: 12px ui-monospace, monospace; fill: currentColor; opacity: .85; }}
200 .vl {{ font: 11px system-ui, sans-serif; fill: currentColor; opacity: .7; }}
201 .grid {{ display: grid; grid-template-columns: repeat(auto-fill, minmax(215px, 1fr));
202 gap: .4rem; }}
203 .cell {{ padding: .45rem .6rem; border-radius: 4px; display: flex;
204 justify-content: space-between; gap: .5rem; color: #fff; }}
205 .pair {{ font: 12px ui-monospace, monospace; }}
206 .n {{ font-weight: 700; }}
207 table {{ border-collapse: collapse; width: 100%; font-size: .92rem; }}
208 td, th {{ text-align: left; padding: .3rem .5rem;
209 border-bottom: 1px solid rgba(128,128,128,.25); }}
210 td.num {{ text-align: right; font-variant-numeric: tabular-nums; }}
211 code {{ font-size: .9em; }}
212 .wrap {{ overflow-x: auto; }}
213</style>
214<h1>Ansel include hygiene</h1>
215<p class="sub">{len(files)} files · {len(headers)} headers · {len(tus)} translation units ·
216{sum(len(v) for v in graph.values()):,} direct include edges</p>
217
218<h2>Cycles</h2>
219{cycles_html}
220
221<h2>Blast radius — how many TUs rebuild when you touch this header</h2>
222<p class="note">This is the number Doxygen's <em>included-by</em> graph draws but does not
223count. A header near the top cannot be edited cheaply by anyone.</p>
224<div class="wrap">{bar_chart(top_blast, 'Top headers by transitive fan-in', 'TUs', PALETTE[0])}</div>
225<div class="wrap">{bar_chart(top_cost, weighted_title, 'lines', PALETTE[1])}</div>
226
227<h2>God-headers — what including one costs</h2>
228<div class="wrap">{bar_chart(top_closure, 'Headers dragging in the most other project headers', 'headers', PALETTE[2])}</div>
229<div class="wrap">{bar_chart(heavy_tu, 'Heaviest translation units (total header lines preprocessed)', 'lines', PALETTE[4])}</div>
230
231<h2>Layering inversions</h2>
232<p class="note">A file including something from a higher layer. Layer order:
233external/win → common → control → gui/dtgtk/bauhaus → develop → iop/imageio →
234libs/views/chart → cli.</p>
235{matrix(viol)}
236
237{unused_section}
238
239<h2>Reproducing</h2>
240<div class="wrap"><table>
241<tr><th>command</th><th>what it answers</th></tr>
242<tr><td><code>python3 tools/include_graph.py --summary</code></td><td>metrics, for before/after diffing</td></tr>
243<tr><td><code>python3 tools/include_graph.py --mermaid</code></td><td>directory-level graph</td></tr>
244<tr><td><code>python3 tools/include_unused.py --json u.json</code></td><td>candidate unneeded includes</td></tr>
245<tr><td><code>python3 tools/include_unused.py --verify</code></td><td>confirm candidates by recompiling</td></tr>
246<tr><td><code>python3 tools/pragma_once_to_guards.py --verify</code></td><td>no <code>#pragma once</code> came back</td></tr>
247<tr><td><code>doxygen doc/Doxyfile</code></td><td>per-file include / included-by / directory graphs</td></tr>
248</table></div>
249"""
250 with open(out_path, 'w', encoding='utf-8') as fh:
251 fh.write(doc)
252 print('wrote %s (%d headers, %d TUs, %d cycles)'
253 % (out_path, len(headers), len(tus), len(comps)))
254 return 0
255
256
257if __name__ == '__main__':
258 sys.exit(main())
const float max
const dt_colormatrix_t matrix
bar_chart(rows, title, unit, colour=PALETTE[0], width=900, row_h=22)
closures(files, graph)