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.
Files changed (130) hide show
  1. bobframes/__init__.py +3 -0
  2. bobframes/_version.py +1 -0
  3. bobframes/catalog.py +154 -0
  4. bobframes/cli.py +266 -0
  5. bobframes/derive_post_merge.py +365 -0
  6. bobframes/derives/__init__.py +0 -0
  7. bobframes/derives/pass_class_breakdown.py +102 -0
  8. bobframes/derives/texture_usage.py +121 -0
  9. bobframes/discovery.py +132 -0
  10. bobframes/global_entities.py +99 -0
  11. bobframes/html/__init__.py +0 -0
  12. bobframes/html/template.py +1056 -0
  13. bobframes/lint.py +114 -0
  14. bobframes/manifest.py +127 -0
  15. bobframes/parquetize.py +282 -0
  16. bobframes/parsers/__init__.py +0 -0
  17. bobframes/parsers/derive_program_transitions.py +73 -0
  18. bobframes/parsers/parse_init_state.py +675 -0
  19. bobframes/paths.py +111 -0
  20. bobframes/probes/__init__.py +0 -0
  21. bobframes/probes/whatif.py +165 -0
  22. bobframes/qrd_harness.py +119 -0
  23. bobframes/query_examples.py +222 -0
  24. bobframes/rdcmd.py +72 -0
  25. bobframes/replay/__init__.py +26 -0
  26. bobframes/replay/replay_main.py +2305 -0
  27. bobframes/reports/__init__.py +0 -0
  28. bobframes/reports/_dashboard.py +425 -0
  29. bobframes/reports/ab.py +88 -0
  30. bobframes/reports/base.py +114 -0
  31. bobframes/reports/cache.py +147 -0
  32. bobframes/reports/chrome.py +1306 -0
  33. bobframes/reports/cli.py +99 -0
  34. bobframes/reports/delta.py +167 -0
  35. bobframes/reports/discovery.py +118 -0
  36. bobframes/reports/draws_by_class.py +165 -0
  37. bobframes/reports/formatters.py +122 -0
  38. bobframes/reports/instancing_opportunities.py +276 -0
  39. bobframes/reports/orchestrator.py +59 -0
  40. bobframes/reports/overdraw.py +293 -0
  41. bobframes/reports/pass_gpu.py +190 -0
  42. bobframes/reports/shader_hotlist.py +240 -0
  43. bobframes/reports/trend_table.py +444 -0
  44. bobframes/resource_labels.py +162 -0
  45. bobframes/run.py +480 -0
  46. bobframes/schemas.py +426 -0
  47. bobframes/stable_keys.py +83 -0
  48. bobframes/tests/__init__.py +0 -0
  49. bobframes/tests/_render_util.py +84 -0
  50. bobframes/tests/data/golden/_reports/draws_by_class.html +323 -0
  51. bobframes/tests/data/golden/_reports/drill/District 01/2026-05-28_r110600/index.html +1560 -0
  52. bobframes/tests/data/golden/_reports/index.html +264 -0
  53. bobframes/tests/data/golden/_reports/instancing_opportunities.html +266 -0
  54. bobframes/tests/data/golden/_reports/overdraw.html +275 -0
  55. bobframes/tests/data/golden/_reports/pass_gpu.html +277 -0
  56. bobframes/tests/data/golden/_reports/shader_hotlist.html +265 -0
  57. bobframes/tests/data/golden/_reports/trend_table.html +390 -0
  58. bobframes/tests/data/golden/index.html +1175 -0
  59. bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/_manifest.json +51 -0
  60. bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/buffers.parquet +0 -0
  61. bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/clears.parquet +0 -0
  62. bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/counters_per_event.parquet +0 -0
  63. bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/descriptor_access.parquet +0 -0
  64. bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/dispatches.parquet +0 -0
  65. bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/draw_bindings.parquet +0 -0
  66. bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/draws.parquet +0 -0
  67. bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/events.parquet +0 -0
  68. bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/fbos.parquet +0 -0
  69. bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/frame_totals.parquet +0 -0
  70. bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/ibo_samples.parquet +0 -0
  71. bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/indirect_args.parquet +0 -0
  72. bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/passes.parquet +0 -0
  73. bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/pixel_history.parquet +0 -0
  74. bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/post_vs_samples.parquet +0 -0
  75. bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/program_transitions.parquet +0 -0
  76. bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/programs.parquet +0 -0
  77. bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/render_targets.parquet +0 -0
  78. bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/resource_creation.parquet +0 -0
  79. bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/rt_event_timeline.parquet +0 -0
  80. bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/samplers.parquet +0 -0
  81. bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/shaders.parquet +0 -0
  82. bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/state_change_events.parquet +0 -0
  83. bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/texture_samples.parquet +0 -0
  84. bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/textures.parquet +0 -0
  85. bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/vbo_samples.parquet +0 -0
  86. bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/vertex_inputs.parquet +0 -0
  87. bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/_manifest.json +51 -0
  88. bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/buffers.parquet +0 -0
  89. bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/clears.parquet +0 -0
  90. bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/counters_per_event.parquet +0 -0
  91. bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/descriptor_access.parquet +0 -0
  92. bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/dispatches.parquet +0 -0
  93. bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/draw_bindings.parquet +0 -0
  94. bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/draws.parquet +0 -0
  95. bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/events.parquet +0 -0
  96. bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/fbos.parquet +0 -0
  97. bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/frame_totals.parquet +0 -0
  98. bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/ibo_samples.parquet +0 -0
  99. bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/indirect_args.parquet +0 -0
  100. bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/passes.parquet +0 -0
  101. bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/pixel_history.parquet +0 -0
  102. bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/post_vs_samples.parquet +0 -0
  103. bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/program_transitions.parquet +0 -0
  104. bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/programs.parquet +0 -0
  105. bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/render_targets.parquet +0 -0
  106. bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/resource_creation.parquet +0 -0
  107. bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/rt_event_timeline.parquet +0 -0
  108. bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/samplers.parquet +0 -0
  109. bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/shaders.parquet +0 -0
  110. bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/state_change_events.parquet +0 -0
  111. bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/texture_samples.parquet +0 -0
  112. bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/textures.parquet +0 -0
  113. bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/vbo_samples.parquet +0 -0
  114. bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/vertex_inputs.parquet +0 -0
  115. bobframes/tests/make_synthetic.py +171 -0
  116. bobframes/tests/smoke.py +199 -0
  117. bobframes/tests/test_determinism.py +19 -0
  118. bobframes/tests/test_discovery.py +97 -0
  119. bobframes/tests/test_hardening.py +142 -0
  120. bobframes/tests/test_parity.py +22 -0
  121. bobframes/tests/test_perf.py +18 -0
  122. bobframes/tests/test_replay_drift.py +115 -0
  123. bobframes/tests/test_schemas.py +26 -0
  124. bobframes/tests/test_schemas_unit.py +55 -0
  125. bobframes/tests/test_stable_keys.py +61 -0
  126. bobframes-0.1.0.dist-info/METADATA +144 -0
  127. bobframes-0.1.0.dist-info/RECORD +130 -0
  128. bobframes-0.1.0.dist-info/WHEEL +4 -0
  129. bobframes-0.1.0.dist-info/entry_points.txt +2 -0
  130. bobframes-0.1.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,365 @@
1
+ """Post-merge derivations: augment existing parquets with derived columns.
2
+
3
+ Computes columns that the replay didn't fill (or filled in an older schema
4
+ version), purely from data already in the drop's Parquet files. Safe to
5
+ re-run idempotently.
6
+
7
+ Added in SCHEMA_VERSION 2:
8
+ - draws.draw_class (from blend / depth_write / marker)
9
+ - draws.parent_pass_path_norm (strip 'Frame N/' prefix)
10
+ - passes.marker_path_norm (same)
11
+ - events.parent_marker_path_norm (same)
12
+
13
+ Run via: python -m bobframes.derive_post_merge <out_dir>
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ import os
19
+ import re
20
+ import sys
21
+
22
+ import pyarrow as pa
23
+ import pyarrow.csv as pacsv
24
+ import pyarrow.parquet as papq
25
+
26
+ _RE_FRAME_PREFIX = re.compile(r'^Frame\s+\d+/?')
27
+
28
+
29
+ def _strip_frame(path: str) -> str:
30
+ if not path:
31
+ return ''
32
+ return _RE_FRAME_PREFIX.sub('', path)
33
+
34
+
35
+ def _classify_draw(blend_enable: int, depth_write: int,
36
+ marker_path: str, blend_src_color: str, blend_dst_color: str) -> str:
37
+ mp = (marker_path or '').lower()
38
+ if 'shadow' in mp:
39
+ return 'shadow'
40
+ if 'prepass' in mp or 'depthonly' in mp:
41
+ return 'prepass'
42
+ if 'slate' in mp or '/ui' in mp or mp.endswith('ui'):
43
+ return 'ui'
44
+ if 'postprocess' in mp or 'tonemap' in mp or 'bloom' in mp or 'eyeadapt' in mp:
45
+ return 'postprocess'
46
+ if 'decal' in mp:
47
+ return 'decal'
48
+ if 'translucen' in mp:
49
+ return 'translucent'
50
+ if int(blend_enable or 0):
51
+ bs = (blend_src_color or '').lower()
52
+ bd = (blend_dst_color or '').lower()
53
+ if bs == 'one' and bd == 'one':
54
+ return 'additive'
55
+ return 'translucent'
56
+ # MobileBasePass / BasePass draws are scene opaques even when depth_write=0
57
+ # (prepass already wrote depth with EarlyZPass=2).
58
+ if 'basepass' in mp:
59
+ return 'opaque'
60
+ if int(depth_write or 0):
61
+ return 'opaque'
62
+ return 'other'
63
+
64
+
65
+ def _derive_draws(out_dir: str) -> bool:
66
+ pq_path = os.path.join(out_dir, 'draws.parquet')
67
+ csv_path = os.path.join(out_dir, 'draws.csv')
68
+ if not os.path.exists(pq_path):
69
+ return False
70
+ t = papq.read_table(pq_path)
71
+ cols = t.column_names
72
+
73
+ parent = t.column('parent_pass_path').to_pylist() if 'parent_pass_path' in cols else []
74
+ norm = [_strip_frame(p) for p in parent]
75
+
76
+ blend_en = t.column('blend_enable').to_pylist() if 'blend_enable' in cols else [0] * t.num_rows
77
+ depth_w = t.column('depth_write_enable').to_pylist() if 'depth_write_enable' in cols else [0] * t.num_rows
78
+ bsc = t.column('blend_src_color').to_pylist() if 'blend_src_color' in cols else [''] * t.num_rows
79
+ bdc = t.column('blend_dst_color').to_pylist() if 'blend_dst_color' in cols else [''] * t.num_rows
80
+ classes = [_classify_draw(be, dw, p, sc, dc) for be, dw, p, sc, dc
81
+ in zip(blend_en, depth_w, parent, bsc, bdc)]
82
+
83
+ # Build new table in schema order
84
+ from . import schemas
85
+ target_cols = list(schemas.DRAWS_COLS)
86
+ new_arrays: dict[str, pa.Array] = {}
87
+ for c in target_cols:
88
+ if c == 'parent_pass_path_norm':
89
+ new_arrays[c] = pa.array(norm, type=pa.string())
90
+ elif c == 'draw_class':
91
+ new_arrays[c] = pa.array(classes, type=pa.string())
92
+ elif c in cols:
93
+ new_arrays[c] = t.column(c)
94
+ else:
95
+ # missing source column; emit empty
96
+ dt = schemas.infer_dtype(c)
97
+ default = 0 if dt in ('int', 'bool') else (0.0 if dt == 'float' else '')
98
+ new_arrays[c] = pa.array([default] * t.num_rows)
99
+
100
+ out_t = pa.table(new_arrays)
101
+ papq.write_table(out_t, pq_path, compression='snappy')
102
+ pacsv.write_csv(out_t, csv_path)
103
+ return True
104
+
105
+
106
+ def _derive_path_norm(out_dir: str, table: str, src_col: str, dst_col: str) -> bool:
107
+ pq_path = os.path.join(out_dir, f'{table}.parquet')
108
+ csv_path = os.path.join(out_dir, f'{table}.csv')
109
+ if not os.path.exists(pq_path):
110
+ return False
111
+ t = papq.read_table(pq_path)
112
+ cols = t.column_names
113
+ if src_col not in cols:
114
+ return False
115
+
116
+ norm = [_strip_frame(p) for p in t.column(src_col).to_pylist()]
117
+
118
+ from . import schemas
119
+ schema_attr = {
120
+ 'passes': schemas.PASSES_COLS,
121
+ 'events': schemas.EVENTS_COLS,
122
+ }[table]
123
+ target_cols = list(schema_attr)
124
+ new_arrays: dict[str, pa.Array] = {}
125
+ for c in target_cols:
126
+ if c == dst_col:
127
+ new_arrays[c] = pa.array(norm, type=pa.string())
128
+ elif c in cols:
129
+ new_arrays[c] = t.column(c)
130
+ else:
131
+ dt = schemas.infer_dtype(c)
132
+ default = 0 if dt in ('int', 'bool') else (0.0 if dt == 'float' else '')
133
+ new_arrays[c] = pa.array([default] * t.num_rows)
134
+
135
+ out_t = pa.table(new_arrays)
136
+ papq.write_table(out_t, pq_path, compression='snappy')
137
+ pacsv.write_csv(out_t, csv_path)
138
+ return True
139
+
140
+
141
+ # --- Texture estimated bytes ------------------------------------------------
142
+
143
+ # Bytes per pixel by RD-style format Name(). Falls back to 4 on unknown.
144
+ _BYTES_PER_PIXEL = {
145
+ 'R8G8B8A8_UNORM': 4, 'R8G8B8A8_SRGB': 4, 'R8G8B8A8_SNORM': 4,
146
+ 'R8G8B8_UNORM': 3, 'R8G8B8_SRGB': 3,
147
+ 'B8G8R8A8_UNORM': 4, 'B8G8R8A8_SRGB': 4,
148
+ 'R8G8_UNORM': 2, 'R8G8_SNORM': 2,
149
+ 'R8_UNORM': 1, 'R8_SNORM': 1,
150
+ 'R16G16B16A16_FLOAT': 8, 'R16G16B16A16_UNORM': 8,
151
+ 'R16G16_FLOAT': 4, 'R16_FLOAT': 2,
152
+ 'R32G32B32A32_FLOAT': 16, 'R32_FLOAT': 4,
153
+ 'R11G11B10_FLOAT': 4, 'R10G10B10A2_UNORM': 4,
154
+ 'R5G6B5_UNORM': 2, 'R5G5B5A1_UNORM': 2,
155
+ 'D24_UNORM_S8_UINT': 4, 'D32_FLOAT_S8_UINT': 8,
156
+ 'D16_UNORM': 2, 'D32_FLOAT': 4, 'D24_UNORM': 3,
157
+ # Compressed: 0.5 bpp (4-bit) for BC1/ETC1; 1 bpp for BC3/ETC2_RGBA/ASTC_4x4
158
+ 'BC1_UNORM': 0.5, 'BC1_SRGB': 0.5,
159
+ 'BC3_UNORM': 1.0, 'BC3_SRGB': 1.0,
160
+ 'BC4_UNORM': 0.5, 'BC5_UNORM': 1.0,
161
+ 'BC7_UNORM': 1.0, 'BC7_SRGB': 1.0,
162
+ 'ETC1_RGB8': 0.5, 'ETC2_RGB8': 0.5, 'ETC2_RGB8_SRGB': 0.5,
163
+ 'ETC2_RGB8A1': 0.5, 'ETC2_RGBA8': 1.0, 'ETC2_RGBA8_SRGB': 1.0,
164
+ 'EAC_R11_UNORM': 0.5, 'EAC_RG11_UNORM': 1.0,
165
+ 'ASTC_4x4_UNORM': 1.0, 'ASTC_4x4_SRGB': 1.0,
166
+ 'ASTC_5x4_UNORM': 0.8, 'ASTC_5x5_UNORM': 0.64,
167
+ 'ASTC_6x5_UNORM': 0.533, 'ASTC_6x6_UNORM': 0.444,
168
+ 'ASTC_8x5_UNORM': 0.4, 'ASTC_8x6_UNORM': 0.333,
169
+ 'ASTC_8x8_UNORM': 0.25, 'ASTC_10x10_UNORM': 0.16,
170
+ 'ASTC_12x12_UNORM': 0.111,
171
+ }
172
+
173
+
174
+ def _est_bytes_for_texture(format_str: str, width: int, height: int,
175
+ depth: int, mip_levels: int, sample_count: int,
176
+ kind: str) -> int:
177
+ if width <= 0 or height <= 0:
178
+ return 0
179
+ bpp = _BYTES_PER_PIXEL.get(format_str, 4)
180
+ base = width * height * max(depth, 1) * bpp
181
+ if mip_levels and mip_levels > 1:
182
+ base *= 4.0 / 3.0
183
+ samples = max(sample_count, 1)
184
+ base *= samples
185
+ if kind and 'cube' in kind.lower():
186
+ base *= 6
187
+ return int(round(base))
188
+
189
+
190
+ def _derive_est_bytes(out_dir: str, table: str) -> bool:
191
+ pq_path = os.path.join(out_dir, f'{table}.parquet')
192
+ csv_path = os.path.join(out_dir, f'{table}.csv')
193
+ if not os.path.exists(pq_path):
194
+ return False
195
+ t = papq.read_table(pq_path)
196
+ cols = t.column_names
197
+ if 'est_bytes' not in cols or 'format' not in cols:
198
+ return False
199
+ fmts = t.column('format').to_pylist()
200
+ widths = t.column('width').to_pylist() if 'width' in cols else [0] * t.num_rows
201
+ heights = t.column('height').to_pylist() if 'height' in cols else [0] * t.num_rows
202
+ depths = t.column('depth').to_pylist() if 'depth' in cols else [0] * t.num_rows
203
+ mips = t.column('mip_levels').to_pylist() if 'mip_levels' in cols else [1] * t.num_rows
204
+ samps = t.column('sample_count').to_pylist() if 'sample_count' in cols else [1] * t.num_rows
205
+ kinds = t.column('kind').to_pylist() if 'kind' in cols else [''] * t.num_rows
206
+
207
+ new_eb = [_est_bytes_for_texture(fmts[i] or '', int(widths[i] or 0),
208
+ int(heights[i] or 0), int(depths[i] or 0),
209
+ int(mips[i] or 1), int(samps[i] or 1),
210
+ kinds[i] or '')
211
+ for i in range(t.num_rows)]
212
+
213
+ from . import schemas
214
+ target_cols = list(schemas.TEXTURES_COLS if table == 'textures' else schemas.RENDER_TARGETS_COLS)
215
+ new_arrays: dict[str, pa.Array] = {}
216
+ for c in target_cols:
217
+ if c == 'est_bytes':
218
+ new_arrays[c] = pa.array(new_eb, type=pa.int64())
219
+ elif c in cols:
220
+ new_arrays[c] = t.column(c)
221
+ else:
222
+ dt = schemas.infer_dtype(c)
223
+ default = 0 if dt in ('int', 'bool') else (0.0 if dt == 'float' else '')
224
+ new_arrays[c] = pa.array([default] * t.num_rows)
225
+
226
+ out_t = pa.table(new_arrays)
227
+ papq.write_table(out_t, pq_path, compression='snappy')
228
+ pacsv.write_csv(out_t, csv_path)
229
+ return True
230
+
231
+
232
+ # --- Shader complexity_score -------------------------------------------------
233
+
234
+ def _derive_complexity_score(out_dir: str) -> bool:
235
+ pq_path = os.path.join(out_dir, 'shaders.parquet')
236
+ csv_path = os.path.join(out_dir, 'shaders.csv')
237
+ if not os.path.exists(pq_path):
238
+ return False
239
+ t = papq.read_table(pq_path)
240
+ cols = t.column_names
241
+ def _col(name):
242
+ return t.column(name).to_pylist() if name in cols else [0] * t.num_rows
243
+ ts = _col('total_texture_samples')
244
+ br = _col('total_branches')
245
+ lo = _col('total_loops')
246
+ di = _col('total_discards')
247
+ df = _col('total_dfdx_dfdy')
248
+ m4 = _col('total_mat4_constructors')
249
+ sl = _col('src_len')
250
+ scores = [
251
+ float(ts[i] or 0) * 2.0
252
+ + float(br[i] or 0) * 0.5
253
+ + float(lo[i] or 0) * 2.0
254
+ + float(di[i] or 0) * 0.3
255
+ + float(df[i] or 0) * 0.5
256
+ + float(m4[i] or 0) * 0.3
257
+ + min(float(sl[i] or 0) / 100.0, 50.0)
258
+ for i in range(t.num_rows)
259
+ ]
260
+
261
+ from . import schemas
262
+ target_cols = list(schemas.SHADERS_COLS)
263
+ new_arrays: dict[str, pa.Array] = {}
264
+ for c in target_cols:
265
+ if c == 'complexity_score':
266
+ new_arrays[c] = pa.array(scores, type=pa.float64())
267
+ elif c in cols:
268
+ new_arrays[c] = t.column(c)
269
+ else:
270
+ dt = schemas.infer_dtype(c)
271
+ default = 0 if dt in ('int', 'bool') else (0.0 if dt == 'float' else '')
272
+ new_arrays[c] = pa.array([default] * t.num_rows)
273
+
274
+ out_t = pa.table(new_arrays)
275
+ papq.write_table(out_t, pq_path, compression='snappy')
276
+ pacsv.write_csv(out_t, csv_path)
277
+ return True
278
+
279
+
280
+ def _derive_frame_totals_bytes(out_dir: str) -> bool:
281
+ """After textures.est_bytes is filled, update frame_totals byte aggregates."""
282
+ ft_path = os.path.join(out_dir, 'frame_totals.parquet')
283
+ ft_csv = os.path.join(out_dir, 'frame_totals.csv')
284
+ tx_path = os.path.join(out_dir, 'textures.parquet')
285
+ bf_path = os.path.join(out_dir, 'buffers.parquet')
286
+ if not os.path.exists(ft_path):
287
+ return False
288
+
289
+ ft = papq.read_table(ft_path)
290
+ cols = ft.column_names
291
+
292
+ # Per-capture aggregates
293
+ tex_by_cap: dict[str, int] = {}
294
+ if os.path.exists(tx_path):
295
+ tx = papq.read_table(tx_path, columns=['capture', 'est_bytes'])
296
+ cap = tx.column('capture').to_pylist()
297
+ eb = tx.column('est_bytes').to_pylist()
298
+ for c, b in zip(cap, eb):
299
+ tex_by_cap[c] = tex_by_cap.get(c, 0) + int(b or 0)
300
+
301
+ vbo_by_cap: dict[str, int] = {}
302
+ ibo_by_cap: dict[str, int] = {}
303
+ ubo_by_cap: dict[str, int] = {}
304
+ if os.path.exists(bf_path):
305
+ bf = papq.read_table(bf_path, columns=['capture', 'allocated_size_bytes',
306
+ 'used_as_vbo', 'used_as_ibo', 'used_as_ubo'])
307
+ cap = bf.column('capture').to_pylist()
308
+ sz = bf.column('allocated_size_bytes').to_pylist()
309
+ v = bf.column('used_as_vbo').to_pylist()
310
+ i = bf.column('used_as_ibo').to_pylist()
311
+ u = bf.column('used_as_ubo').to_pylist()
312
+ for ci, s, vi, ii, ui in zip(cap, sz, v, i, u):
313
+ if vi: vbo_by_cap[ci] = vbo_by_cap.get(ci, 0) + int(s or 0)
314
+ if ii: ibo_by_cap[ci] = ibo_by_cap.get(ci, 0) + int(s or 0)
315
+ if ui: ubo_by_cap[ci] = ubo_by_cap.get(ci, 0) + int(s or 0)
316
+
317
+ cap_col = ft.column('capture').to_pylist()
318
+ new_tex = [tex_by_cap.get(c, 0) for c in cap_col]
319
+ new_vbo = [vbo_by_cap.get(c, 0) for c in cap_col]
320
+ new_ibo = [ibo_by_cap.get(c, 0) for c in cap_col]
321
+ new_ubo = [ubo_by_cap.get(c, 0) for c in cap_col]
322
+
323
+ from . import schemas
324
+ target = list(schemas.FRAME_TOTALS_COLS)
325
+ new_arrays: dict[str, pa.Array] = {}
326
+ for c in target:
327
+ if c == 'total_texture_bytes_allocated':
328
+ new_arrays[c] = pa.array(new_tex, type=pa.int64())
329
+ elif c == 'total_vbo_bytes_uploaded':
330
+ new_arrays[c] = pa.array(new_vbo, type=pa.int64())
331
+ elif c == 'total_ibo_bytes_uploaded':
332
+ new_arrays[c] = pa.array(new_ibo, type=pa.int64())
333
+ elif c == 'total_ubo_bytes_uploaded':
334
+ new_arrays[c] = pa.array(new_ubo, type=pa.int64())
335
+ elif c in cols:
336
+ new_arrays[c] = ft.column(c)
337
+ else:
338
+ dt = schemas.infer_dtype(c)
339
+ default = 0 if dt in ('int', 'bool') else (0.0 if dt == 'float' else '')
340
+ new_arrays[c] = pa.array([default] * ft.num_rows)
341
+
342
+ out_t = pa.table(new_arrays)
343
+ papq.write_table(out_t, ft_path, compression='snappy')
344
+ pacsv.write_csv(out_t, ft_csv)
345
+ return True
346
+
347
+
348
+ def derive(out_dir: str) -> dict[str, bool]:
349
+ """Run all post-merge derivations on an _analysis_out directory."""
350
+ results = {}
351
+ results['draws'] = _derive_draws(out_dir)
352
+ results['passes'] = _derive_path_norm(out_dir, 'passes', 'marker_path', 'marker_path_norm')
353
+ results['events'] = _derive_path_norm(out_dir, 'events', 'parent_marker_path', 'parent_marker_path_norm')
354
+ results['textures_est_bytes'] = _derive_est_bytes(out_dir, 'textures')
355
+ results['render_targets_est_bytes'] = _derive_est_bytes(out_dir, 'render_targets')
356
+ results['shaders_complexity'] = _derive_complexity_score(out_dir)
357
+ results['frame_totals_bytes'] = _derive_frame_totals_bytes(out_dir)
358
+ return results
359
+
360
+
361
+ if __name__ == '__main__':
362
+ if len(sys.argv) != 2:
363
+ print('usage: derive_post_merge.py <out_dir>', file=sys.stderr)
364
+ sys.exit(2)
365
+ print(derive(sys.argv[1]))
File without changes
@@ -0,0 +1,102 @@
1
+ """Per-pass × draw_class aggregation.
2
+
3
+ Reads draws.parquet + counters_per_event.parquet, joins on event_id for
4
+ gpu_duration_s, groups by (area, drop_date, drop_label, capture,
5
+ parent_pass_path_norm, draw_class). Emits one row per group.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import os
11
+ from collections import defaultdict
12
+
13
+ import pyarrow as pa
14
+ import pyarrow.csv as pacsv
15
+ import pyarrow.parquet as papq
16
+
17
+ from .. import schemas
18
+
19
+
20
+ def build(out_dir: str) -> int:
21
+ draws_path = os.path.join(out_dir, 'draws.parquet')
22
+ counters_path = os.path.join(out_dir, 'counters_per_event.parquet')
23
+ if not os.path.exists(draws_path):
24
+ return 0
25
+
26
+ draws = papq.read_table(draws_path, columns=[
27
+ 'area', 'drop_date', 'drop_label', 'capture',
28
+ 'event_id', 'parent_pass_path_norm', 'draw_class',
29
+ 'num_indices', 'num_instances',
30
+ ])
31
+
32
+ durations: dict[tuple, float] = {}
33
+ if os.path.exists(counters_path):
34
+ ct = papq.read_table(counters_path, columns=[
35
+ 'area', 'drop_date', 'drop_label', 'capture',
36
+ 'event_id', 'counter_name', 'value_double',
37
+ ])
38
+ ar = ct.column('area').to_pylist()
39
+ dd = ct.column('drop_date').to_pylist()
40
+ dl = ct.column('drop_label').to_pylist()
41
+ cp = ct.column('capture').to_pylist()
42
+ ev = ct.column('event_id').to_pylist()
43
+ cn = ct.column('counter_name').to_pylist()
44
+ vd = ct.column('value_double').to_pylist()
45
+ for i in range(ct.num_rows):
46
+ if cn[i] == 'GPU Duration':
47
+ durations[(ar[i], dd[i], dl[i], cp[i], ev[i])] = float(vd[i] or 0.0)
48
+
49
+ agg: dict[tuple, dict] = defaultdict(lambda: {
50
+ 'n_draws': 0, 'n_dispatches': 0,
51
+ 'sum_pre_vs_vertices': 0, 'sum_gpu_duration_s': 0.0,
52
+ })
53
+ d_ar = draws.column('area').to_pylist()
54
+ d_dd = draws.column('drop_date').to_pylist()
55
+ d_dl = draws.column('drop_label').to_pylist()
56
+ d_cp = draws.column('capture').to_pylist()
57
+ d_ev = draws.column('event_id').to_pylist()
58
+ d_pp = draws.column('parent_pass_path_norm').to_pylist()
59
+ d_cl = draws.column('draw_class').to_pylist()
60
+ d_ni = draws.column('num_indices').to_pylist()
61
+ d_ic = draws.column('num_instances').to_pylist()
62
+
63
+ for i in range(draws.num_rows):
64
+ key = (d_ar[i], d_dd[i], d_dl[i], d_cp[i], d_pp[i] or '', d_cl[i] or 'other')
65
+ a = agg[key]
66
+ a['n_draws'] += 1
67
+ a['sum_pre_vs_vertices'] += int(d_ni[i] or 0) * max(int(d_ic[i] or 1), 1)
68
+ dur = durations.get((d_ar[i], d_dd[i], d_dl[i], d_cp[i], d_ev[i]), 0.0)
69
+ a['sum_gpu_duration_s'] += dur
70
+
71
+ cols_target = list(schemas.PASS_CLASS_BREAKDOWN_COLS)
72
+ out_rows = []
73
+ for key, vals in sorted(agg.items()):
74
+ out_rows.append({
75
+ 'area': key[0], 'drop_date': key[1], 'drop_label': key[2],
76
+ 'capture': key[3], 'marker_path_norm': key[4], 'draw_class': key[5],
77
+ 'n_draws': vals['n_draws'], 'n_dispatches': vals['n_dispatches'],
78
+ 'sum_pre_vs_vertices': vals['sum_pre_vs_vertices'],
79
+ 'sum_gpu_duration_s': vals['sum_gpu_duration_s'],
80
+ })
81
+
82
+ arrays: dict[str, pa.Array] = {}
83
+ for c in cols_target:
84
+ dt = schemas.infer_dtype(c)
85
+ vs = [r.get(c) for r in out_rows]
86
+ if dt == 'int':
87
+ arrays[c] = pa.array([int(v or 0) for v in vs], type=pa.int64())
88
+ elif dt == 'float':
89
+ arrays[c] = pa.array([float(v or 0.0) for v in vs], type=pa.float64())
90
+ else:
91
+ arrays[c] = pa.array([str(v or '') for v in vs], type=pa.string())
92
+ table = pa.table(arrays)
93
+ papq.write_table(table, os.path.join(out_dir, 'pass_class_breakdown.parquet'),
94
+ compression='snappy')
95
+ pacsv.write_csv(table, os.path.join(out_dir, 'pass_class_breakdown.csv'))
96
+ return table.num_rows
97
+
98
+
99
+ if __name__ == '__main__':
100
+ import sys
101
+ p = sys.argv[1] if len(sys.argv) > 1 else '.'
102
+ print(f'wrote {build(p)} rows')
@@ -0,0 +1,121 @@
1
+ """Per-texture usage heat.
2
+
3
+ Reads descriptor_access.parquet filtered to ReadOnlyResource (textures),
4
+ groups by (area, drop_date, drop_label, capture, resource_id), counts
5
+ unique events sampled + total accesses + first/last event. Joins into
6
+ textures.parquet to attach stable_key + label + format.
7
+
8
+ Lets reports answer: which texture is sampled the most? Which is bound but
9
+ never sampled? Memory-vs-usage ratio per texture?
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import os
15
+ from collections import defaultdict
16
+
17
+ import pyarrow as pa
18
+ import pyarrow.csv as pacsv
19
+ import pyarrow.parquet as papq
20
+
21
+ from .. import schemas
22
+
23
+
24
+ def build(out_dir: str) -> int:
25
+ da_path = os.path.join(out_dir, 'descriptor_access.parquet')
26
+ tx_path = os.path.join(out_dir, 'textures.parquet')
27
+ if not os.path.exists(da_path):
28
+ return 0
29
+
30
+ da = papq.read_table(da_path, columns=[
31
+ 'area', 'drop_date', 'drop_label', 'capture',
32
+ 'event_id', 'descriptor_kind', 'resource_id',
33
+ ])
34
+ ar = da.column('area').to_pylist()
35
+ dd = da.column('drop_date').to_pylist()
36
+ dl = da.column('drop_label').to_pylist()
37
+ cp = da.column('capture').to_pylist()
38
+ ev = da.column('event_id').to_pylist()
39
+ kn = da.column('descriptor_kind').to_pylist()
40
+ rid = da.column('resource_id').to_pylist()
41
+
42
+ agg: dict[tuple, dict] = defaultdict(lambda: {
43
+ 'events': set(), 'count': 0,
44
+ 'first_event_id': -1, 'last_event_id': -1,
45
+ })
46
+ # Arm RD's GLES backend uses 'ImageSampler' (sampled tex+sampler combo)
47
+ # plus 'ReadOnlyResource' on other backends. Accept both.
48
+ TEX_KINDS = {'ImageSampler', 'ReadOnlyResource'}
49
+ for i in range(da.num_rows):
50
+ if kn[i] not in TEX_KINDS:
51
+ continue
52
+ r = rid[i]
53
+ if not r:
54
+ continue
55
+ key = (ar[i], dd[i], dl[i], cp[i], int(r))
56
+ a = agg[key]
57
+ e = int(ev[i] or 0)
58
+ a['events'].add(e)
59
+ a['count'] += 1
60
+ if a['first_event_id'] < 0 or e < a['first_event_id']:
61
+ a['first_event_id'] = e
62
+ if e > a['last_event_id']:
63
+ a['last_event_id'] = e
64
+
65
+ # Build textures lookup: (area, drop_date, drop_label, capture, tex_id) -> {stable_key, label, format}
66
+ tex_info: dict[tuple, dict] = {}
67
+ if os.path.exists(tx_path):
68
+ tx = papq.read_table(tx_path, columns=[
69
+ 'area', 'drop_date', 'drop_label', 'capture',
70
+ 'tex_id', 'stable_key', 'label', 'format',
71
+ ])
72
+ for i in range(tx.num_rows):
73
+ tex_info[(
74
+ tx.column('area')[i].as_py(),
75
+ tx.column('drop_date')[i].as_py(),
76
+ tx.column('drop_label')[i].as_py(),
77
+ tx.column('capture')[i].as_py(),
78
+ int(tx.column('tex_id')[i].as_py() or 0),
79
+ )] = {
80
+ 'stable_key': tx.column('stable_key')[i].as_py() or '',
81
+ 'label': tx.column('label')[i].as_py() or '',
82
+ 'format': tx.column('format')[i].as_py() or '',
83
+ }
84
+
85
+ out_rows = []
86
+ for key, vals in sorted(agg.items(), key=lambda kv: (kv[0][0], kv[0][1], kv[0][2], kv[0][3], -len(kv[1]['events']))):
87
+ info = tex_info.get(key, {'stable_key': '', 'label': '', 'format': ''})
88
+ out_rows.append({
89
+ 'area': key[0], 'drop_date': key[1], 'drop_label': key[2],
90
+ 'capture': key[3], 'tex_id': key[4],
91
+ 'stable_key': info['stable_key'],
92
+ 'label': info['label'],
93
+ 'format': info['format'],
94
+ 'n_unique_events_sampled': len(vals['events']),
95
+ 'n_descriptor_accesses': vals['count'],
96
+ 'first_event_id': vals['first_event_id'],
97
+ 'last_event_id': vals['last_event_id'],
98
+ })
99
+
100
+ cols_target = list(schemas.TEXTURE_USAGE_COLS)
101
+ arrays: dict[str, pa.Array] = {}
102
+ for c in cols_target:
103
+ dt = schemas.infer_dtype(c)
104
+ vs = [r.get(c) for r in out_rows]
105
+ if dt == 'int':
106
+ arrays[c] = pa.array([int(v or 0) for v in vs], type=pa.int64())
107
+ elif dt == 'float':
108
+ arrays[c] = pa.array([float(v or 0.0) for v in vs], type=pa.float64())
109
+ else:
110
+ arrays[c] = pa.array([str(v or '') for v in vs], type=pa.string())
111
+ table = pa.table(arrays)
112
+ papq.write_table(table, os.path.join(out_dir, 'texture_usage.parquet'),
113
+ compression='snappy')
114
+ pacsv.write_csv(table, os.path.join(out_dir, 'texture_usage.csv'))
115
+ return table.num_rows
116
+
117
+
118
+ if __name__ == '__main__':
119
+ import sys
120
+ p = sys.argv[1] if len(sys.argv) > 1 else '.'
121
+ print(f'wrote {build(p)} rows')