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
bobframes/__init__.py
ADDED
bobframes/_version.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.1.0"
|
bobframes/catalog.py
ADDED
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
"""Build <root>/_data/_catalog.parquet and _catalog.json.
|
|
2
|
+
|
|
3
|
+
One row per (area, drop_date, drop_label, capture). Per-capture row counts
|
|
4
|
+
are computed by reading each drop's parquets and grouping by the `capture`
|
|
5
|
+
column — that way the catalog reflects what actually landed for each
|
|
6
|
+
capture, not just drop-level totals.
|
|
7
|
+
|
|
8
|
+
Also tracks schema version, build timestamp, replay status, and the relative
|
|
9
|
+
path to the drop's data dir (`_data/<area>/<drop>`). Path is RELATIVE for
|
|
10
|
+
portability across machines.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import datetime as _dt
|
|
16
|
+
import json
|
|
17
|
+
import os
|
|
18
|
+
from collections import defaultdict
|
|
19
|
+
|
|
20
|
+
import pyarrow as pa
|
|
21
|
+
import pyarrow.csv as pacsv
|
|
22
|
+
import pyarrow.parquet as papq
|
|
23
|
+
|
|
24
|
+
from . import paths as _paths
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
_CATALOG_TABLE_KEYS = [
|
|
28
|
+
'draws', 'events', 'shaders', 'textures',
|
|
29
|
+
'render_targets', 'buffers', 'programs',
|
|
30
|
+
'samplers', 'fbos', 'state_change_events',
|
|
31
|
+
'counters_per_event', 'descriptor_access',
|
|
32
|
+
'passes', 'frame_totals',
|
|
33
|
+
'clears', 'dispatches', 'rt_event_timeline',
|
|
34
|
+
'vertex_inputs', 'resource_creation',
|
|
35
|
+
'draw_bindings', 'program_transitions',
|
|
36
|
+
'pixel_history', 'vbo_samples', 'ibo_samples',
|
|
37
|
+
'post_vs_samples', 'texture_samples', 'indirect_args',
|
|
38
|
+
'pass_class_breakdown', 'texture_usage',
|
|
39
|
+
]
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _find_manifests(root: str) -> list[tuple[str, str, dict]]:
|
|
43
|
+
"""Walk _data/<area>/<drop>/_manifest.json. Returns [(data_dir, rel_path, manifest)]."""
|
|
44
|
+
out = []
|
|
45
|
+
data_root = _paths.data_root(root)
|
|
46
|
+
if not os.path.isdir(data_root):
|
|
47
|
+
return out
|
|
48
|
+
for area_entry in sorted(os.listdir(data_root)):
|
|
49
|
+
if area_entry.startswith(('_', '.')):
|
|
50
|
+
continue
|
|
51
|
+
area_dir = os.path.join(data_root, area_entry)
|
|
52
|
+
if not os.path.isdir(area_dir):
|
|
53
|
+
continue
|
|
54
|
+
for drop_entry in sorted(os.listdir(area_dir)):
|
|
55
|
+
drop_dir = os.path.join(area_dir, drop_entry)
|
|
56
|
+
if not os.path.isdir(drop_dir):
|
|
57
|
+
continue
|
|
58
|
+
mf = os.path.join(drop_dir, '_manifest.json')
|
|
59
|
+
if not os.path.exists(mf):
|
|
60
|
+
continue
|
|
61
|
+
try:
|
|
62
|
+
with open(mf, 'r', encoding='utf-8') as f:
|
|
63
|
+
m = json.load(f)
|
|
64
|
+
except Exception:
|
|
65
|
+
continue
|
|
66
|
+
rel = _paths.drop_dir_rel(area_entry, drop_entry)
|
|
67
|
+
out.append((drop_dir, rel, m))
|
|
68
|
+
return out
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _per_capture_row_counts(data_dir: str, captures: list[str]) -> dict[str, dict[str, int]]:
|
|
72
|
+
"""Walk all parquets in data_dir; return {capture: {table: row_count}}."""
|
|
73
|
+
result: dict[str, dict[str, int]] = {c: defaultdict(int) for c in captures}
|
|
74
|
+
for table in _CATALOG_TABLE_KEYS:
|
|
75
|
+
pq = os.path.join(data_dir, f'{table}.parquet')
|
|
76
|
+
if not os.path.exists(pq):
|
|
77
|
+
continue
|
|
78
|
+
try:
|
|
79
|
+
t = papq.read_table(pq, columns=['capture'])
|
|
80
|
+
caps = t.column('capture').to_pylist()
|
|
81
|
+
except Exception:
|
|
82
|
+
continue
|
|
83
|
+
for c in caps:
|
|
84
|
+
if c in result:
|
|
85
|
+
result[c][table] += 1
|
|
86
|
+
return {c: dict(d) for c, d in result.items()}
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _capture_rows(data_dir: str, rel_path: str, manifest: dict) -> list[dict]:
|
|
90
|
+
captures = manifest.get('captures') or manifest.get('stems') or []
|
|
91
|
+
cap_status = manifest.get('capture_status') or manifest.get('stem_status') or {}
|
|
92
|
+
|
|
93
|
+
per_cap = _per_capture_row_counts(data_dir, captures) if captures else {}
|
|
94
|
+
|
|
95
|
+
rows: list[dict] = []
|
|
96
|
+
for cap in captures:
|
|
97
|
+
counts = per_cap.get(cap, {})
|
|
98
|
+
rows.append({
|
|
99
|
+
'area': manifest['area'],
|
|
100
|
+
'drop_date': manifest['drop_date'],
|
|
101
|
+
'drop_label': manifest.get('drop_label', '') or '',
|
|
102
|
+
'capture': cap,
|
|
103
|
+
'schema_version': int(manifest.get('schema_version', 0)),
|
|
104
|
+
'build_timestamp': manifest.get('build_timestamp', ''),
|
|
105
|
+
'replay_status': cap_status.get(cap, 'unknown'),
|
|
106
|
+
**{f'row_count_{k}': int(counts.get(k, 0)) for k in _CATALOG_TABLE_KEYS},
|
|
107
|
+
'analysis_out_path': rel_path,
|
|
108
|
+
})
|
|
109
|
+
return rows
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def build_catalog(root: str) -> dict:
|
|
113
|
+
manifests = _find_manifests(root)
|
|
114
|
+
all_rows: list[dict] = []
|
|
115
|
+
for data_dir, rel_path, m in manifests:
|
|
116
|
+
all_rows.extend(_capture_rows(data_dir, rel_path, m))
|
|
117
|
+
|
|
118
|
+
cols = [
|
|
119
|
+
'area', 'drop_date', 'drop_label', 'capture',
|
|
120
|
+
'schema_version', 'build_timestamp', 'replay_status',
|
|
121
|
+
] + [f'row_count_{k}' for k in _CATALOG_TABLE_KEYS] + ['analysis_out_path']
|
|
122
|
+
|
|
123
|
+
arrays: dict[str, pa.Array] = {}
|
|
124
|
+
for c in cols:
|
|
125
|
+
vs = [r.get(c, '' if not c.startswith('row_count_') and c != 'schema_version' else 0)
|
|
126
|
+
for r in all_rows]
|
|
127
|
+
if c.startswith('row_count_') or c == 'schema_version':
|
|
128
|
+
arrays[c] = pa.array(vs, type=pa.int64())
|
|
129
|
+
else:
|
|
130
|
+
arrays[c] = pa.array([str(v) for v in vs], type=pa.string())
|
|
131
|
+
table = pa.table(arrays)
|
|
132
|
+
|
|
133
|
+
os.makedirs(_paths.data_root(root), exist_ok=True)
|
|
134
|
+
papq.write_table(table, _paths.catalog_parquet(root), compression='snappy')
|
|
135
|
+
pacsv.write_csv(table, _paths.catalog_csv(root))
|
|
136
|
+
|
|
137
|
+
summary = {
|
|
138
|
+
'schema_version': max((r['schema_version'] for r in all_rows), default=0),
|
|
139
|
+
'build_timestamp': _dt.datetime.now(_dt.timezone.utc).replace(microsecond=0).isoformat(),
|
|
140
|
+
'drop_count': len({(r['area'], r['drop_date'], r['drop_label']) for r in all_rows}),
|
|
141
|
+
'capture_count': len(all_rows),
|
|
142
|
+
'areas': sorted({r['area'] for r in all_rows}),
|
|
143
|
+
}
|
|
144
|
+
with open(_paths.catalog_json(root), 'w', encoding='utf-8') as f:
|
|
145
|
+
json.dump(summary, f, indent=2)
|
|
146
|
+
|
|
147
|
+
return summary
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
if __name__ == '__main__':
|
|
151
|
+
import sys
|
|
152
|
+
root = sys.argv[1] if len(sys.argv) > 1 else '.'
|
|
153
|
+
s = build_catalog(root)
|
|
154
|
+
print(json.dumps(s, indent=2))
|
bobframes/cli.py
ADDED
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
"""Console entry point for the ``bobframes`` command (c11 dispatcher).
|
|
2
|
+
|
|
3
|
+
A single argparse dispatcher over the verbs in ARCHITECTURE §4. ``<root>`` is positional with
|
|
4
|
+
default ``.`` across every verb; flags are long-form only. Wired as ``[project.scripts]
|
|
5
|
+
bobframes = "bobframes.cli:main"`` and runnable as ``python -m bobframes.cli``.
|
|
6
|
+
|
|
7
|
+
Heavy modules (pipeline, reports, pyarrow) are imported lazily inside each handler so that fast
|
|
8
|
+
verbs like ``version`` / ``check`` stay cheap.
|
|
9
|
+
|
|
10
|
+
Exit codes (ARCHITECTURE §4): 0 success · 1 pipeline/build failure · 2 user error (argparse) ·
|
|
11
|
+
3 external tool missing · 4 interrupted.
|
|
12
|
+
"""
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import argparse
|
|
16
|
+
import logging
|
|
17
|
+
import os
|
|
18
|
+
import sys
|
|
19
|
+
|
|
20
|
+
# CLI report name -> reports submodule. `build(root)` on each builds one report.
|
|
21
|
+
_REPORTS = {
|
|
22
|
+
'draws-by-class': 'draws_by_class',
|
|
23
|
+
'trend': 'trend_table',
|
|
24
|
+
'instancing': 'instancing_opportunities',
|
|
25
|
+
'pass-gpu': 'pass_gpu',
|
|
26
|
+
'shader': 'shader_hotlist',
|
|
27
|
+
'overdraw': 'overdraw',
|
|
28
|
+
'dashboard': '_dashboard',
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _configure_logging(verbose: bool) -> None:
|
|
33
|
+
"""Attach the shared ``[HH:MM:SS] message`` handler to the 'bobframes' logger (G-8)."""
|
|
34
|
+
logger = logging.getLogger('bobframes')
|
|
35
|
+
logger.setLevel(logging.DEBUG if verbose else logging.INFO)
|
|
36
|
+
if logger.handlers:
|
|
37
|
+
return
|
|
38
|
+
h = logging.StreamHandler(sys.stdout)
|
|
39
|
+
h.setFormatter(logging.Formatter('[%(asctime)s] %(message)s', datefmt='%H:%M:%S'))
|
|
40
|
+
logger.addHandler(h)
|
|
41
|
+
logger.propagate = False
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
# --- verb handlers -----------------------------------------------------------
|
|
45
|
+
|
|
46
|
+
def _cmd_version(args: argparse.Namespace) -> int:
|
|
47
|
+
from . import __version__, schemas
|
|
48
|
+
try:
|
|
49
|
+
import pyarrow
|
|
50
|
+
pa = pyarrow.__version__
|
|
51
|
+
except Exception:
|
|
52
|
+
pa = 'not installed'
|
|
53
|
+
print(f'bobframes {__version__} schema {schemas.SCHEMA_VERSION} pyarrow {pa}')
|
|
54
|
+
return 0
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _cmd_ingest(args: argparse.Namespace) -> int:
|
|
58
|
+
from . import run
|
|
59
|
+
rargv = ['--root', os.path.abspath(args.root)]
|
|
60
|
+
if args.area:
|
|
61
|
+
rargv += ['--area', args.area]
|
|
62
|
+
if args.label:
|
|
63
|
+
rargv += ['--label', args.label]
|
|
64
|
+
if args.capture:
|
|
65
|
+
rargv += ['--capture', args.capture]
|
|
66
|
+
if args.force:
|
|
67
|
+
rargv += ['--force']
|
|
68
|
+
if args.render_only:
|
|
69
|
+
rargv += ['--render-only']
|
|
70
|
+
rargv += ['--workers', str(args.workers), '--pixel-grid', str(args.pixel_grid)]
|
|
71
|
+
return run.main(rargv)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _cmd_render(args: argparse.Namespace) -> int:
|
|
75
|
+
from . import run
|
|
76
|
+
rargv = ['--root', os.path.abspath(args.root), '--render-only']
|
|
77
|
+
if args.area:
|
|
78
|
+
rargv += ['--area', args.area]
|
|
79
|
+
if args.label:
|
|
80
|
+
rargv += ['--label', args.label]
|
|
81
|
+
return run.main(rargv)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _cmd_ab(args: argparse.Namespace) -> int:
|
|
85
|
+
from .reports import ab
|
|
86
|
+
argv = [os.path.abspath(args.root),
|
|
87
|
+
'--baseline-label', args.baseline_label,
|
|
88
|
+
'--compare-label', args.compare_label]
|
|
89
|
+
if args.baseline_date:
|
|
90
|
+
argv += ['--baseline-date', args.baseline_date]
|
|
91
|
+
if args.compare_date:
|
|
92
|
+
argv += ['--compare-date', args.compare_date]
|
|
93
|
+
return ab.main(argv)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _cmd_report(args: argparse.Namespace) -> int:
|
|
97
|
+
import importlib
|
|
98
|
+
modname = _REPORTS.get(args.name)
|
|
99
|
+
if modname is None:
|
|
100
|
+
print(f'unknown report {args.name!r}; choose from: {", ".join(sorted(_REPORTS))}',
|
|
101
|
+
file=sys.stderr)
|
|
102
|
+
return 2
|
|
103
|
+
mod = importlib.import_module(f'.reports.{modname}', package='bobframes')
|
|
104
|
+
out = mod.build(os.path.abspath(args.root))
|
|
105
|
+
print(f'wrote {out}')
|
|
106
|
+
return 0
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def _cmd_catalog(args: argparse.Namespace) -> int:
|
|
110
|
+
from . import catalog
|
|
111
|
+
log = logging.getLogger('bobframes')
|
|
112
|
+
summary = catalog.build_catalog(os.path.abspath(args.root))
|
|
113
|
+
log.info(f"catalog: {summary['drop_count']} drops, {summary['capture_count']} captures")
|
|
114
|
+
return 0
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def _cmd_lint(args: argparse.Namespace) -> int:
|
|
118
|
+
from . import lint
|
|
119
|
+
rc = 0
|
|
120
|
+
for path in args.files:
|
|
121
|
+
hits = lint.lint_file(path)
|
|
122
|
+
for lineno, label, snip in hits:
|
|
123
|
+
print(f'{path}:{lineno}: [{label}] {snip}')
|
|
124
|
+
rc = 1
|
|
125
|
+
return rc
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def _cmd_check(args: argparse.Namespace) -> int:
|
|
129
|
+
if sys.platform != 'win32':
|
|
130
|
+
print('bobframes v1 is Windows-only (qrenderdoc replay requirement). '
|
|
131
|
+
'Track GH issue for Linux/macOS support.', file=sys.stderr)
|
|
132
|
+
return 3
|
|
133
|
+
if args.write_config:
|
|
134
|
+
print('--write-config is a v0.2 feature (config layer); not available yet.',
|
|
135
|
+
file=sys.stderr)
|
|
136
|
+
return 2
|
|
137
|
+
from . import qrd_harness, rdcmd
|
|
138
|
+
missing = False
|
|
139
|
+
for name, finder in (('renderdoccmd', rdcmd.find_renderdoccmd),
|
|
140
|
+
('qrenderdoc', qrd_harness.find_qrenderdoc)):
|
|
141
|
+
try:
|
|
142
|
+
print(f'{name}: {finder()}')
|
|
143
|
+
except FileNotFoundError:
|
|
144
|
+
print(f'{name}: NOT FOUND', file=sys.stderr)
|
|
145
|
+
missing = True
|
|
146
|
+
return 3 if missing else 0
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def _cmd_serve(args: argparse.Namespace) -> int:
|
|
150
|
+
import functools
|
|
151
|
+
import http.server
|
|
152
|
+
import socketserver
|
|
153
|
+
|
|
154
|
+
root = os.path.abspath(args.root)
|
|
155
|
+
handler = functools.partial(http.server.SimpleHTTPRequestHandler, directory=root)
|
|
156
|
+
log = logging.getLogger('bobframes')
|
|
157
|
+
try:
|
|
158
|
+
with socketserver.TCPServer((args.bind, args.port), handler) as httpd:
|
|
159
|
+
log.info(f'serving {root} at http://{args.bind}:{args.port} (Ctrl+C to stop)')
|
|
160
|
+
httpd.serve_forever()
|
|
161
|
+
except KeyboardInterrupt:
|
|
162
|
+
return 4
|
|
163
|
+
return 0
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def _cmd_smoke(args: argparse.Namespace) -> int:
|
|
167
|
+
# End-to-end smoke (G-12). No --data → render-only against the bundled synthetic fixture;
|
|
168
|
+
# --data DIR → full ingest + render against a real capture root.
|
|
169
|
+
from .tests import smoke
|
|
170
|
+
return smoke.main(data=args.data, pixel_grid=getattr(args, 'pixel_grid', 4))
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
# --- parser ------------------------------------------------------------------
|
|
174
|
+
|
|
175
|
+
def _build_parser() -> argparse.ArgumentParser:
|
|
176
|
+
common = argparse.ArgumentParser(add_help=False)
|
|
177
|
+
common.add_argument('--verbose', action='store_true', help='DEBUG-level logging')
|
|
178
|
+
|
|
179
|
+
p = argparse.ArgumentParser(
|
|
180
|
+
prog='bobframes',
|
|
181
|
+
description='RenderDoc capture pipeline: ingest, analyze, render.')
|
|
182
|
+
sub = p.add_subparsers(dest='verb', metavar='<verb>')
|
|
183
|
+
|
|
184
|
+
sp = sub.add_parser('ingest', parents=[common],
|
|
185
|
+
help='full pipeline: export, parse, replay, parquetize, derive, render')
|
|
186
|
+
sp.add_argument('root', nargs='?', default='.')
|
|
187
|
+
sp.add_argument('--area')
|
|
188
|
+
sp.add_argument('--label')
|
|
189
|
+
sp.add_argument('--capture')
|
|
190
|
+
sp.add_argument('--force', action='store_true')
|
|
191
|
+
sp.add_argument('--render-only', action='store_true')
|
|
192
|
+
sp.add_argument('--workers', type=int, default=min(4, os.cpu_count() or 4))
|
|
193
|
+
sp.add_argument('--pixel-grid', type=int, default=4)
|
|
194
|
+
sp.set_defaults(func=_cmd_ingest)
|
|
195
|
+
|
|
196
|
+
sp = sub.add_parser('render', parents=[common],
|
|
197
|
+
help='render-only: rebuild HTML + catalog from existing Parquet')
|
|
198
|
+
sp.add_argument('root', nargs='?', default='.')
|
|
199
|
+
sp.add_argument('--area')
|
|
200
|
+
sp.add_argument('--label')
|
|
201
|
+
sp.set_defaults(func=_cmd_render)
|
|
202
|
+
|
|
203
|
+
sp = sub.add_parser('ab', parents=[common], help='all reports for one drop pair')
|
|
204
|
+
sp.add_argument('root', nargs='?', default='.')
|
|
205
|
+
sp.add_argument('--baseline-label', required=True)
|
|
206
|
+
sp.add_argument('--compare-label', required=True)
|
|
207
|
+
sp.add_argument('--baseline-date')
|
|
208
|
+
sp.add_argument('--compare-date')
|
|
209
|
+
sp.set_defaults(func=_cmd_ab)
|
|
210
|
+
|
|
211
|
+
sp = sub.add_parser('report', parents=[common], help='build one named report')
|
|
212
|
+
sp.add_argument('name', choices=sorted(_REPORTS))
|
|
213
|
+
sp.add_argument('root', nargs='?', default='.')
|
|
214
|
+
sp.set_defaults(func=_cmd_report)
|
|
215
|
+
|
|
216
|
+
sp = sub.add_parser('catalog', parents=[common], help='rebuild _data/_catalog.parquet only')
|
|
217
|
+
sp.add_argument('root', nargs='?', default='.')
|
|
218
|
+
sp.set_defaults(func=_cmd_catalog)
|
|
219
|
+
|
|
220
|
+
sp = sub.add_parser('lint', parents=[common], help='lint HTML/MD files against the banlist')
|
|
221
|
+
sp.add_argument('files', nargs='+')
|
|
222
|
+
sp.set_defaults(func=_cmd_lint)
|
|
223
|
+
|
|
224
|
+
sp = sub.add_parser('check', parents=[common], help='resolve external tool paths')
|
|
225
|
+
sp.add_argument('--write-config', action='store_true', help='(v0.2)')
|
|
226
|
+
sp.set_defaults(func=_cmd_check)
|
|
227
|
+
|
|
228
|
+
sp = sub.add_parser('version', parents=[common], help='print version, schema, pyarrow')
|
|
229
|
+
sp.set_defaults(func=_cmd_version)
|
|
230
|
+
|
|
231
|
+
sp = sub.add_parser('serve', parents=[common], help='static preview server')
|
|
232
|
+
sp.add_argument('root', nargs='?', default='.')
|
|
233
|
+
sp.add_argument('--port', type=int, default=8000)
|
|
234
|
+
sp.add_argument('--bind', default='127.0.0.1')
|
|
235
|
+
sp.set_defaults(func=_cmd_serve)
|
|
236
|
+
|
|
237
|
+
sp = sub.add_parser('smoke', parents=[common], help='end-to-end smoke test')
|
|
238
|
+
sp.add_argument('--data', help='capture dir (default: bundled synthetic; c15)')
|
|
239
|
+
sp.set_defaults(func=_cmd_smoke)
|
|
240
|
+
|
|
241
|
+
return p
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def main(argv: list[str] | None = None) -> int:
|
|
245
|
+
argv = list(sys.argv[1:] if argv is None else argv)
|
|
246
|
+
parser = _build_parser()
|
|
247
|
+
|
|
248
|
+
if not argv:
|
|
249
|
+
parser.print_help()
|
|
250
|
+
return 0
|
|
251
|
+
|
|
252
|
+
args = parser.parse_args(argv)
|
|
253
|
+
if not getattr(args, 'func', None):
|
|
254
|
+
parser.print_help()
|
|
255
|
+
return 0
|
|
256
|
+
|
|
257
|
+
_configure_logging(getattr(args, 'verbose', False))
|
|
258
|
+
try:
|
|
259
|
+
return args.func(args)
|
|
260
|
+
except KeyboardInterrupt:
|
|
261
|
+
print('interrupted', file=sys.stderr)
|
|
262
|
+
return 4
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
if __name__ == '__main__':
|
|
266
|
+
sys.exit(main())
|