bobframes 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- bobframes/__init__.py +3 -0
- bobframes/_version.py +1 -0
- bobframes/catalog.py +154 -0
- bobframes/cli.py +266 -0
- bobframes/derive_post_merge.py +365 -0
- bobframes/derives/__init__.py +0 -0
- bobframes/derives/pass_class_breakdown.py +102 -0
- bobframes/derives/texture_usage.py +121 -0
- bobframes/discovery.py +132 -0
- bobframes/global_entities.py +99 -0
- bobframes/html/__init__.py +0 -0
- bobframes/html/template.py +1056 -0
- bobframes/lint.py +114 -0
- bobframes/manifest.py +127 -0
- bobframes/parquetize.py +282 -0
- bobframes/parsers/__init__.py +0 -0
- bobframes/parsers/derive_program_transitions.py +73 -0
- bobframes/parsers/parse_init_state.py +675 -0
- bobframes/paths.py +111 -0
- bobframes/probes/__init__.py +0 -0
- bobframes/probes/whatif.py +165 -0
- bobframes/qrd_harness.py +119 -0
- bobframes/query_examples.py +222 -0
- bobframes/rdcmd.py +72 -0
- bobframes/replay/__init__.py +26 -0
- bobframes/replay/replay_main.py +2305 -0
- bobframes/reports/__init__.py +0 -0
- bobframes/reports/_dashboard.py +425 -0
- bobframes/reports/ab.py +88 -0
- bobframes/reports/base.py +114 -0
- bobframes/reports/cache.py +147 -0
- bobframes/reports/chrome.py +1306 -0
- bobframes/reports/cli.py +99 -0
- bobframes/reports/delta.py +167 -0
- bobframes/reports/discovery.py +118 -0
- bobframes/reports/draws_by_class.py +165 -0
- bobframes/reports/formatters.py +122 -0
- bobframes/reports/instancing_opportunities.py +276 -0
- bobframes/reports/orchestrator.py +59 -0
- bobframes/reports/overdraw.py +293 -0
- bobframes/reports/pass_gpu.py +190 -0
- bobframes/reports/shader_hotlist.py +240 -0
- bobframes/reports/trend_table.py +444 -0
- bobframes/resource_labels.py +162 -0
- bobframes/run.py +480 -0
- bobframes/schemas.py +426 -0
- bobframes/stable_keys.py +83 -0
- bobframes/tests/__init__.py +0 -0
- bobframes/tests/_render_util.py +84 -0
- bobframes/tests/data/golden/_reports/draws_by_class.html +323 -0
- bobframes/tests/data/golden/_reports/drill/District 01/2026-05-28_r110600/index.html +1560 -0
- bobframes/tests/data/golden/_reports/index.html +264 -0
- bobframes/tests/data/golden/_reports/instancing_opportunities.html +266 -0
- bobframes/tests/data/golden/_reports/overdraw.html +275 -0
- bobframes/tests/data/golden/_reports/pass_gpu.html +277 -0
- bobframes/tests/data/golden/_reports/shader_hotlist.html +265 -0
- bobframes/tests/data/golden/_reports/trend_table.html +390 -0
- bobframes/tests/data/golden/index.html +1175 -0
- bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/_manifest.json +51 -0
- bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/buffers.parquet +0 -0
- bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/clears.parquet +0 -0
- bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/counters_per_event.parquet +0 -0
- bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/descriptor_access.parquet +0 -0
- bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/dispatches.parquet +0 -0
- bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/draw_bindings.parquet +0 -0
- bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/draws.parquet +0 -0
- bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/events.parquet +0 -0
- bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/fbos.parquet +0 -0
- bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/frame_totals.parquet +0 -0
- bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/ibo_samples.parquet +0 -0
- bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/indirect_args.parquet +0 -0
- bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/passes.parquet +0 -0
- bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/pixel_history.parquet +0 -0
- bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/post_vs_samples.parquet +0 -0
- bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/program_transitions.parquet +0 -0
- bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/programs.parquet +0 -0
- bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/render_targets.parquet +0 -0
- bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/resource_creation.parquet +0 -0
- bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/rt_event_timeline.parquet +0 -0
- bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/samplers.parquet +0 -0
- bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/shaders.parquet +0 -0
- bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/state_change_events.parquet +0 -0
- bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/texture_samples.parquet +0 -0
- bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/textures.parquet +0 -0
- bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/vbo_samples.parquet +0 -0
- bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/vertex_inputs.parquet +0 -0
- bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/_manifest.json +51 -0
- bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/buffers.parquet +0 -0
- bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/clears.parquet +0 -0
- bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/counters_per_event.parquet +0 -0
- bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/descriptor_access.parquet +0 -0
- bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/dispatches.parquet +0 -0
- bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/draw_bindings.parquet +0 -0
- bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/draws.parquet +0 -0
- bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/events.parquet +0 -0
- bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/fbos.parquet +0 -0
- bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/frame_totals.parquet +0 -0
- bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/ibo_samples.parquet +0 -0
- bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/indirect_args.parquet +0 -0
- bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/passes.parquet +0 -0
- bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/pixel_history.parquet +0 -0
- bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/post_vs_samples.parquet +0 -0
- bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/program_transitions.parquet +0 -0
- bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/programs.parquet +0 -0
- bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/render_targets.parquet +0 -0
- bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/resource_creation.parquet +0 -0
- bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/rt_event_timeline.parquet +0 -0
- bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/samplers.parquet +0 -0
- bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/shaders.parquet +0 -0
- bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/state_change_events.parquet +0 -0
- bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/texture_samples.parquet +0 -0
- bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/textures.parquet +0 -0
- bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/vbo_samples.parquet +0 -0
- bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/vertex_inputs.parquet +0 -0
- bobframes/tests/make_synthetic.py +171 -0
- bobframes/tests/smoke.py +199 -0
- bobframes/tests/test_determinism.py +19 -0
- bobframes/tests/test_discovery.py +97 -0
- bobframes/tests/test_hardening.py +142 -0
- bobframes/tests/test_parity.py +22 -0
- bobframes/tests/test_perf.py +18 -0
- bobframes/tests/test_replay_drift.py +115 -0
- bobframes/tests/test_schemas.py +26 -0
- bobframes/tests/test_schemas_unit.py +55 -0
- bobframes/tests/test_stable_keys.py +61 -0
- bobframes-0.1.0.dist-info/METADATA +144 -0
- bobframes-0.1.0.dist-info/RECORD +130 -0
- bobframes-0.1.0.dist-info/WHEEL +4 -0
- bobframes-0.1.0.dist-info/entry_points.txt +2 -0
- bobframes-0.1.0.dist-info/licenses/LICENSE +21 -0
|
File without changes
|
|
@@ -0,0 +1,425 @@
|
|
|
1
|
+
"""Cumulative dashboard: hero strip + card grid of report summaries.
|
|
2
|
+
|
|
3
|
+
Lives at <root>/_reports/index.html. Linked from root index.html.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import os
|
|
9
|
+
import statistics
|
|
10
|
+
import sys
|
|
11
|
+
from collections import Counter, defaultdict
|
|
12
|
+
|
|
13
|
+
import pyarrow.parquet as papq
|
|
14
|
+
|
|
15
|
+
from . import base
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _top_meshes(root: str, drops: list, n: int = 3) -> list:
|
|
19
|
+
"""Return [(label, repeat, indices_med)] where label is a human-readable synthetic."""
|
|
20
|
+
cache = base.cache_path(root, 'draws_summary')
|
|
21
|
+
if not os.path.exists(cache):
|
|
22
|
+
return []
|
|
23
|
+
try:
|
|
24
|
+
t = papq.read_table(cache, columns=[
|
|
25
|
+
'mesh_hash', 'num_indices', 'program_id',
|
|
26
|
+
'draw_class', 'parent_pass_path_norm'])
|
|
27
|
+
except Exception:
|
|
28
|
+
return []
|
|
29
|
+
cols = {c: t.column(c).to_pylist() for c in t.column_names}
|
|
30
|
+
counts: Counter = Counter()
|
|
31
|
+
indices: dict = defaultdict(list)
|
|
32
|
+
cls_by_mesh: dict = {}
|
|
33
|
+
pass_by_mesh: dict = {}
|
|
34
|
+
for i in range(t.num_rows):
|
|
35
|
+
mh = cols['mesh_hash'][i]
|
|
36
|
+
prog = cols['program_id'][i] or 0
|
|
37
|
+
n_idx = cols['num_indices'][i] or 0
|
|
38
|
+
if not mh or n_idx <= 0 or prog == 0:
|
|
39
|
+
continue
|
|
40
|
+
counts[mh] += 1
|
|
41
|
+
indices[mh].append(n_idx)
|
|
42
|
+
cls_by_mesh.setdefault(mh, cols['draw_class'][i] or 'other')
|
|
43
|
+
pass_by_mesh.setdefault(mh, cols['parent_pass_path_norm'][i] or '')
|
|
44
|
+
out = []
|
|
45
|
+
for mh, c in counts.most_common(n):
|
|
46
|
+
try:
|
|
47
|
+
med = int(statistics.median(indices[mh])) if indices[mh] else 0
|
|
48
|
+
except statistics.StatisticsError:
|
|
49
|
+
med = 0
|
|
50
|
+
cls = cls_by_mesh.get(mh, 'other')
|
|
51
|
+
suffix = base.pass_suffix(pass_by_mesh.get(mh, '')) or '?'
|
|
52
|
+
hash_tag = str(mh)[-4:] if mh else ''
|
|
53
|
+
label = f'{cls}/{suffix}/{med}v#{hash_tag}'
|
|
54
|
+
out.append((label, c, med))
|
|
55
|
+
return out
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _top_passes(drops: list, n: int = 3) -> list:
|
|
59
|
+
"""Return [(area, pass_label, gpu_s)] where pass_label is the suffix only."""
|
|
60
|
+
agg: dict = defaultdict(float)
|
|
61
|
+
for d in drops:
|
|
62
|
+
for r in d.rows:
|
|
63
|
+
p = os.path.join(r.drop_dir, 'pass_class_breakdown.parquet')
|
|
64
|
+
if not os.path.exists(p):
|
|
65
|
+
continue
|
|
66
|
+
try:
|
|
67
|
+
t = papq.read_table(p, columns=['area', 'marker_path_norm',
|
|
68
|
+
'sum_gpu_duration_s'])
|
|
69
|
+
except Exception:
|
|
70
|
+
continue
|
|
71
|
+
cols = {c: t.column(c).to_pylist() for c in t.column_names}
|
|
72
|
+
for i in range(t.num_rows):
|
|
73
|
+
key = (cols['area'][i], cols['marker_path_norm'][i] or '')
|
|
74
|
+
agg[key] += cols['sum_gpu_duration_s'][i] or 0.0
|
|
75
|
+
ranked = sorted(agg.items(), key=lambda kv: kv[1], reverse=True)[:n]
|
|
76
|
+
return [(a, base.pass_suffix(m) or m, g) for (a, m), g in ranked]
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _top_shaders(root: str, n: int = 3) -> list:
|
|
80
|
+
"""Return [(label, complexity, cost_proxy)] where label is `frag-cplx-{int(cplx)}`."""
|
|
81
|
+
cache = base.cache_path(root, 'shader_summary')
|
|
82
|
+
if not os.path.exists(cache):
|
|
83
|
+
return []
|
|
84
|
+
try:
|
|
85
|
+
t = papq.read_table(cache,
|
|
86
|
+
columns=['stable_key', 'shader_type', 'complexity_score',
|
|
87
|
+
'used_by_draw_count'])
|
|
88
|
+
except Exception:
|
|
89
|
+
return []
|
|
90
|
+
cols = {c: t.column(c).to_pylist() for c in t.column_names}
|
|
91
|
+
cost: dict = defaultdict(float)
|
|
92
|
+
cplx: dict = {}
|
|
93
|
+
stype: dict = {}
|
|
94
|
+
for i in range(t.num_rows):
|
|
95
|
+
if cols['shader_type'][i] != 'fragment':
|
|
96
|
+
continue
|
|
97
|
+
sk = cols['stable_key'][i] or ''
|
|
98
|
+
if not sk:
|
|
99
|
+
continue
|
|
100
|
+
c_val = float(cols['complexity_score'][i] or 0)
|
|
101
|
+
uses = int(cols['used_by_draw_count'][i] or 0)
|
|
102
|
+
cost[sk] += c_val * uses
|
|
103
|
+
cplx[sk] = max(cplx.get(sk, 0), c_val)
|
|
104
|
+
stype[sk] = cols['shader_type'][i]
|
|
105
|
+
ranked = sorted(cost.items(), key=lambda kv: kv[1], reverse=True)[:n]
|
|
106
|
+
out = []
|
|
107
|
+
for sk, c in ranked:
|
|
108
|
+
cval = cplx[sk]
|
|
109
|
+
label = f'{stype[sk][:4]}-cplx-{int(cval)}'
|
|
110
|
+
out.append((label, cval, c))
|
|
111
|
+
return out
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def _per_area_draws(drops: list) -> dict:
|
|
115
|
+
"""Return {area: {n_draws, dominant_class}}."""
|
|
116
|
+
per: dict = defaultdict(lambda: {'n_draws': 0, 'by_class': Counter()})
|
|
117
|
+
for d in drops:
|
|
118
|
+
for r in d.rows:
|
|
119
|
+
p = os.path.join(r.drop_dir, 'pass_class_breakdown.parquet')
|
|
120
|
+
if not os.path.exists(p):
|
|
121
|
+
continue
|
|
122
|
+
try:
|
|
123
|
+
t = papq.read_table(p, columns=['area', 'draw_class', 'n_draws'])
|
|
124
|
+
except Exception:
|
|
125
|
+
continue
|
|
126
|
+
cols = {c: t.column(c).to_pylist() for c in t.column_names}
|
|
127
|
+
for i in range(t.num_rows):
|
|
128
|
+
a = cols['area'][i]
|
|
129
|
+
cls = cols['draw_class'][i] or 'other'
|
|
130
|
+
n = cols['n_draws'][i] or 0
|
|
131
|
+
per[a]['n_draws'] += n
|
|
132
|
+
per[a]['by_class'][cls] += n
|
|
133
|
+
res: dict = {}
|
|
134
|
+
for a, v in per.items():
|
|
135
|
+
dom = v['by_class'].most_common(1)[0][0] if v['by_class'] else '-'
|
|
136
|
+
res[a] = {'n_draws': v['n_draws'], 'dominant_class': dom}
|
|
137
|
+
return res
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def _top_areas_gpu(drops: list, n: int = 3) -> list:
|
|
141
|
+
"""Return [(area, gpu_s, draws)] top by gpu."""
|
|
142
|
+
agg: dict = defaultdict(lambda: {'gpu': 0.0, 'draws': 0})
|
|
143
|
+
for d in drops:
|
|
144
|
+
for r in d.rows:
|
|
145
|
+
p = os.path.join(r.drop_dir, 'frame_totals.parquet')
|
|
146
|
+
if not os.path.exists(p):
|
|
147
|
+
continue
|
|
148
|
+
try:
|
|
149
|
+
t = papq.read_table(p, columns=['total_gpu_duration_s', 'n_draws'])
|
|
150
|
+
except Exception:
|
|
151
|
+
continue
|
|
152
|
+
gpu_vals = t.column('total_gpu_duration_s').to_pylist()
|
|
153
|
+
draw_vals = t.column('n_draws').to_pylist()
|
|
154
|
+
for g, dr in zip(gpu_vals, draw_vals):
|
|
155
|
+
agg[r.area]['gpu'] += float(g or 0)
|
|
156
|
+
agg[r.area]['draws'] += int(dr or 0)
|
|
157
|
+
rows = [(a, v['gpu'], v['draws']) for a, v in agg.items()]
|
|
158
|
+
rows.sort(key=lambda x: x[1], reverse=True)
|
|
159
|
+
return rows[:n]
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def _worst_overdraw(drops: list, n: int = 3) -> list:
|
|
163
|
+
"""Return [(area, rt_label, reject_pct, n_samples)]. Rejection = 1 - passed%.
|
|
164
|
+
|
|
165
|
+
depth_test_failed is usually 0 in mobile (early-z handled by hardware);
|
|
166
|
+
real signal is shadow/backface/discard rejection ratio.
|
|
167
|
+
"""
|
|
168
|
+
agg: dict = defaultdict(lambda: {'n_samples': 0, 'n_passed': 0,
|
|
169
|
+
'drop_dir': '', 'capture': None})
|
|
170
|
+
for d in drops:
|
|
171
|
+
for r in d.rows:
|
|
172
|
+
p = os.path.join(r.drop_dir, 'pixel_history.parquet')
|
|
173
|
+
if not os.path.exists(p):
|
|
174
|
+
continue
|
|
175
|
+
try:
|
|
176
|
+
t = papq.read_table(p,
|
|
177
|
+
columns=['area', 'rt_id', 'passed', 'capture'])
|
|
178
|
+
except Exception:
|
|
179
|
+
continue
|
|
180
|
+
if t.num_rows == 0:
|
|
181
|
+
continue
|
|
182
|
+
cols = {c: t.column(c).to_pylist() for c in t.column_names}
|
|
183
|
+
for i in range(t.num_rows):
|
|
184
|
+
key = (cols['area'][i], cols['rt_id'][i])
|
|
185
|
+
agg[key]['n_samples'] += 1
|
|
186
|
+
if cols['passed'][i]:
|
|
187
|
+
agg[key]['n_passed'] += 1
|
|
188
|
+
if not agg[key]['drop_dir']:
|
|
189
|
+
agg[key]['drop_dir'] = r.drop_dir
|
|
190
|
+
agg[key]['capture'] = cols['capture'][i]
|
|
191
|
+
rows = []
|
|
192
|
+
for (area, rt_id), v in agg.items():
|
|
193
|
+
if v['n_samples'] < 20:
|
|
194
|
+
continue
|
|
195
|
+
reject_pct = (1.0 - v['n_passed'] / v['n_samples']) * 100.0
|
|
196
|
+
rt_label = base.label_for(v['drop_dir'], v['capture'], 'rt', rt_id) \
|
|
197
|
+
or f'rt_{rt_id}'
|
|
198
|
+
rows.append((area, rt_label, reject_pct, v['n_samples']))
|
|
199
|
+
rows.sort(key=lambda x: x[2], reverse=True)
|
|
200
|
+
return rows[:n]
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def _global_kpis(drops: list) -> list:
|
|
204
|
+
"""Cheap-to-compute global numbers from frame_totals across drops."""
|
|
205
|
+
total_gpu = 0.0
|
|
206
|
+
total_draws = 0
|
|
207
|
+
captures = 0
|
|
208
|
+
areas: set = set()
|
|
209
|
+
for d in drops:
|
|
210
|
+
captures += d.n_captures
|
|
211
|
+
areas.update(d.areas)
|
|
212
|
+
for r in d.rows:
|
|
213
|
+
p = os.path.join(r.drop_dir, 'frame_totals.parquet')
|
|
214
|
+
if not os.path.exists(p):
|
|
215
|
+
continue
|
|
216
|
+
try:
|
|
217
|
+
t = papq.read_table(p, columns=['total_gpu_duration_s', 'n_draws'])
|
|
218
|
+
except Exception:
|
|
219
|
+
continue
|
|
220
|
+
for v in t.column('total_gpu_duration_s').to_pylist():
|
|
221
|
+
if v is not None:
|
|
222
|
+
total_gpu += float(v)
|
|
223
|
+
for v in t.column('n_draws').to_pylist():
|
|
224
|
+
if v is not None:
|
|
225
|
+
total_draws += int(v)
|
|
226
|
+
return [
|
|
227
|
+
{'label': 'total gpu (s)', 'value': base.fmt_float(total_gpu, 3)},
|
|
228
|
+
{'label': 'total draws', 'value': base.fmt_int(total_draws)},
|
|
229
|
+
{'label': 'areas', 'value': base.fmt_int(len(areas))},
|
|
230
|
+
]
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def _card_table(rows: list, columns: list) -> str:
|
|
234
|
+
parts = ['<table class="report"><thead><tr>']
|
|
235
|
+
for col_name, _, num in columns:
|
|
236
|
+
cls = ' class="num"' if num else ''
|
|
237
|
+
parts.append(f'<th{cls}>{base.h(col_name)}</th>')
|
|
238
|
+
parts.append('</tr></thead><tbody>')
|
|
239
|
+
for row in rows:
|
|
240
|
+
parts.append('<tr>')
|
|
241
|
+
for col_name, fn, num in columns:
|
|
242
|
+
cls = ' class="num"' if num else ''
|
|
243
|
+
val = fn(row)
|
|
244
|
+
parts.append(f'<td{cls}>{val}</td>')
|
|
245
|
+
parts.append('</tr>')
|
|
246
|
+
parts.append('</tbody></table>')
|
|
247
|
+
return ''.join(parts)
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
def build(root: str, *, drops: list | None = None, ab=None) -> str:
|
|
251
|
+
if drops is None:
|
|
252
|
+
drops = base.discover_drops(root)
|
|
253
|
+
|
|
254
|
+
out_dir = os.path.join(root, '_reports')
|
|
255
|
+
os.makedirs(out_dir, exist_ok=True)
|
|
256
|
+
out_path = os.path.join(out_dir, 'index.html')
|
|
257
|
+
|
|
258
|
+
parts = [base.page_open('reports dashboard', hdr_offset_px=120)]
|
|
259
|
+
parts.append(base.header(
|
|
260
|
+
'reports dashboard',
|
|
261
|
+
drops=len(drops),
|
|
262
|
+
captures=sum(d.n_captures for d in drops),
|
|
263
|
+
build_ts=base.now_iso(),
|
|
264
|
+
crumb_depth=1,
|
|
265
|
+
current_page='dashboard',
|
|
266
|
+
))
|
|
267
|
+
|
|
268
|
+
# Summary bar: worst area by GPU rank + global counts
|
|
269
|
+
cards = []
|
|
270
|
+
|
|
271
|
+
top_a = _top_areas_gpu(drops, n=999)
|
|
272
|
+
n_areas = len(top_a)
|
|
273
|
+
total_draws = sum(t[2] for t in top_a)
|
|
274
|
+
if top_a:
|
|
275
|
+
worst_area, worst_gpu, worst_draws = top_a[0]
|
|
276
|
+
parts.append(base.summary_bar(
|
|
277
|
+
'worst gpu area',
|
|
278
|
+
worst_area,
|
|
279
|
+
sub=(f'rank 1 of {n_areas} areas; this area {base.fmt_int(worst_draws)} draws; '
|
|
280
|
+
f'all areas {base.fmt_int(total_draws)} draws'),
|
|
281
|
+
link_href=f'trend_table.html#gpu',
|
|
282
|
+
link_text='trend',
|
|
283
|
+
tone='neutral',
|
|
284
|
+
))
|
|
285
|
+
top_a = top_a[:3]
|
|
286
|
+
body_tt = _card_table(
|
|
287
|
+
top_a,
|
|
288
|
+
[
|
|
289
|
+
('area', lambda r: base.h(r[0]), False),
|
|
290
|
+
('gpu (s)', lambda r: base.fmt_float(r[1], 3), True),
|
|
291
|
+
('draws', lambda r: base.fmt_int(r[2]), True),
|
|
292
|
+
]
|
|
293
|
+
)
|
|
294
|
+
cards.append(
|
|
295
|
+
'<a class="dash-card" href="trend_table.html">'
|
|
296
|
+
'<h3>trend table</h3>'
|
|
297
|
+
f'{body_tt}'
|
|
298
|
+
'</a>'
|
|
299
|
+
)
|
|
300
|
+
|
|
301
|
+
# Card: instancing
|
|
302
|
+
top_m = _top_meshes(root, drops)
|
|
303
|
+
body_im = _card_table(
|
|
304
|
+
top_m,
|
|
305
|
+
[
|
|
306
|
+
('mesh', lambda r: base.h(r[0]), False),
|
|
307
|
+
('repeat', lambda r: base.fmt_int(r[1]), True),
|
|
308
|
+
('indices typ', lambda r: base.fmt_int(r[2]), True),
|
|
309
|
+
]
|
|
310
|
+
)
|
|
311
|
+
cards.append(
|
|
312
|
+
'<a class="dash-card" href="instancing_opportunities.html">'
|
|
313
|
+
'<h3>instancing opportunities</h3>'
|
|
314
|
+
f'{body_im}'
|
|
315
|
+
'</a>'
|
|
316
|
+
)
|
|
317
|
+
|
|
318
|
+
# Card: pass gpu
|
|
319
|
+
top_p = _top_passes(drops)
|
|
320
|
+
body_pg = _card_table(
|
|
321
|
+
top_p,
|
|
322
|
+
[
|
|
323
|
+
('area', lambda r: base.h(r[0]), False),
|
|
324
|
+
('marker', lambda r: base.safe_chrome_text(base.trunc_left(r[1], 32)), False),
|
|
325
|
+
('gpu (s)', lambda r: base.fmt_float(r[2], 3), True),
|
|
326
|
+
]
|
|
327
|
+
)
|
|
328
|
+
cards.append(
|
|
329
|
+
'<a class="dash-card" href="pass_gpu.html">'
|
|
330
|
+
'<h3>pass gpu</h3>'
|
|
331
|
+
f'{body_pg}'
|
|
332
|
+
'</a>'
|
|
333
|
+
)
|
|
334
|
+
|
|
335
|
+
# Card: shader hotlist
|
|
336
|
+
top_s = _top_shaders(root)
|
|
337
|
+
body_sh = _card_table(
|
|
338
|
+
top_s,
|
|
339
|
+
[
|
|
340
|
+
('shader', lambda r: base.h(r[0]), False),
|
|
341
|
+
('complexity', lambda r: base.fmt_float(r[1], 2), True),
|
|
342
|
+
('cost proxy', lambda r: base.fmt_float(r[2], 1), True),
|
|
343
|
+
]
|
|
344
|
+
)
|
|
345
|
+
cards.append(
|
|
346
|
+
'<a class="dash-card" href="shader_hotlist.html">'
|
|
347
|
+
'<h3>shader hotlist</h3>'
|
|
348
|
+
f'{body_sh}'
|
|
349
|
+
'</a>'
|
|
350
|
+
)
|
|
351
|
+
|
|
352
|
+
# Card: overdraw — by rejection ratio (1 - passed%)
|
|
353
|
+
wo = _worst_overdraw(drops)
|
|
354
|
+
body_od = _card_table(
|
|
355
|
+
wo,
|
|
356
|
+
[
|
|
357
|
+
('area', lambda r: base.h(r[0]), False),
|
|
358
|
+
('rt', lambda r: base.h(r[1]), False),
|
|
359
|
+
('rejected %', lambda r: base.fmt_pct(r[2]), True),
|
|
360
|
+
]
|
|
361
|
+
)
|
|
362
|
+
cards.append(
|
|
363
|
+
'<a class="dash-card" href="overdraw.html">'
|
|
364
|
+
'<h3>overdraw</h3>'
|
|
365
|
+
f'{body_od}'
|
|
366
|
+
'</a>'
|
|
367
|
+
)
|
|
368
|
+
|
|
369
|
+
# Card: draws by class — top 5 areas by draw count, dominant class
|
|
370
|
+
pa = _per_area_draws(drops)
|
|
371
|
+
pa_rows = sorted(pa.items(), key=lambda kv: kv[1]['n_draws'], reverse=True)[:5]
|
|
372
|
+
body_dc = _card_table(
|
|
373
|
+
pa_rows,
|
|
374
|
+
[
|
|
375
|
+
('area', lambda r: base.h(r[0]), False),
|
|
376
|
+
('draws', lambda r: base.fmt_int(r[1]['n_draws']), True),
|
|
377
|
+
('dominant', lambda r: base.h(r[1]['dominant_class']), False),
|
|
378
|
+
]
|
|
379
|
+
)
|
|
380
|
+
cards.append(
|
|
381
|
+
'<a class="dash-card" href="draws_by_class.html">'
|
|
382
|
+
'<h3>draws by class</h3>'
|
|
383
|
+
f'{body_dc}'
|
|
384
|
+
'</a>'
|
|
385
|
+
)
|
|
386
|
+
|
|
387
|
+
parts.append(
|
|
388
|
+
'<rdc-search-cards data-target=".dash-grid">'
|
|
389
|
+
'<label for="rdc-search">filter</label>'
|
|
390
|
+
'<input id="rdc-search" type="search" placeholder="filter cards">'
|
|
391
|
+
'<span class="rdc-count"></span>'
|
|
392
|
+
'</rdc-search-cards>'
|
|
393
|
+
)
|
|
394
|
+
parts.append(f'<div class="dash-grid">{"".join(cards)}</div>')
|
|
395
|
+
|
|
396
|
+
# A/B section
|
|
397
|
+
ab_root = os.path.join(out_dir, 'ab')
|
|
398
|
+
if os.path.isdir(ab_root):
|
|
399
|
+
ab_pairs = sorted(d for d in os.listdir(ab_root)
|
|
400
|
+
if os.path.isdir(os.path.join(ab_root, d)))
|
|
401
|
+
if ab_pairs:
|
|
402
|
+
parts.append(f'<h2 id="ab">a/b comparisons</h2>')
|
|
403
|
+
parts.append('<div class="pair-list">')
|
|
404
|
+
for pair in ab_pairs:
|
|
405
|
+
files = sorted(f for f in os.listdir(os.path.join(ab_root, pair))
|
|
406
|
+
if f.endswith('.html'))
|
|
407
|
+
chips = ''.join(
|
|
408
|
+
f'<a href="ab/{base.h(pair)}/{base.h(f)}" data-link-kind="primary">{base.h(f[:-5])}</a>'
|
|
409
|
+
for f in files
|
|
410
|
+
)
|
|
411
|
+
parts.append(
|
|
412
|
+
f'<div class="pair-group">'
|
|
413
|
+
f'<h3>{base.h(pair)}</h3>'
|
|
414
|
+
f'<div class="chip-cluster">{chips}</div>'
|
|
415
|
+
f'</div>'
|
|
416
|
+
)
|
|
417
|
+
parts.append('</div>')
|
|
418
|
+
|
|
419
|
+
parts.append(base.page_close())
|
|
420
|
+
|
|
421
|
+
return base.write_report(out_path, parts)
|
|
422
|
+
|
|
423
|
+
|
|
424
|
+
if __name__ == '__main__':
|
|
425
|
+
sys.exit(base.run_report(build, module_name='_dashboard'))
|
bobframes/reports/ab.py
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"""Unified A/B entry point. Generates all 6 reports for one drop pair.
|
|
2
|
+
|
|
3
|
+
Usage:
|
|
4
|
+
python -m bobframes.reports.ab \\
|
|
5
|
+
--baseline-label r110565 \\
|
|
6
|
+
--compare-label r110600 \\
|
|
7
|
+
[--baseline-date 2026-05-27] \\
|
|
8
|
+
[--compare-date 2026-06-15] \\
|
|
9
|
+
[--root .]
|
|
10
|
+
|
|
11
|
+
Writes _reports/ab/<labelA>_vs_<labelB>/<name>.html for each report.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import argparse
|
|
17
|
+
import os
|
|
18
|
+
import sys
|
|
19
|
+
|
|
20
|
+
from . import base
|
|
21
|
+
from . import draws_by_class as report_draws_by_class
|
|
22
|
+
from . import trend_table as report_trend
|
|
23
|
+
from . import instancing_opportunities as report_instancing
|
|
24
|
+
from . import pass_gpu as report_pass_gpu
|
|
25
|
+
from . import shader_hotlist as report_shader
|
|
26
|
+
from . import overdraw as report_overdraw
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
_MODULES = [
|
|
30
|
+
report_draws_by_class,
|
|
31
|
+
report_trend,
|
|
32
|
+
report_instancing,
|
|
33
|
+
report_pass_gpu,
|
|
34
|
+
report_shader,
|
|
35
|
+
report_overdraw,
|
|
36
|
+
]
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def main(argv: list[str]) -> int:
|
|
40
|
+
ap = argparse.ArgumentParser(prog='bobframes.reports.ab')
|
|
41
|
+
ap.add_argument('root', nargs='?', default='.')
|
|
42
|
+
# Hidden one-release alias for the old --root flag (positional is canonical, §4).
|
|
43
|
+
ap.add_argument('--root', dest='root', default=argparse.SUPPRESS, help=argparse.SUPPRESS)
|
|
44
|
+
ap.add_argument('--baseline-label', required=True)
|
|
45
|
+
ap.add_argument('--compare-label', required=True)
|
|
46
|
+
ap.add_argument('--baseline-date', default=None)
|
|
47
|
+
ap.add_argument('--compare-date', default=None)
|
|
48
|
+
args = ap.parse_args(argv)
|
|
49
|
+
|
|
50
|
+
root = os.path.abspath(args.root)
|
|
51
|
+
baseline = base.resolve_drop_set(root, label=args.baseline_label,
|
|
52
|
+
date=args.baseline_date)
|
|
53
|
+
compare = base.resolve_drop_set(root, label=args.compare_label,
|
|
54
|
+
date=args.compare_date)
|
|
55
|
+
if not baseline:
|
|
56
|
+
print(f'baseline not found: label={args.baseline_label}, '
|
|
57
|
+
f'date={args.baseline_date}', file=sys.stderr)
|
|
58
|
+
return 2
|
|
59
|
+
if not compare:
|
|
60
|
+
print(f'compare not found: label={args.compare_label}, '
|
|
61
|
+
f'date={args.compare_date}', file=sys.stderr)
|
|
62
|
+
return 2
|
|
63
|
+
|
|
64
|
+
print(f'a/b: {baseline.key} ({baseline.n_captures} captures) '
|
|
65
|
+
f'vs {compare.key} ({compare.n_captures} captures)')
|
|
66
|
+
|
|
67
|
+
drops = [baseline, compare]
|
|
68
|
+
ab = (baseline, compare)
|
|
69
|
+
for mod in _MODULES:
|
|
70
|
+
try:
|
|
71
|
+
out = mod.build(root, drops=drops, ab=ab)
|
|
72
|
+
print(f' wrote {out}')
|
|
73
|
+
except Exception as e:
|
|
74
|
+
print(f' {mod.__name__} FAILED: {e}', file=sys.stderr)
|
|
75
|
+
return 1
|
|
76
|
+
# Rebuild dashboard so its a/b table picks up new pair
|
|
77
|
+
from . import _dashboard as report_dashboard
|
|
78
|
+
try:
|
|
79
|
+
out = report_dashboard.build(root)
|
|
80
|
+
print(f' refreshed dashboard {out}')
|
|
81
|
+
except Exception as e:
|
|
82
|
+
print(f' dashboard refresh FAILED: {e}', file=sys.stderr)
|
|
83
|
+
return 1
|
|
84
|
+
return 0
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
if __name__ == '__main__':
|
|
88
|
+
sys.exit(main(sys.argv[1:]))
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
"""Shared chrome for Layer 2 reports.
|
|
2
|
+
|
|
3
|
+
This module is now a thin facade re-exporting from the topic modules below.
|
|
4
|
+
Existing reports keep `from . import base` working unchanged.
|
|
5
|
+
|
|
6
|
+
Topic modules:
|
|
7
|
+
- chrome: CSS tokens, page open/close, header, KPI strip, section card, legend, footer, ab_strip
|
|
8
|
+
- formatters: fmt_int/float/pct/bytes/id_short, mesh_hash_short, trunc_mid/left, safe_chrome_text
|
|
9
|
+
- delta: delta_cell, delta_pill, rank_pill, inline_bar, class_segments_bar, sparkline_svg
|
|
10
|
+
- discovery: DropRow, DropSet, discover_drops, resolve_drop_set, ok_capture_set
|
|
11
|
+
- cache: load_global_entities, load_labels, label_for, cache_dir, cache_path, build_per_drop_cache
|
|
12
|
+
- cli: run_report, ab_subdir, output_path, now_iso, _lint_or_raise, write_report, crumb_depth, rel_path_to_drop_index, rel_path_to_drop_file
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
from .chrome import (
|
|
18
|
+
DRAW_CLASSES,
|
|
19
|
+
_FAVICON_HREF,
|
|
20
|
+
ab_picker,
|
|
21
|
+
ab_picker_for,
|
|
22
|
+
ab_strip,
|
|
23
|
+
chrome_css,
|
|
24
|
+
class_color_var,
|
|
25
|
+
components_js,
|
|
26
|
+
design_tokens_css,
|
|
27
|
+
footer_legend,
|
|
28
|
+
h,
|
|
29
|
+
header,
|
|
30
|
+
icon,
|
|
31
|
+
kpi_chip,
|
|
32
|
+
kpi_strip,
|
|
33
|
+
legend,
|
|
34
|
+
link,
|
|
35
|
+
page_close,
|
|
36
|
+
page_open,
|
|
37
|
+
section_card,
|
|
38
|
+
summary_bar,
|
|
39
|
+
)
|
|
40
|
+
from .formatters import (
|
|
41
|
+
_BANNED_CHROME_CHARS,
|
|
42
|
+
fmt_bytes,
|
|
43
|
+
fmt_float,
|
|
44
|
+
fmt_id_short,
|
|
45
|
+
fmt_int,
|
|
46
|
+
fmt_pct,
|
|
47
|
+
mesh_hash_short,
|
|
48
|
+
pass_short,
|
|
49
|
+
pass_suffix,
|
|
50
|
+
safe_chrome_text,
|
|
51
|
+
trunc_left,
|
|
52
|
+
trunc_mid,
|
|
53
|
+
)
|
|
54
|
+
from .delta import (
|
|
55
|
+
class_segments_bar,
|
|
56
|
+
delta_cell,
|
|
57
|
+
delta_pill,
|
|
58
|
+
inline_bar,
|
|
59
|
+
rank_pill,
|
|
60
|
+
sparkline_svg,
|
|
61
|
+
)
|
|
62
|
+
from .discovery import (
|
|
63
|
+
DropRow,
|
|
64
|
+
DropSet,
|
|
65
|
+
discover_drops,
|
|
66
|
+
ok_capture_set,
|
|
67
|
+
resolve_drop_set,
|
|
68
|
+
)
|
|
69
|
+
from .cache import (
|
|
70
|
+
_read_drop_parquet,
|
|
71
|
+
build_per_drop_cache,
|
|
72
|
+
cache_dir,
|
|
73
|
+
cache_path,
|
|
74
|
+
label_for,
|
|
75
|
+
load_global_entities,
|
|
76
|
+
load_labels,
|
|
77
|
+
)
|
|
78
|
+
from .cli import (
|
|
79
|
+
_lint_or_raise,
|
|
80
|
+
ab_subdir,
|
|
81
|
+
crumb_depth,
|
|
82
|
+
now_iso,
|
|
83
|
+
output_path,
|
|
84
|
+
rel_path_to_drop_file,
|
|
85
|
+
rel_path_to_drop_index,
|
|
86
|
+
run_report,
|
|
87
|
+
write_report,
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
__all__ = [
|
|
92
|
+
# chrome
|
|
93
|
+
'DRAW_CLASSES', 'ab_picker', 'ab_picker_for', 'ab_strip',
|
|
94
|
+
'chrome_css', 'class_color_var',
|
|
95
|
+
'components_js', 'design_tokens_css', 'footer_legend', 'h', 'header',
|
|
96
|
+
'icon', 'kpi_chip', 'kpi_strip', 'legend', 'link',
|
|
97
|
+
'page_close', 'page_open', 'section_card', 'summary_bar',
|
|
98
|
+
# formatters
|
|
99
|
+
'fmt_bytes', 'fmt_float', 'fmt_id_short', 'fmt_int', 'fmt_pct',
|
|
100
|
+
'mesh_hash_short', 'pass_short', 'pass_suffix', 'safe_chrome_text',
|
|
101
|
+
'trunc_left', 'trunc_mid',
|
|
102
|
+
# delta
|
|
103
|
+
'class_segments_bar', 'delta_cell', 'delta_pill', 'inline_bar',
|
|
104
|
+
'rank_pill', 'sparkline_svg',
|
|
105
|
+
# discovery
|
|
106
|
+
'DropRow', 'DropSet', 'discover_drops', 'ok_capture_set', 'resolve_drop_set',
|
|
107
|
+
# cache
|
|
108
|
+
'build_per_drop_cache', 'cache_dir', 'cache_path', 'label_for',
|
|
109
|
+
'load_global_entities', 'load_labels',
|
|
110
|
+
# cli
|
|
111
|
+
'_lint_or_raise', 'ab_subdir', 'crumb_depth', 'now_iso',
|
|
112
|
+
'output_path', 'rel_path_to_drop_file', 'rel_path_to_drop_index',
|
|
113
|
+
'run_report', 'write_report',
|
|
114
|
+
]
|