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
|
@@ -0,0 +1,444 @@
|
|
|
1
|
+
"""Trend table: per-area KPI matrices across drop_dates.
|
|
2
|
+
|
|
3
|
+
One <h2> per KPI, rows = area, columns = drop_dates (+ delta cols + sparkline).
|
|
4
|
+
Plus per-area class-count matrix at the end.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import json
|
|
10
|
+
import os
|
|
11
|
+
import sys
|
|
12
|
+
from collections import defaultdict
|
|
13
|
+
|
|
14
|
+
import pyarrow.parquet as papq
|
|
15
|
+
|
|
16
|
+
from . import base
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
# (col_name, label, fmt, lower_is_better, regression_pct)
|
|
20
|
+
KPIS = [
|
|
21
|
+
('total_gpu_duration_s', 'gpu (s)', '{:+,.3f}', True, 10.0),
|
|
22
|
+
('n_draws', 'draws', '{:+,.0f}', True, 10.0),
|
|
23
|
+
('vbo_bytes_bound_derived', 'vbo bytes', '{:+,.0f}', True, 15.0),
|
|
24
|
+
('ibo_bytes_bound_derived', 'ibo bytes', '{:+,.0f}', True, 15.0),
|
|
25
|
+
('program_switches', 'prog switches', '{:+,.0f}', True, 20.0),
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
_INT_KPIS = {'n_draws', 'vbo_bytes_bound_derived', 'ibo_bytes_bound_derived',
|
|
29
|
+
'program_switches'}
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _aggregate_frame_totals(drop: base.DropSet, ok_caps: set) -> dict:
|
|
33
|
+
"""Return {area: {kpi: sum}} from frame_totals.parquet."""
|
|
34
|
+
out: dict = defaultdict(lambda: defaultdict(float))
|
|
35
|
+
for r in drop.rows:
|
|
36
|
+
p = os.path.join(r.drop_dir, 'frame_totals.parquet')
|
|
37
|
+
if not os.path.exists(p):
|
|
38
|
+
continue
|
|
39
|
+
try:
|
|
40
|
+
t = papq.read_table(p)
|
|
41
|
+
except Exception:
|
|
42
|
+
continue
|
|
43
|
+
cols = {c: t.column(c).to_pylist() for c in t.column_names}
|
|
44
|
+
for i in range(t.num_rows):
|
|
45
|
+
cap = cols['capture'][i]
|
|
46
|
+
key = (r.area, r.drop_date, r.drop_label, cap)
|
|
47
|
+
if ok_caps and key not in ok_caps:
|
|
48
|
+
continue
|
|
49
|
+
for kpi, *_ in KPIS:
|
|
50
|
+
if kpi in cols:
|
|
51
|
+
v = cols[kpi][i]
|
|
52
|
+
if v is not None:
|
|
53
|
+
out[r.area][kpi] += v
|
|
54
|
+
return out
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _aggregate_buffer_bytes(drop: base.DropSet, ok_caps: set) -> dict:
|
|
58
|
+
"""Return {area: {vbo_bytes_bound_derived, ibo_..., ubo_...}}.
|
|
59
|
+
|
|
60
|
+
Buffer used_as_* flags are unpopulated (parser limitation).
|
|
61
|
+
Derive via joins:
|
|
62
|
+
vbo_bytes = sum allocated_size_bytes of distinct buffer_ids in vertex_inputs
|
|
63
|
+
ibo_bytes = sum allocated_size_bytes of distinct ibo_ids in draws
|
|
64
|
+
ubo_bytes = sum allocated_size_bytes of distinct resource_ids in
|
|
65
|
+
draw_bindings where slot_kind='ubo'
|
|
66
|
+
"""
|
|
67
|
+
out: dict = defaultdict(lambda: defaultdict(int))
|
|
68
|
+
|
|
69
|
+
for r in drop.rows:
|
|
70
|
+
ao = r.drop_dir
|
|
71
|
+
|
|
72
|
+
bufs_p = os.path.join(ao, 'buffers.parquet')
|
|
73
|
+
if not os.path.exists(bufs_p):
|
|
74
|
+
continue
|
|
75
|
+
try:
|
|
76
|
+
bt = papq.read_table(bufs_p,
|
|
77
|
+
columns=['capture', 'buffer_id', 'allocated_size_bytes'])
|
|
78
|
+
except Exception:
|
|
79
|
+
continue
|
|
80
|
+
bc = {n: bt.column(n).to_pylist() for n in bt.column_names}
|
|
81
|
+
size_by: dict[tuple, int] = {}
|
|
82
|
+
for i in range(bt.num_rows):
|
|
83
|
+
size_by[(bc['capture'][i], bc['buffer_id'][i])] = bc['allocated_size_bytes'][i] or 0
|
|
84
|
+
|
|
85
|
+
# vbo via vertex_inputs
|
|
86
|
+
vi_p = os.path.join(ao, 'vertex_inputs.parquet')
|
|
87
|
+
if os.path.exists(vi_p):
|
|
88
|
+
try:
|
|
89
|
+
vi = papq.read_table(vi_p, columns=['capture', 'buffer_id'])
|
|
90
|
+
except Exception:
|
|
91
|
+
vi = None
|
|
92
|
+
if vi is not None:
|
|
93
|
+
seen: set = set()
|
|
94
|
+
vc = {n: vi.column(n).to_pylist() for n in vi.column_names}
|
|
95
|
+
for i in range(vi.num_rows):
|
|
96
|
+
cap = vc['capture'][i]
|
|
97
|
+
bid = vc['buffer_id'][i]
|
|
98
|
+
if not bid:
|
|
99
|
+
continue
|
|
100
|
+
key = (r.area, r.drop_date, r.drop_label, cap)
|
|
101
|
+
if ok_caps and key not in ok_caps:
|
|
102
|
+
continue
|
|
103
|
+
pk = (r.area, cap, bid)
|
|
104
|
+
if pk in seen:
|
|
105
|
+
continue
|
|
106
|
+
seen.add(pk)
|
|
107
|
+
out[r.area]['vbo_bytes_bound_derived'] += size_by.get((cap, bid), 0)
|
|
108
|
+
|
|
109
|
+
# ibo via draws.ibo_id
|
|
110
|
+
draws_p = os.path.join(ao, 'draws.parquet')
|
|
111
|
+
if os.path.exists(draws_p):
|
|
112
|
+
try:
|
|
113
|
+
schema_cols = set(papq.read_schema(draws_p).names)
|
|
114
|
+
want = ['capture', 'ibo_id'] if 'ibo_id' in schema_cols else None
|
|
115
|
+
if want:
|
|
116
|
+
dt = papq.read_table(draws_p, columns=want)
|
|
117
|
+
else:
|
|
118
|
+
dt = None
|
|
119
|
+
except Exception:
|
|
120
|
+
dt = None
|
|
121
|
+
if dt is not None:
|
|
122
|
+
seen = set()
|
|
123
|
+
dc = {n: dt.column(n).to_pylist() for n in dt.column_names}
|
|
124
|
+
for i in range(dt.num_rows):
|
|
125
|
+
cap = dc['capture'][i]
|
|
126
|
+
ibo = dc['ibo_id'][i]
|
|
127
|
+
if not ibo:
|
|
128
|
+
continue
|
|
129
|
+
key = (r.area, r.drop_date, r.drop_label, cap)
|
|
130
|
+
if ok_caps and key not in ok_caps:
|
|
131
|
+
continue
|
|
132
|
+
pk = (r.area, cap, ibo)
|
|
133
|
+
if pk in seen:
|
|
134
|
+
continue
|
|
135
|
+
seen.add(pk)
|
|
136
|
+
out[r.area]['ibo_bytes_bound_derived'] += size_by.get((cap, ibo), 0)
|
|
137
|
+
|
|
138
|
+
# ubo via draw_bindings where slot_kind='ubo'
|
|
139
|
+
db_p = os.path.join(ao, 'draw_bindings.parquet')
|
|
140
|
+
if os.path.exists(db_p):
|
|
141
|
+
try:
|
|
142
|
+
db = papq.read_table(db_p,
|
|
143
|
+
columns=['capture', 'slot_kind', 'resource_id'])
|
|
144
|
+
except Exception:
|
|
145
|
+
db = None
|
|
146
|
+
if db is not None:
|
|
147
|
+
seen = set()
|
|
148
|
+
dbc = {n: db.column(n).to_pylist() for n in db.column_names}
|
|
149
|
+
for i in range(db.num_rows):
|
|
150
|
+
if dbc['slot_kind'][i] != 'ubo':
|
|
151
|
+
continue
|
|
152
|
+
cap = dbc['capture'][i]
|
|
153
|
+
rid = dbc['resource_id'][i]
|
|
154
|
+
if not rid:
|
|
155
|
+
continue
|
|
156
|
+
key = (r.area, r.drop_date, r.drop_label, cap)
|
|
157
|
+
if ok_caps and key not in ok_caps:
|
|
158
|
+
continue
|
|
159
|
+
pk = (r.area, cap, rid)
|
|
160
|
+
if pk in seen:
|
|
161
|
+
continue
|
|
162
|
+
seen.add(pk)
|
|
163
|
+
out[r.area]['ubo_bytes_bound_derived'] += size_by.get((cap, rid), 0)
|
|
164
|
+
|
|
165
|
+
return out
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def _aggregate_class_counts(drop: base.DropSet, ok_caps: set) -> dict:
|
|
169
|
+
"""Return {area: {draw_class: n_draws}}."""
|
|
170
|
+
out: dict = defaultdict(lambda: defaultdict(int))
|
|
171
|
+
for r in drop.rows:
|
|
172
|
+
p = os.path.join(r.drop_dir, 'pass_class_breakdown.parquet')
|
|
173
|
+
if not os.path.exists(p):
|
|
174
|
+
continue
|
|
175
|
+
try:
|
|
176
|
+
t = papq.read_table(p, columns=['capture', 'draw_class', 'n_draws'])
|
|
177
|
+
except Exception:
|
|
178
|
+
continue
|
|
179
|
+
c = {n: t.column(n).to_pylist() for n in t.column_names}
|
|
180
|
+
for i in range(t.num_rows):
|
|
181
|
+
cap = c['capture'][i]
|
|
182
|
+
key = (r.area, r.drop_date, r.drop_label, cap)
|
|
183
|
+
if ok_caps and key not in ok_caps:
|
|
184
|
+
continue
|
|
185
|
+
cls = c['draw_class'][i] or 'other'
|
|
186
|
+
out[r.area][cls] += c['n_draws'][i] or 0
|
|
187
|
+
return out
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def _device_string(drop: base.DropSet) -> str:
|
|
191
|
+
for r in drop.rows:
|
|
192
|
+
p = os.path.join(r.drop_dir, 'frame_metadata.jsonl')
|
|
193
|
+
if not os.path.exists(p):
|
|
194
|
+
continue
|
|
195
|
+
try:
|
|
196
|
+
with open(p, 'r', encoding='utf-8') as f:
|
|
197
|
+
for line in f:
|
|
198
|
+
try:
|
|
199
|
+
o = json.loads(line)
|
|
200
|
+
except json.JSONDecodeError:
|
|
201
|
+
continue
|
|
202
|
+
s = o.get('gl_renderer_string') or o.get('gl_renderer') or ''
|
|
203
|
+
if s:
|
|
204
|
+
return s
|
|
205
|
+
except OSError:
|
|
206
|
+
continue
|
|
207
|
+
return ''
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def _kpi_matrix(kpi: str, label: str, fmt: str, lower_is_better, threshold,
|
|
211
|
+
per_drop_area_data: list, areas: list, drops: list) -> str:
|
|
212
|
+
"""Render one KPI matrix as h2 + table-wrap (one per KPI when n_drops >= 2)."""
|
|
213
|
+
is_int = kpi in _INT_KPIS
|
|
214
|
+
|
|
215
|
+
parts = []
|
|
216
|
+
parts.append(f'<rdc-sticky-h2><h2 id="{base.h(kpi)}">{base.h(label)}</h2></rdc-sticky-h2>')
|
|
217
|
+
parts.append('<div class="table-wrap"><rdc-sortable-table>')
|
|
218
|
+
parts.append('<table class="report"><thead><tr>')
|
|
219
|
+
parts.append('<th>area</th>')
|
|
220
|
+
n_drops = len(drops)
|
|
221
|
+
for i, d in enumerate(drops):
|
|
222
|
+
parts.append(f'<th class="num">{base.h(d.key)}</th>')
|
|
223
|
+
if i > 0:
|
|
224
|
+
latest_cls = ' delta-latest' if i == n_drops - 1 else ''
|
|
225
|
+
parts.append(f'<th class="num{latest_cls}">delta</th>')
|
|
226
|
+
if n_drops >= 3:
|
|
227
|
+
parts.append('<th class="num">trend</th>')
|
|
228
|
+
parts.append('</tr></thead><tbody>')
|
|
229
|
+
|
|
230
|
+
for area in areas:
|
|
231
|
+
parts.append('<tr>')
|
|
232
|
+
parts.append(f'<td>{base.h(area)}</td>')
|
|
233
|
+
series: list = []
|
|
234
|
+
prev = None
|
|
235
|
+
for i, d in enumerate(drops):
|
|
236
|
+
v = per_drop_area_data[i].get(area, {}).get(kpi)
|
|
237
|
+
series.append(v)
|
|
238
|
+
if is_int:
|
|
239
|
+
parts.append(f'<td class="num">{base.fmt_int(v)}</td>')
|
|
240
|
+
else:
|
|
241
|
+
parts.append(f'<td class="num">{base.fmt_float(v, 3)}</td>')
|
|
242
|
+
if i > 0:
|
|
243
|
+
cell = base.delta_cell(
|
|
244
|
+
v, prev,
|
|
245
|
+
lower_is_better=lower_is_better,
|
|
246
|
+
fmt=fmt,
|
|
247
|
+
regression_threshold_pct=threshold,
|
|
248
|
+
)
|
|
249
|
+
if i == n_drops - 1:
|
|
250
|
+
cell = cell.replace('class="delta', 'class="delta delta-latest', 1)
|
|
251
|
+
parts.append(cell)
|
|
252
|
+
prev = v
|
|
253
|
+
if n_drops >= 3:
|
|
254
|
+
parts.append(f'<td class="num">{base.sparkline_svg(series)}</td>')
|
|
255
|
+
parts.append('</tr>')
|
|
256
|
+
parts.append('</tbody></table></rdc-sortable-table></div>')
|
|
257
|
+
return '\n'.join(parts)
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
def _single_drop_matrix(per_drop_ft: list, areas: list, drops: list) -> str:
|
|
261
|
+
"""Render single wide matrix (rows=area, cols=KPI) when n_drops==1."""
|
|
262
|
+
parts = []
|
|
263
|
+
parts.append('<h2 id="matrix">per-area kpi matrix</h2>')
|
|
264
|
+
parts.append('<div class="table-wrap"><rdc-sortable-table>')
|
|
265
|
+
parts.append('<table class="report"><thead><tr>')
|
|
266
|
+
parts.append('<th>area</th>')
|
|
267
|
+
for kpi, label, *_ in KPIS:
|
|
268
|
+
parts.append(f'<th class="num">{base.h(label)}</th>')
|
|
269
|
+
parts.append('</tr></thead><tbody>')
|
|
270
|
+
data = per_drop_ft[0]
|
|
271
|
+
# Precompute per-column max for heatmap normalization
|
|
272
|
+
col_max: dict = {}
|
|
273
|
+
for kpi, _, _, _, _ in KPIS:
|
|
274
|
+
vals = [float(data.get(a, {}).get(kpi) or 0) for a in areas]
|
|
275
|
+
col_max[kpi] = max(vals) if vals else 0.0
|
|
276
|
+
for area in areas:
|
|
277
|
+
parts.append('<tr>')
|
|
278
|
+
parts.append(f'<td>{base.h(area)}</td>')
|
|
279
|
+
for kpi, _, _, _, _ in KPIS:
|
|
280
|
+
v = data.get(area, {}).get(kpi)
|
|
281
|
+
cmax = col_max[kpi]
|
|
282
|
+
if kpi in _INT_KPIS:
|
|
283
|
+
val_str = base.fmt_int(v)
|
|
284
|
+
else:
|
|
285
|
+
val_str = base.fmt_float(v, 3)
|
|
286
|
+
if v is not None and cmax > 0:
|
|
287
|
+
parts.append(
|
|
288
|
+
f'<td class="num"><rdc-heatmap-cell data-value="{v}" '
|
|
289
|
+
f'data-min="0" data-max="{cmax}" data-direction="hot">'
|
|
290
|
+
f'{val_str}</rdc-heatmap-cell></td>'
|
|
291
|
+
)
|
|
292
|
+
else:
|
|
293
|
+
parts.append(f'<td class="num">{val_str}</td>')
|
|
294
|
+
parts.append('</tr>')
|
|
295
|
+
parts.append('</tbody></table></rdc-sortable-table></div>')
|
|
296
|
+
return '\n'.join(parts)
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
def _class_count_matrix(per_drop_area_class: list, areas: list,
|
|
300
|
+
drops: list) -> str:
|
|
301
|
+
parts = []
|
|
302
|
+
parts.append('<rdc-sticky-h2><h2 id="class_counts">draws by class</h2></rdc-sticky-h2>')
|
|
303
|
+
parts.append('<div class="table-wrap"><rdc-sortable-table>')
|
|
304
|
+
parts.append('<table class="report"><thead><tr>')
|
|
305
|
+
parts.append('<th>area</th>')
|
|
306
|
+
single = len(drops) == 1
|
|
307
|
+
for d in drops:
|
|
308
|
+
for cls in base.DRAW_CLASSES:
|
|
309
|
+
head = base.h(cls) if single else f'{base.h(d.key)}/{base.h(cls)}'
|
|
310
|
+
parts.append(f'<th class="num">{head}</th>')
|
|
311
|
+
parts.append('</tr></thead><tbody>')
|
|
312
|
+
for area in areas:
|
|
313
|
+
parts.append('<tr>')
|
|
314
|
+
parts.append(f'<td>{base.h(area)}</td>')
|
|
315
|
+
for i in range(len(drops)):
|
|
316
|
+
cc = per_drop_area_class[i].get(area, {})
|
|
317
|
+
for cls in base.DRAW_CLASSES:
|
|
318
|
+
parts.append(f'<td class="num">{base.fmt_int(cc.get(cls, 0))}</td>')
|
|
319
|
+
parts.append('</tr>')
|
|
320
|
+
parts.append('</tbody></table></rdc-sortable-table></div>')
|
|
321
|
+
return '\n'.join(parts)
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
def build(root: str, *, drops: list | None = None, ab=None) -> str:
|
|
325
|
+
if drops is None:
|
|
326
|
+
drops = base.discover_drops(root)
|
|
327
|
+
if not drops:
|
|
328
|
+
out_path = base.output_path(root, 'trend_table', ab)
|
|
329
|
+
with open(out_path, 'w', encoding='utf-8') as f:
|
|
330
|
+
f.write(base.page_open('trend table'))
|
|
331
|
+
f.write(base.header('trend table', drops=0, captures=0,
|
|
332
|
+
build_ts=base.now_iso()))
|
|
333
|
+
f.write('<p class="note">no drops found in catalog</p>')
|
|
334
|
+
f.write(base.page_close())
|
|
335
|
+
base._lint_or_raise(out_path)
|
|
336
|
+
return out_path
|
|
337
|
+
|
|
338
|
+
ok_caps = base.ok_capture_set(root)
|
|
339
|
+
|
|
340
|
+
per_drop_ft = []
|
|
341
|
+
per_drop_bytes = []
|
|
342
|
+
per_drop_class = []
|
|
343
|
+
device_strings = []
|
|
344
|
+
for d in drops:
|
|
345
|
+
ft = _aggregate_frame_totals(d, ok_caps)
|
|
346
|
+
bb = _aggregate_buffer_bytes(d, ok_caps)
|
|
347
|
+
for area in list(ft.keys()) + list(bb.keys()):
|
|
348
|
+
ft.setdefault(area, {})
|
|
349
|
+
ft[area].update(bb.get(area, {}))
|
|
350
|
+
per_drop_ft.append(ft)
|
|
351
|
+
per_drop_bytes.append(bb)
|
|
352
|
+
per_drop_class.append(_aggregate_class_counts(d, ok_caps))
|
|
353
|
+
device_strings.append(_device_string(d))
|
|
354
|
+
|
|
355
|
+
all_areas = sorted({a for d in per_drop_ft for a in d.keys()})
|
|
356
|
+
drop_keys_l = [d.key for d in drops]
|
|
357
|
+
|
|
358
|
+
body_attrs = {'data-multi-section': 'true'} if len(drops) > 1 else None
|
|
359
|
+
parts = [base.page_open('trend table', hdr_offset_px=120, body_attrs=body_attrs)]
|
|
360
|
+
parts.append(base.header(
|
|
361
|
+
'trend table',
|
|
362
|
+
drops=len(drops),
|
|
363
|
+
captures=sum(d.n_captures for d in drops),
|
|
364
|
+
build_ts=base.now_iso(),
|
|
365
|
+
crumb_depth=base.crumb_depth(ab),
|
|
366
|
+
))
|
|
367
|
+
if ab is not None:
|
|
368
|
+
baseline, compare = ab
|
|
369
|
+
parts.append(base.ab_strip(
|
|
370
|
+
ab,
|
|
371
|
+
baseline_suffix=f' ({baseline.n_captures} captures)',
|
|
372
|
+
compare_suffix=f' ({compare.n_captures} captures)',
|
|
373
|
+
))
|
|
374
|
+
parts.append(base.ab_picker_for(root, 'trend_table', ab=ab))
|
|
375
|
+
|
|
376
|
+
# Summary bar: worst KPI (n=1) or biggest regression (n>1)
|
|
377
|
+
if len(drops) == 1:
|
|
378
|
+
ft = per_drop_ft[0]
|
|
379
|
+
worst_area, worst_gpu = None, 0.0
|
|
380
|
+
for area, kpis in ft.items():
|
|
381
|
+
g = float(kpis.get('total_gpu_duration_s', 0) or 0)
|
|
382
|
+
if g > worst_gpu:
|
|
383
|
+
worst_gpu, worst_area = g, area
|
|
384
|
+
if worst_area is not None:
|
|
385
|
+
parts.append(base.summary_bar(
|
|
386
|
+
'worst gpu area',
|
|
387
|
+
worst_area,
|
|
388
|
+
sub=f'rank 1 of {len(all_areas)} areas',
|
|
389
|
+
tone='neutral',
|
|
390
|
+
))
|
|
391
|
+
else:
|
|
392
|
+
worst_pct = 0.0
|
|
393
|
+
worst_tuple = None
|
|
394
|
+
for kpi, label, *_rest in KPIS:
|
|
395
|
+
for area in all_areas:
|
|
396
|
+
prev = None
|
|
397
|
+
for di, drop in enumerate(drops):
|
|
398
|
+
cur = per_drop_ft[di].get(area, {}).get(kpi)
|
|
399
|
+
if cur is None:
|
|
400
|
+
continue
|
|
401
|
+
if prev is not None and prev > 0:
|
|
402
|
+
pct = 100.0 * (float(cur) - float(prev)) / float(prev)
|
|
403
|
+
if pct > worst_pct:
|
|
404
|
+
worst_pct = pct
|
|
405
|
+
worst_tuple = (area, label, pct, drops[di-1].key, drop.key)
|
|
406
|
+
prev = cur
|
|
407
|
+
if worst_tuple is not None:
|
|
408
|
+
area, label, pct, prev_key, cur_key = worst_tuple
|
|
409
|
+
tone = 'alarm' if pct >= 10.0 else 'warn'
|
|
410
|
+
parts.append(base.summary_bar(
|
|
411
|
+
'biggest regression',
|
|
412
|
+
f'{area} / {label} +{base.fmt_float(pct, 1)}%',
|
|
413
|
+
sub=f'{prev_key} to {cur_key}',
|
|
414
|
+
tone=tone,
|
|
415
|
+
))
|
|
416
|
+
|
|
417
|
+
if any(device_strings):
|
|
418
|
+
chips = []
|
|
419
|
+
for d, dev in zip(drops, device_strings):
|
|
420
|
+
chips.append(f'{base.h(d.key)}: {base.safe_chrome_text(dev) or "no metadata"}')
|
|
421
|
+
parts.append(f'<div class="device-strip">{" | ".join(chips)}</div>')
|
|
422
|
+
|
|
423
|
+
if len(drops) == 1:
|
|
424
|
+
parts.append(_single_drop_matrix(per_drop_ft, all_areas, drops))
|
|
425
|
+
else:
|
|
426
|
+
parts.append('<nav class="toc">')
|
|
427
|
+
for kpi, label, *_ in KPIS:
|
|
428
|
+
parts.append(f'<a href="#{base.h(kpi)}" data-link-kind="crumb">{base.h(label)}</a>')
|
|
429
|
+
parts.append('<a href="#class_counts" data-link-kind="crumb">draws by class</a>')
|
|
430
|
+
parts.append('</nav>')
|
|
431
|
+
for kpi, label, fmt, lib, thr in KPIS:
|
|
432
|
+
parts.append(_kpi_matrix(kpi, label, fmt, lib, thr,
|
|
433
|
+
per_drop_ft, all_areas, drops))
|
|
434
|
+
|
|
435
|
+
parts.append(_class_count_matrix(per_drop_class, all_areas, drops))
|
|
436
|
+
|
|
437
|
+
parts.append(base.page_close())
|
|
438
|
+
|
|
439
|
+
out_path = base.output_path(root, 'trend_table', ab)
|
|
440
|
+
return base.write_report(out_path, parts)
|
|
441
|
+
|
|
442
|
+
|
|
443
|
+
if __name__ == '__main__':
|
|
444
|
+
sys.exit(base.run_report(build, module_name='trend_table'))
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
"""Build _resource_labels.json sidecar from per-capture stage labels + Parquet.
|
|
2
|
+
|
|
3
|
+
The HTML browser reads this file and enriches ID columns at render time so
|
|
4
|
+
e.g. `tex_id=2184` shows as `2184 SceneDepthZ`.
|
|
5
|
+
|
|
6
|
+
Structure on disk:
|
|
7
|
+
{
|
|
8
|
+
"by_capture": {
|
|
9
|
+
"1": {
|
|
10
|
+
"texture": {"2184": "SceneDepthZ", ...},
|
|
11
|
+
"shader": {"2192": "compute 224B hash:39013910"},
|
|
12
|
+
"program": {"2193": "vs:2192 fs:0"},
|
|
13
|
+
"sampler": {"116": "..."},
|
|
14
|
+
"fbo": {"...": "..."},
|
|
15
|
+
"buffer": {"...": "..."}
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
`build_from_stage` runs at merge time when _stage/<cap>/labels.json + the
|
|
21
|
+
already-merged Parquet files are both present.
|
|
22
|
+
|
|
23
|
+
`build_from_outdir` rebuilds from an existing _analysis_out/ (when no
|
|
24
|
+
stage is available, e.g. --render-only mode).
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
from __future__ import annotations
|
|
28
|
+
|
|
29
|
+
import json
|
|
30
|
+
import os
|
|
31
|
+
from typing import Iterable
|
|
32
|
+
|
|
33
|
+
import pyarrow.parquet as papq
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _short_name_for_shader(shader_row: dict) -> str:
|
|
37
|
+
stype = shader_row.get('shader_type', '') or ''
|
|
38
|
+
src_len = int(shader_row.get('src_len', 0) or 0)
|
|
39
|
+
h = shader_row.get('src_hash', '') or ''
|
|
40
|
+
samples = int(shader_row.get('total_texture_samples', 0) or 0)
|
|
41
|
+
branches = int(shader_row.get('total_branches', 0) or 0)
|
|
42
|
+
loops = int(shader_row.get('total_loops', 0) or 0)
|
|
43
|
+
bits = []
|
|
44
|
+
if stype: bits.append(stype)
|
|
45
|
+
if src_len: bits.append(f'{src_len}B')
|
|
46
|
+
if h: bits.append(f'h:{h[:8]}')
|
|
47
|
+
if samples: bits.append(f'tex={samples}')
|
|
48
|
+
if branches: bits.append(f'br={branches}')
|
|
49
|
+
if loops: bits.append(f'lo={loops}')
|
|
50
|
+
return ' '.join(bits)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _short_name_for_program(prog_row: dict, shader_names: dict[int, str]) -> str:
|
|
54
|
+
vs = int(prog_row.get('vs_shader_id', 0) or 0)
|
|
55
|
+
fs = int(prog_row.get('fs_shader_id', 0) or 0)
|
|
56
|
+
cs = int(prog_row.get('cs_shader_id', 0) or 0)
|
|
57
|
+
bits = []
|
|
58
|
+
if vs: bits.append(f'vs:{vs}')
|
|
59
|
+
if fs: bits.append(f'fs:{fs}')
|
|
60
|
+
if cs: bits.append(f'cs:{cs}')
|
|
61
|
+
n_uniforms = int(prog_row.get('num_active_uniforms', 0) or 0)
|
|
62
|
+
if n_uniforms: bits.append(f'u={n_uniforms}')
|
|
63
|
+
return ' '.join(bits)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _per_capture_from_parquet(out_dir: str) -> dict[str, dict[str, dict[str, str]]]:
|
|
67
|
+
"""Read the drop's Parquet files and build per-capture label maps.
|
|
68
|
+
|
|
69
|
+
Pulls glObjectLabel-supplied names from textures/programs/samplers/fbos
|
|
70
|
+
columns, and synthesizes names for shaders + programs without labels.
|
|
71
|
+
"""
|
|
72
|
+
by_cap: dict[str, dict[str, dict[str, str]]] = {}
|
|
73
|
+
|
|
74
|
+
def _bag(cap: str) -> dict[str, dict[str, str]]:
|
|
75
|
+
return by_cap.setdefault(cap, {
|
|
76
|
+
'texture': {}, 'shader': {}, 'program': {},
|
|
77
|
+
'sampler': {}, 'fbo': {}, 'buffer': {},
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
def _read(table: str, id_col: str, label_col: str, kind: str) -> None:
|
|
81
|
+
p = os.path.join(out_dir, f'{table}.parquet')
|
|
82
|
+
if not os.path.exists(p):
|
|
83
|
+
return
|
|
84
|
+
cols = list(papq.read_schema(p).names)
|
|
85
|
+
want = ['capture', id_col]
|
|
86
|
+
if label_col in cols:
|
|
87
|
+
want.append(label_col)
|
|
88
|
+
try:
|
|
89
|
+
t = papq.read_table(p, columns=want)
|
|
90
|
+
except Exception:
|
|
91
|
+
return
|
|
92
|
+
cap_arr = t.column('capture').to_pylist()
|
|
93
|
+
id_arr = t.column(id_col).to_pylist()
|
|
94
|
+
lab_arr = t.column(label_col).to_pylist() if label_col in cols else [''] * t.num_rows
|
|
95
|
+
for c, i, l in zip(cap_arr, id_arr, lab_arr):
|
|
96
|
+
if not i:
|
|
97
|
+
continue
|
|
98
|
+
sid = str(i)
|
|
99
|
+
if l:
|
|
100
|
+
_bag(c)[kind][sid] = l
|
|
101
|
+
|
|
102
|
+
_read('textures', 'tex_id', 'label', 'texture')
|
|
103
|
+
_read('render_targets', 'rt_id', 'label', 'texture') # RTs go into texture bucket
|
|
104
|
+
_read('programs', 'program_id', 'label', 'program')
|
|
105
|
+
_read('samplers', 'sampler_id', 'label', 'sampler')
|
|
106
|
+
_read('fbos', 'fbo_id', 'label', 'fbo')
|
|
107
|
+
|
|
108
|
+
# Synthesize shader names from shaders.parquet
|
|
109
|
+
sh_path = os.path.join(out_dir, 'shaders.parquet')
|
|
110
|
+
if os.path.exists(sh_path):
|
|
111
|
+
t = papq.read_table(sh_path)
|
|
112
|
+
cols = t.column_names
|
|
113
|
+
idx = {c: t.column(c).to_pylist() for c in cols}
|
|
114
|
+
n = t.num_rows
|
|
115
|
+
for i in range(n):
|
|
116
|
+
cap = idx['capture'][i]
|
|
117
|
+
sid = idx['shader_id'][i]
|
|
118
|
+
if not sid:
|
|
119
|
+
continue
|
|
120
|
+
row = {c: idx[c][i] for c in cols}
|
|
121
|
+
name = _short_name_for_shader(row)
|
|
122
|
+
if name:
|
|
123
|
+
_bag(cap)['shader'][str(sid)] = name
|
|
124
|
+
|
|
125
|
+
# Synthesize program "name" (vs:N fs:M) if no glObjectLabel name set
|
|
126
|
+
pr_path = os.path.join(out_dir, 'programs.parquet')
|
|
127
|
+
if os.path.exists(pr_path):
|
|
128
|
+
t = papq.read_table(pr_path)
|
|
129
|
+
cols = t.column_names
|
|
130
|
+
idx = {c: t.column(c).to_pylist() for c in cols}
|
|
131
|
+
n = t.num_rows
|
|
132
|
+
for i in range(n):
|
|
133
|
+
cap = idx['capture'][i]
|
|
134
|
+
pid = idx['program_id'][i]
|
|
135
|
+
if not pid:
|
|
136
|
+
continue
|
|
137
|
+
existing = _bag(cap)['program'].get(str(pid), '')
|
|
138
|
+
if existing:
|
|
139
|
+
continue
|
|
140
|
+
row = {c: idx[c][i] for c in cols}
|
|
141
|
+
sname = _short_name_for_program(row, {})
|
|
142
|
+
if sname:
|
|
143
|
+
_bag(cap)['program'][str(pid)] = sname
|
|
144
|
+
|
|
145
|
+
return by_cap
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def write_resource_labels(out_dir: str) -> str:
|
|
149
|
+
"""Read _analysis_out/*.parquet, produce _resource_labels.json sidecar."""
|
|
150
|
+
by_cap = _per_capture_from_parquet(out_dir)
|
|
151
|
+
path = os.path.join(out_dir, '_resource_labels.json')
|
|
152
|
+
with open(path, 'w', encoding='utf-8') as f:
|
|
153
|
+
json.dump({'by_capture': by_cap}, f, separators=(',', ':'))
|
|
154
|
+
return path
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
if __name__ == '__main__':
|
|
158
|
+
import sys
|
|
159
|
+
p = sys.argv[1] if len(sys.argv) > 1 else '.'
|
|
160
|
+
out = write_resource_labels(p)
|
|
161
|
+
sz = os.path.getsize(out)
|
|
162
|
+
print(f'wrote {out} ({sz} bytes)')
|