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
bobframes/discovery.py ADDED
@@ -0,0 +1,132 @@
1
+ """Area + dated-drop walking.
2
+
3
+ Layout:
4
+ <root>/
5
+ <Area>/
6
+ <YYYY-MM-DD>[_<label>]/
7
+ <capture>.rdc, .xml, .zip.xml, .zip
8
+
9
+ Finds the newest dated drop per area; filters by --area / --label / --capture.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import os
15
+ import re
16
+ from dataclasses import dataclass
17
+
18
+ DATED_RE = re.compile(r'^(\d{4}-\d{2}-\d{2})(?:_(.*))?$')
19
+
20
+
21
+ @dataclass(frozen=True)
22
+ class Drop:
23
+ area: str
24
+ drop_date: str # 'YYYY-MM-DD'
25
+ drop_label: str # text after the date, or '' if absent
26
+ drop_dir: str # absolute path to the dated folder
27
+ captures: tuple[str, ...] # rdc filenames without .rdc extension, sorted
28
+
29
+
30
+ def _list_areas(root: str) -> list[tuple[str, str]]:
31
+ out = []
32
+ for entry in sorted(os.listdir(root)):
33
+ if entry.startswith('_') or entry.startswith('.'):
34
+ continue
35
+ full = os.path.join(root, entry)
36
+ if os.path.isdir(full):
37
+ out.append((entry, full))
38
+ return out
39
+
40
+
41
+ def _latest_drop_dir(area_dir: str) -> tuple[str, str, str] | None:
42
+ """Return (drop_date, drop_label, drop_dir) for newest dated sub-dir, else None."""
43
+ dated = []
44
+ for entry in os.listdir(area_dir):
45
+ m = DATED_RE.match(entry)
46
+ if not m:
47
+ continue
48
+ full = os.path.join(area_dir, entry)
49
+ if not os.path.isdir(full):
50
+ continue
51
+ dated.append((m.group(1), m.group(2) or '', full))
52
+ if not dated:
53
+ return None
54
+ dated.sort(reverse=True)
55
+ return dated[0]
56
+
57
+
58
+ def _captures(drop_dir: str) -> tuple[str, ...]:
59
+ names = []
60
+ for entry in sorted(os.listdir(drop_dir)):
61
+ if entry.endswith('.rdc') and os.path.isfile(os.path.join(drop_dir, entry)):
62
+ names.append(entry[:-4])
63
+ names.sort(key=lambda s: (len(s), s))
64
+ return tuple(names)
65
+
66
+
67
+ def find_drops(
68
+ root: str,
69
+ area_filter: str | None = None,
70
+ label_filter: str | None = None,
71
+ capture_filter: str | None = None,
72
+ ) -> list[Drop]:
73
+ """Return drops to process. One per area (newest dated drop).
74
+
75
+ area_filter: exact area folder name to restrict to.
76
+ label_filter: only return drops whose drop_label matches.
77
+ capture_filter: restricts each drop's captures tuple to just this name.
78
+ """
79
+ root = os.path.abspath(root)
80
+ if not os.path.isdir(root):
81
+ raise FileNotFoundError(f'root not found: {root}')
82
+
83
+ drops: list[Drop] = []
84
+ for area, area_dir in _list_areas(root):
85
+ if area_filter and area != area_filter:
86
+ continue
87
+ latest = _latest_drop_dir(area_dir)
88
+ if latest is None:
89
+ continue
90
+ drop_date, drop_label, drop_dir = latest
91
+ if label_filter and drop_label != label_filter:
92
+ continue
93
+ captures = _captures(drop_dir)
94
+ if not captures:
95
+ continue
96
+ if capture_filter:
97
+ if capture_filter not in captures:
98
+ continue
99
+ captures = (capture_filter,)
100
+ drops.append(Drop(
101
+ area=area,
102
+ drop_date=drop_date,
103
+ drop_label=drop_label,
104
+ drop_dir=drop_dir,
105
+ captures=captures,
106
+ ))
107
+ return drops
108
+
109
+
110
+ def parse_single_drop_arg(arg: str, root: str) -> Drop:
111
+ """Parse a positional argument like 'Chor bazar/2026-05-27_r110565/' into a Drop.
112
+
113
+ Used when the user passes a specific drop directory rather than --area.
114
+ """
115
+ arg = arg.rstrip('/\\')
116
+ parts = arg.replace('\\', '/').split('/')
117
+ if len(parts) < 2:
118
+ raise ValueError(f'expected <area>/<dated_drop>, got {arg!r}')
119
+ area, dated = parts[-2], parts[-1]
120
+ m = DATED_RE.match(dated)
121
+ if not m:
122
+ raise ValueError(f'not a dated drop folder: {dated!r}')
123
+ drop_dir = os.path.join(root, area, dated)
124
+ if not os.path.isdir(drop_dir):
125
+ raise FileNotFoundError(f'drop dir does not exist: {drop_dir}')
126
+ return Drop(
127
+ area=area,
128
+ drop_date=m.group(1),
129
+ drop_label=m.group(2) or '',
130
+ drop_dir=drop_dir,
131
+ captures=_captures(drop_dir),
132
+ )
@@ -0,0 +1,99 @@
1
+ """Emit <root>/_global_entities.parquet.
2
+
3
+ One row per (stable_key, kind, area, drop_date, drop_label, capture, local_id).
4
+ This is the canonical index for cross-drop entity joins. Layer 2 reports
5
+ join into draws/passes/events tables using stable_key directly:
6
+
7
+ SELECT *
8
+ FROM read_parquet('**/draws.parquet') d
9
+ JOIN read_parquet('_global_entities.parquet') g
10
+ ON g.area = d.area
11
+ AND g.drop_date = d.drop_date
12
+ AND g.drop_label = d.drop_label
13
+ AND g.capture = d.capture
14
+ AND g.local_id = d.fs_shader_id
15
+ AND g.kind = 'shader'
16
+ WHERE g.stable_key = 'abc123...';
17
+
18
+ The (kind, local_id) pair lets reports resolve any per-capture resource ID
19
+ back to its drop-spanning identity.
20
+ """
21
+
22
+ from __future__ import annotations
23
+
24
+ import glob
25
+ import os
26
+
27
+ import pyarrow as pa
28
+ import pyarrow.csv as pacsv
29
+ import pyarrow.parquet as papq
30
+
31
+
32
+ _ENTITY_TABLES = [
33
+ ('shaders', 'shader_id', 'shader'),
34
+ ('textures', 'tex_id', 'texture'),
35
+ ('render_targets', 'rt_id', 'texture'), # RTs are textures
36
+ ('programs', 'program_id', 'program'),
37
+ ('samplers', 'sampler_id', 'sampler'),
38
+ ('fbos', 'fbo_id', 'fbo'),
39
+ ('buffers', 'buffer_id', 'buffer'),
40
+ ]
41
+
42
+
43
+ _OUT_COLS = ('stable_key', 'kind', 'area', 'drop_date', 'drop_label',
44
+ 'capture', 'local_id')
45
+
46
+
47
+ def build_global_entities(root: str) -> int:
48
+ """Walk every entity parquet under <root>; emit _global_entities.parquet.
49
+
50
+ Returns the row count written.
51
+ """
52
+ cols: dict[str, list] = {c: [] for c in _OUT_COLS}
53
+
54
+ from . import paths as _paths
55
+ data_root = _paths.data_root(root)
56
+ for table, id_col, kind in _ENTITY_TABLES:
57
+ for path in sorted(glob.glob(os.path.join(data_root, '*', '*',
58
+ f'{table}.parquet'))):
59
+ try:
60
+ t = papq.read_table(path, columns=['stable_key', 'area', 'drop_date',
61
+ 'drop_label', 'capture', id_col])
62
+ except Exception:
63
+ continue
64
+ sk = t.column('stable_key').to_pylist()
65
+ ar = t.column('area').to_pylist()
66
+ dd = t.column('drop_date').to_pylist()
67
+ dl = t.column('drop_label').to_pylist()
68
+ cp = t.column('capture').to_pylist()
69
+ lid = t.column(id_col).to_pylist()
70
+ for i in range(len(sk)):
71
+ if not sk[i]:
72
+ continue # skip entities without a stable_key (best-effort only)
73
+ cols['stable_key'].append(sk[i])
74
+ cols['kind'].append(kind)
75
+ cols['area'].append(ar[i])
76
+ cols['drop_date'].append(dd[i])
77
+ cols['drop_label'].append(dl[i])
78
+ cols['capture'].append(cp[i])
79
+ cols['local_id'].append(int(lid[i]) if lid[i] is not None else 0)
80
+
81
+ arrays: dict[str, pa.Array] = {}
82
+ for c in _OUT_COLS:
83
+ if c == 'local_id':
84
+ arrays[c] = pa.array(cols[c], type=pa.int64())
85
+ else:
86
+ arrays[c] = pa.array(cols[c], type=pa.string())
87
+ table = pa.table(arrays)
88
+
89
+ os.makedirs(_paths.data_root(root), exist_ok=True)
90
+ papq.write_table(table, _paths.global_entities_parquet(root), compression='snappy')
91
+ pacsv.write_csv(table, _paths.global_entities_csv(root))
92
+ return table.num_rows
93
+
94
+
95
+ if __name__ == '__main__':
96
+ import sys
97
+ root = sys.argv[1] if len(sys.argv) > 1 else '.'
98
+ n = build_global_entities(root)
99
+ print(f'wrote _global_entities.parquet: {n} rows')
File without changes