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."""
71 return '<p>nothing to show</p>'
72 top =
max(v
for _, v
in rows)
or 1
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):
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))
93 """Directory-pair inversion counts as a heat table."""
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
99 for (a, b), items
in pairs:
101 intensity = 0.15 + 0.85 * (n / top)
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)
110 out_path =
'include-report.html'
112 out_path = sys.argv[sys.argv.index(
'-o') + 1]
114 files, graph =
build()
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'))]
121 lines[f] = files[f].count(
'\n') + 1
124 blast = defaultdict(int)
125 blast_lines = defaultdict(int)
130 blast_lines[h] = blast[h] * lines.get(h, 0)
132 comps = [c
for c
in ig.tarjan(graph, list(files))
if len(c) > 1]
134 viol = defaultdict(list)
135 for a, targets
in graph.items():
144 viol[(a.split(os.sep)[1], b.split(os.sep)[1])].append((a, b))
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]
154 if '--unused' in sys.argv:
156 with open(sys.argv[sys.argv.index(
'--unused') + 1], encoding=
'utf-8')
as 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)
163 per_header[c[
'include']] += 1
164 total = sum(len(v)
for v
in data.values())
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'
173 bar_chart(sorted(per_header.items(), key=
lambda kv: -kv[1])[:20],
174 'Most often included without using any of its names',
'files',
176 bar_chart(sorted(per_dir.items(), key=
lambda kv: -kv[1]),
177 'Candidates by directory',
'includes', PALETTE[5])))
179 cycles_html = (
'<p class="ok">The include graph is a DAG — 0 cycles.</p>'
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)))
186 doc = f
"""<title>Ansel include hygiene</title>
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));
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; }}
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>
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>
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>
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>
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>
250 with open(out_path,
'w', encoding=
'utf-8')
as fh:
252 print(
'wrote %s (%d headers, %d TUs, %d cycles)'
253 % (out_path, len(headers), len(tus), len(comps)))