bobframes 0.1.0__tar.gz
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-0.1.0/.gitignore +57 -0
- bobframes-0.1.0/CHANGELOG.md +45 -0
- bobframes-0.1.0/LICENSE +21 -0
- bobframes-0.1.0/PKG-INFO +144 -0
- bobframes-0.1.0/README.md +114 -0
- bobframes-0.1.0/bobframes/__init__.py +3 -0
- bobframes-0.1.0/bobframes/_version.py +1 -0
- bobframes-0.1.0/bobframes/catalog.py +154 -0
- bobframes-0.1.0/bobframes/cli.py +266 -0
- bobframes-0.1.0/bobframes/derive_post_merge.py +365 -0
- bobframes-0.1.0/bobframes/derives/__init__.py +0 -0
- bobframes-0.1.0/bobframes/derives/pass_class_breakdown.py +102 -0
- bobframes-0.1.0/bobframes/derives/texture_usage.py +121 -0
- bobframes-0.1.0/bobframes/discovery.py +132 -0
- bobframes-0.1.0/bobframes/global_entities.py +99 -0
- bobframes-0.1.0/bobframes/html/__init__.py +0 -0
- bobframes-0.1.0/bobframes/html/template.py +1056 -0
- bobframes-0.1.0/bobframes/lint.py +114 -0
- bobframes-0.1.0/bobframes/manifest.py +127 -0
- bobframes-0.1.0/bobframes/parquetize.py +282 -0
- bobframes-0.1.0/bobframes/parsers/__init__.py +0 -0
- bobframes-0.1.0/bobframes/parsers/derive_program_transitions.py +73 -0
- bobframes-0.1.0/bobframes/parsers/parse_init_state.py +675 -0
- bobframes-0.1.0/bobframes/paths.py +111 -0
- bobframes-0.1.0/bobframes/probes/__init__.py +0 -0
- bobframes-0.1.0/bobframes/probes/whatif.py +165 -0
- bobframes-0.1.0/bobframes/qrd_harness.py +119 -0
- bobframes-0.1.0/bobframes/query_examples.py +222 -0
- bobframes-0.1.0/bobframes/rdcmd.py +72 -0
- bobframes-0.1.0/bobframes/replay/__init__.py +26 -0
- bobframes-0.1.0/bobframes/replay/replay_main.py +2305 -0
- bobframes-0.1.0/bobframes/reports/__init__.py +0 -0
- bobframes-0.1.0/bobframes/reports/_dashboard.py +425 -0
- bobframes-0.1.0/bobframes/reports/ab.py +88 -0
- bobframes-0.1.0/bobframes/reports/base.py +114 -0
- bobframes-0.1.0/bobframes/reports/cache.py +147 -0
- bobframes-0.1.0/bobframes/reports/chrome.py +1306 -0
- bobframes-0.1.0/bobframes/reports/cli.py +99 -0
- bobframes-0.1.0/bobframes/reports/delta.py +167 -0
- bobframes-0.1.0/bobframes/reports/discovery.py +118 -0
- bobframes-0.1.0/bobframes/reports/draws_by_class.py +165 -0
- bobframes-0.1.0/bobframes/reports/formatters.py +122 -0
- bobframes-0.1.0/bobframes/reports/instancing_opportunities.py +276 -0
- bobframes-0.1.0/bobframes/reports/orchestrator.py +59 -0
- bobframes-0.1.0/bobframes/reports/overdraw.py +293 -0
- bobframes-0.1.0/bobframes/reports/pass_gpu.py +190 -0
- bobframes-0.1.0/bobframes/reports/shader_hotlist.py +240 -0
- bobframes-0.1.0/bobframes/reports/trend_table.py +444 -0
- bobframes-0.1.0/bobframes/resource_labels.py +162 -0
- bobframes-0.1.0/bobframes/run.py +480 -0
- bobframes-0.1.0/bobframes/schemas.py +426 -0
- bobframes-0.1.0/bobframes/stable_keys.py +83 -0
- bobframes-0.1.0/bobframes/tests/__init__.py +0 -0
- bobframes-0.1.0/bobframes/tests/_render_util.py +84 -0
- bobframes-0.1.0/bobframes/tests/data/golden/_reports/draws_by_class.html +323 -0
- bobframes-0.1.0/bobframes/tests/data/golden/_reports/drill/District 01/2026-05-28_r110600/index.html +1560 -0
- bobframes-0.1.0/bobframes/tests/data/golden/_reports/index.html +264 -0
- bobframes-0.1.0/bobframes/tests/data/golden/_reports/instancing_opportunities.html +266 -0
- bobframes-0.1.0/bobframes/tests/data/golden/_reports/overdraw.html +275 -0
- bobframes-0.1.0/bobframes/tests/data/golden/_reports/pass_gpu.html +277 -0
- bobframes-0.1.0/bobframes/tests/data/golden/_reports/shader_hotlist.html +265 -0
- bobframes-0.1.0/bobframes/tests/data/golden/_reports/trend_table.html +390 -0
- bobframes-0.1.0/bobframes/tests/data/golden/index.html +1175 -0
- bobframes-0.1.0/bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/_manifest.json +51 -0
- bobframes-0.1.0/bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/buffers.parquet +0 -0
- bobframes-0.1.0/bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/clears.parquet +0 -0
- bobframes-0.1.0/bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/counters_per_event.parquet +0 -0
- bobframes-0.1.0/bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/descriptor_access.parquet +0 -0
- bobframes-0.1.0/bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/dispatches.parquet +0 -0
- bobframes-0.1.0/bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/draw_bindings.parquet +0 -0
- bobframes-0.1.0/bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/draws.parquet +0 -0
- bobframes-0.1.0/bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/events.parquet +0 -0
- bobframes-0.1.0/bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/fbos.parquet +0 -0
- bobframes-0.1.0/bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/frame_totals.parquet +0 -0
- bobframes-0.1.0/bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/ibo_samples.parquet +0 -0
- bobframes-0.1.0/bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/indirect_args.parquet +0 -0
- bobframes-0.1.0/bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/passes.parquet +0 -0
- bobframes-0.1.0/bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/pixel_history.parquet +0 -0
- bobframes-0.1.0/bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/post_vs_samples.parquet +0 -0
- bobframes-0.1.0/bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/program_transitions.parquet +0 -0
- bobframes-0.1.0/bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/programs.parquet +0 -0
- bobframes-0.1.0/bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/render_targets.parquet +0 -0
- bobframes-0.1.0/bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/resource_creation.parquet +0 -0
- bobframes-0.1.0/bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/rt_event_timeline.parquet +0 -0
- bobframes-0.1.0/bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/samplers.parquet +0 -0
- bobframes-0.1.0/bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/shaders.parquet +0 -0
- bobframes-0.1.0/bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/state_change_events.parquet +0 -0
- bobframes-0.1.0/bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/texture_samples.parquet +0 -0
- bobframes-0.1.0/bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/textures.parquet +0 -0
- bobframes-0.1.0/bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/vbo_samples.parquet +0 -0
- bobframes-0.1.0/bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/vertex_inputs.parquet +0 -0
- bobframes-0.1.0/bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/_manifest.json +51 -0
- bobframes-0.1.0/bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/buffers.parquet +0 -0
- bobframes-0.1.0/bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/clears.parquet +0 -0
- bobframes-0.1.0/bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/counters_per_event.parquet +0 -0
- bobframes-0.1.0/bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/descriptor_access.parquet +0 -0
- bobframes-0.1.0/bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/dispatches.parquet +0 -0
- bobframes-0.1.0/bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/draw_bindings.parquet +0 -0
- bobframes-0.1.0/bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/draws.parquet +0 -0
- bobframes-0.1.0/bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/events.parquet +0 -0
- bobframes-0.1.0/bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/fbos.parquet +0 -0
- bobframes-0.1.0/bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/frame_totals.parquet +0 -0
- bobframes-0.1.0/bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/ibo_samples.parquet +0 -0
- bobframes-0.1.0/bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/indirect_args.parquet +0 -0
- bobframes-0.1.0/bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/passes.parquet +0 -0
- bobframes-0.1.0/bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/pixel_history.parquet +0 -0
- bobframes-0.1.0/bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/post_vs_samples.parquet +0 -0
- bobframes-0.1.0/bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/program_transitions.parquet +0 -0
- bobframes-0.1.0/bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/programs.parquet +0 -0
- bobframes-0.1.0/bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/render_targets.parquet +0 -0
- bobframes-0.1.0/bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/resource_creation.parquet +0 -0
- bobframes-0.1.0/bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/rt_event_timeline.parquet +0 -0
- bobframes-0.1.0/bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/samplers.parquet +0 -0
- bobframes-0.1.0/bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/shaders.parquet +0 -0
- bobframes-0.1.0/bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/state_change_events.parquet +0 -0
- bobframes-0.1.0/bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/texture_samples.parquet +0 -0
- bobframes-0.1.0/bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/textures.parquet +0 -0
- bobframes-0.1.0/bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/vbo_samples.parquet +0 -0
- bobframes-0.1.0/bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/vertex_inputs.parquet +0 -0
- bobframes-0.1.0/bobframes/tests/make_synthetic.py +171 -0
- bobframes-0.1.0/bobframes/tests/smoke.py +199 -0
- bobframes-0.1.0/bobframes/tests/test_determinism.py +19 -0
- bobframes-0.1.0/bobframes/tests/test_discovery.py +97 -0
- bobframes-0.1.0/bobframes/tests/test_hardening.py +142 -0
- bobframes-0.1.0/bobframes/tests/test_parity.py +22 -0
- bobframes-0.1.0/bobframes/tests/test_perf.py +18 -0
- bobframes-0.1.0/bobframes/tests/test_replay_drift.py +115 -0
- bobframes-0.1.0/bobframes/tests/test_schemas.py +26 -0
- bobframes-0.1.0/bobframes/tests/test_schemas_unit.py +55 -0
- bobframes-0.1.0/bobframes/tests/test_stable_keys.py +61 -0
- bobframes-0.1.0/pyproject.toml +56 -0
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# Python
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*$py.class
|
|
5
|
+
.Python
|
|
6
|
+
*.egg-info/
|
|
7
|
+
.eggs/
|
|
8
|
+
*.egg
|
|
9
|
+
build/
|
|
10
|
+
dist/
|
|
11
|
+
wheels/
|
|
12
|
+
pip-wheel-metadata/
|
|
13
|
+
|
|
14
|
+
# Virtual env
|
|
15
|
+
.venv/
|
|
16
|
+
venv/
|
|
17
|
+
env/
|
|
18
|
+
|
|
19
|
+
# Editor
|
|
20
|
+
.idea/
|
|
21
|
+
.vscode/
|
|
22
|
+
*.swp
|
|
23
|
+
.DS_Store
|
|
24
|
+
|
|
25
|
+
# Test artifacts
|
|
26
|
+
.pytest_cache/
|
|
27
|
+
.coverage
|
|
28
|
+
htmlcov/
|
|
29
|
+
.tox/
|
|
30
|
+
|
|
31
|
+
# Project-specific: never commit user data, manifests, or rendered output
|
|
32
|
+
**/_data/
|
|
33
|
+
**/_reports/
|
|
34
|
+
**/_stage/
|
|
35
|
+
**/_tmp/
|
|
36
|
+
**/index.html
|
|
37
|
+
**/*.parquet
|
|
38
|
+
**/*.rdc
|
|
39
|
+
**/*.zip.xml
|
|
40
|
+
**/_manifest.json
|
|
41
|
+
**/done.marker
|
|
42
|
+
|
|
43
|
+
# Exception: bundled test fixtures (ADR-8: synthetic _data + golden HTML are test code, not user
|
|
44
|
+
# data). Must override the broad _data/_reports/index.html/parquet/manifest ignores above. Git
|
|
45
|
+
# cannot un-ignore a file under an ignored directory, so re-include the directories first, then files.
|
|
46
|
+
!bobframes/tests/data/
|
|
47
|
+
!bobframes/tests/data/**/
|
|
48
|
+
!bobframes/tests/data/**
|
|
49
|
+
|
|
50
|
+
# Bobframes config
|
|
51
|
+
.bobframes.toml
|
|
52
|
+
|
|
53
|
+
# Generated docs (docs/plan/ IS tracked — only built output is ignored)
|
|
54
|
+
docs/_build/
|
|
55
|
+
|
|
56
|
+
# OS
|
|
57
|
+
Thumbs.db
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
Changes to this project are documented here, newest first.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
## [Unreleased]
|
|
9
|
+
|
|
10
|
+
## [0.1.0] - 2026-05-31
|
|
11
|
+
|
|
12
|
+
First standalone release. v1 is Windows-only (the replay stage drives `qrenderdoc`).
|
|
13
|
+
|
|
14
|
+
### Added
|
|
15
|
+
- Single `bobframes` binary with subcommands `ingest`, `render`, `ab`, `report`, `catalog`, `lint`,
|
|
16
|
+
`check`, `serve`, `smoke`, and `version`. Positional `root` (default `.`) across verbs; long-flag
|
|
17
|
+
only; exit codes `0`/`1`/`2`/`3`/`4`. stdlib `logging` at INFO by default, `--verbose` for DEBUG,
|
|
18
|
+
`[HH:MM:SS]` line format (G-8).
|
|
19
|
+
- Replay script located via `importlib.resources` (`bobframes.replay.replay_script_path`) so replay
|
|
20
|
+
works from an installed wheel, not just an in-tree checkout.
|
|
21
|
+
- Reliability hardening: atomic writes for `_manifest.json`, Parquet+CSV pairs, and `done.marker`
|
|
22
|
+
(`.tmp` + `os.replace`, rollback on failure); process-tree kill (`taskkill /T /F`) when a
|
|
23
|
+
`qrenderdoc` replay times out; per-capture replay-failure isolation
|
|
24
|
+
(`capture_status='replay_failed'` instead of aborting the whole drop); subprocess stderr logged on
|
|
25
|
+
convert-timeout and on every parse; manifest provenance fields `tool_versions` and `host_info`.
|
|
26
|
+
- Test gates: golden-snapshot parity, schema regression, determinism, performance, and a replay
|
|
27
|
+
column-drift guard against `schemas.py` (H-6); mocked-subprocess tests for the hardening branches
|
|
28
|
+
the GPU-less runner cannot exercise; a data-driven `smoke` (render-only against a bundled synthetic
|
|
29
|
+
fixture by default); unit tests for stable keys, schemas, and drop discovery.
|
|
30
|
+
- GitHub Actions CI across a Windows / Python / pyarrow matrix, with a tag-gated PyPI publish job.
|
|
31
|
+
|
|
32
|
+
### Changed
|
|
33
|
+
- **Stable-key format upgrade:** stable keys now carry a `KEY_VERSION = 1` version byte in the hash
|
|
34
|
+
input. Keys produced before this release are not comparable with `0.1.0` keys; rebuild affected
|
|
35
|
+
data with `bobframes ingest --force`. `KEY_VERSION` bumps on any future key-derivation rule change.
|
|
36
|
+
- Timestamps unified to a single UTC `now_iso()` helper (`bobframes.manifest.now_iso`); the
|
|
37
|
+
local-time variant in `reports/cli` was dropped.
|
|
38
|
+
|
|
39
|
+
### Removed
|
|
40
|
+
- The project-embedded `_analysis` package. `python -m _analysis.run` and the other
|
|
41
|
+
`python -m _analysis.*` entry points no longer work; switch to the `bobframes` commands (see the
|
|
42
|
+
migration table in the README). This is a hard rename with no compatibility shim.
|
|
43
|
+
|
|
44
|
+
[Unreleased]: https://github.com/altpsyche/bobframes/compare/v0.1.0...HEAD
|
|
45
|
+
[0.1.0]: https://github.com/altpsyche/bobframes/releases/tag/v0.1.0
|
bobframes-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Siva Subramanyam, Mayhem Studios
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
bobframes-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: bobframes
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: RenderDoc capture pipeline: ingest, analyze, render.
|
|
5
|
+
Project-URL: Homepage, https://github.com/altpsyche/bobframes
|
|
6
|
+
Project-URL: Issues, https://github.com/altpsyche/bobframes/issues
|
|
7
|
+
Project-URL: Changelog, https://github.com/altpsyche/bobframes/blob/main/CHANGELOG.md
|
|
8
|
+
Author-email: Siva Subramanyam <sivasubramanyam@mayhem-studios.com>
|
|
9
|
+
License: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: gpu,parquet,profiling,renderdoc
|
|
12
|
+
Classifier: Development Status :: 3 - Alpha
|
|
13
|
+
Classifier: Environment :: Console
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Operating System :: Microsoft :: Windows
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
20
|
+
Classifier: Topic :: Multimedia :: Graphics
|
|
21
|
+
Classifier: Topic :: Software Development :: Debuggers
|
|
22
|
+
Requires-Python: <3.15,>=3.10
|
|
23
|
+
Requires-Dist: pyarrow<22,>=17
|
|
24
|
+
Provides-Extra: dev
|
|
25
|
+
Requires-Dist: build; extra == 'dev'
|
|
26
|
+
Requires-Dist: hatchling; extra == 'dev'
|
|
27
|
+
Requires-Dist: pytest; extra == 'dev'
|
|
28
|
+
Requires-Dist: twine; extra == 'dev'
|
|
29
|
+
Description-Content-Type: text/markdown
|
|
30
|
+
|
|
31
|
+
# BobFrames
|
|
32
|
+
|
|
33
|
+
RenderDoc capture pipeline: ingest, analyze, render. Point it at a folder of `.rdc` captures and it
|
|
34
|
+
produces `_data/` (Parquet tables) plus `_reports/` (static HTML you can browse). Windows-only in v1.
|
|
35
|
+
|
|
36
|
+
## Requirements
|
|
37
|
+
|
|
38
|
+
- Windows 10 or later (the replay stage drives `qrenderdoc`, which is Windows-only in v1).
|
|
39
|
+
- Python 3.10 - 3.13.
|
|
40
|
+
- RenderDoc 1.x, or Arm Performance Studio, providing `renderdoccmd` and `qrenderdoc` on disk.
|
|
41
|
+
|
|
42
|
+
## Install
|
|
43
|
+
|
|
44
|
+
```
|
|
45
|
+
pipx install bobframes
|
|
46
|
+
bobframes check
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
`bobframes check` prints the resolved paths for `renderdoccmd` and `qrenderdoc` and exits non-zero if
|
|
50
|
+
either is missing, so you can confirm the toolchain before a long ingest.
|
|
51
|
+
|
|
52
|
+
## Quickstart
|
|
53
|
+
|
|
54
|
+
```
|
|
55
|
+
cd path\to\captures # a folder of <Area>\<YYYY-MM-DD[_label]>\*.rdc
|
|
56
|
+
bobframes ingest . # export, parse, replay, parquetize, derive, render
|
|
57
|
+
bobframes serve . # open a local static preview of the reports
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
`ingest` writes Parquet under `_data/` and HTML under `_reports/`. Re-run `bobframes render .` any
|
|
61
|
+
time to rebuild the HTML from existing Parquet without re-replaying captures.
|
|
62
|
+
|
|
63
|
+
## Commands
|
|
64
|
+
|
|
65
|
+
| Command | Purpose |
|
|
66
|
+
|---|---|
|
|
67
|
+
| `ingest [root] [--area X] [--label Y] [--capture N] [--force] [--pixel-grid 4] [--render-only]` | Full pipeline: export, parse, replay, parquetize, derive, manifest, commit, catalog, render. |
|
|
68
|
+
| `render [root] [--area X] [--label Y]` | Rebuild HTML and catalog from existing Parquet. |
|
|
69
|
+
| `ab [root] --baseline-label X --compare-label Y` | All reports for one drop pair under `_reports/ab/<pair>/`. |
|
|
70
|
+
| `report [root] <name>` | Build one named report (draws-by-class, trend, instancing, pass-gpu, shader, overdraw, dashboard). |
|
|
71
|
+
| `catalog [root]` | Rebuild `_data/_catalog.parquet` only. |
|
|
72
|
+
| `lint <file>...` | Check HTML or markdown against the banlist. |
|
|
73
|
+
| `check` | Print resolved tool paths; non-zero when a tool is missing. |
|
|
74
|
+
| `serve [root] [--port 8000] [--bind 127.0.0.1]` | Static preview via the stdlib HTTP server. |
|
|
75
|
+
| `smoke [--data DIR]` | End-to-end check; render-only against the bundled fixture when `--data` is omitted. |
|
|
76
|
+
| `version` | Print `bobframes`, schema, and pyarrow versions. |
|
|
77
|
+
|
|
78
|
+
`<root>` is positional and defaults to `.`. Flags are long-form only. Exit codes: `0` success,
|
|
79
|
+
`1` pipeline or build failure, `2` usage error, `3` external tool missing, `4` interrupted.
|
|
80
|
+
|
|
81
|
+
## External tools
|
|
82
|
+
|
|
83
|
+
The export stage runs `renderdoccmd convert`; the replay stage runs `qrenderdoc --python`. v1 looks
|
|
84
|
+
for both at a baked Arm Performance Studio install path and on `PATH`. A config file and a
|
|
85
|
+
tool-resolver with version globbing arrive in v0.2; until then, install RenderDoc where v1 expects it
|
|
86
|
+
or put the executables on `PATH`. Run `bobframes check` to see what was resolved.
|
|
87
|
+
|
|
88
|
+
## Output layout
|
|
89
|
+
|
|
90
|
+
```
|
|
91
|
+
<root>/
|
|
92
|
+
index.html root catalog view
|
|
93
|
+
<area>/<drop>/ raw RDC inputs (left untouched)
|
|
94
|
+
_data/ pipeline outputs
|
|
95
|
+
_catalog.parquet (+ .csv, .json)
|
|
96
|
+
_global_entities.parquet (+ .csv)
|
|
97
|
+
_query_examples.md
|
|
98
|
+
<area>/<drop>/ per-drop data (29 Parquet tables)
|
|
99
|
+
*.parquet (+ matching .csv)
|
|
100
|
+
_manifest.json, _resource_labels.json
|
|
101
|
+
shader_src/*.glsl, jsonl sidecars
|
|
102
|
+
done.marker
|
|
103
|
+
_reports/ rendered HTML
|
|
104
|
+
*.html (dashboard + reports)
|
|
105
|
+
ab/<pair>/*.html
|
|
106
|
+
drill/<area>/<drop>/index.html per-drop browser
|
|
107
|
+
_cache/
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
The catalog stores each drop's path relative to `<root>` for portability.
|
|
111
|
+
|
|
112
|
+
## Migrating from `_analysis`
|
|
113
|
+
|
|
114
|
+
v1 is a hard rename of the older project-embedded `_analysis` package, with no compatibility shim:
|
|
115
|
+
`python -m _analysis.*` stops working once `bobframes` is installed. Map old invocations to new ones:
|
|
116
|
+
|
|
117
|
+
| Old (`_analysis`) | New (`bobframes`) |
|
|
118
|
+
|---|---|
|
|
119
|
+
| `python -m _analysis.run --root . --area X --label Y` | `bobframes ingest . --area X --label Y` |
|
|
120
|
+
| `python -m _analysis.reports.ab --root . --baseline-label X --compare-label Y` | `bobframes ab . --baseline-label X --compare-label Y` |
|
|
121
|
+
| `python -m _analysis.lint <file>` | `bobframes lint <file>` |
|
|
122
|
+
| `python -m _analysis.tests.smoke` | `bobframes smoke` |
|
|
123
|
+
|
|
124
|
+
## Troubleshooting
|
|
125
|
+
|
|
126
|
+
| Symptom | Resolution |
|
|
127
|
+
|---|---|
|
|
128
|
+
| `renderdoccmd not found` (exit 3) | Install RenderDoc or Arm Performance Studio, or put the executable on `PATH`; confirm with `bobframes check`. |
|
|
129
|
+
| `qrenderdoc` replay hangs | v1 kills the replay process tree on timeout and records `capture_status='replay_failed'` for that capture; the rest of the drop still completes. Re-run with `--force` to retry. |
|
|
130
|
+
| Lint failure during render | The emitted HTML contains a banned token. Run `bobframes lint <file>` to see the line and label. |
|
|
131
|
+
| `schema mismatch` (exit 1) | A drop's `_manifest.json` schema version differs from the installed schema. Rebuild it with `bobframes ingest --force` (the v1 schema-migration path; see G-3). |
|
|
132
|
+
| Permission denied on `_data` | Close any viewer holding a Parquet open, then re-run; writes are staged and renamed atomically. |
|
|
133
|
+
|
|
134
|
+
## Advanced
|
|
135
|
+
|
|
136
|
+
- A/B reports: `bobframes ab . --baseline-label OLD --compare-label NEW` builds a side-by-side set.
|
|
137
|
+
- Programmatic use: import `bobframes.schemas`, `bobframes.discovery`, and `bobframes.paths` to drive
|
|
138
|
+
table lookups, drop discovery, and path resolution from your own scripts.
|
|
139
|
+
- `bobframes/probes/whatif.py` is a manual qrenderdoc-side probe and is not wired as a CLI command.
|
|
140
|
+
- A TOML config file, an externalized draw classifier, and design-token theming are planned for v0.2.
|
|
141
|
+
|
|
142
|
+
## License
|
|
143
|
+
|
|
144
|
+
MIT. See [LICENSE](LICENSE).
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
# BobFrames
|
|
2
|
+
|
|
3
|
+
RenderDoc capture pipeline: ingest, analyze, render. Point it at a folder of `.rdc` captures and it
|
|
4
|
+
produces `_data/` (Parquet tables) plus `_reports/` (static HTML you can browse). Windows-only in v1.
|
|
5
|
+
|
|
6
|
+
## Requirements
|
|
7
|
+
|
|
8
|
+
- Windows 10 or later (the replay stage drives `qrenderdoc`, which is Windows-only in v1).
|
|
9
|
+
- Python 3.10 - 3.13.
|
|
10
|
+
- RenderDoc 1.x, or Arm Performance Studio, providing `renderdoccmd` and `qrenderdoc` on disk.
|
|
11
|
+
|
|
12
|
+
## Install
|
|
13
|
+
|
|
14
|
+
```
|
|
15
|
+
pipx install bobframes
|
|
16
|
+
bobframes check
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
`bobframes check` prints the resolved paths for `renderdoccmd` and `qrenderdoc` and exits non-zero if
|
|
20
|
+
either is missing, so you can confirm the toolchain before a long ingest.
|
|
21
|
+
|
|
22
|
+
## Quickstart
|
|
23
|
+
|
|
24
|
+
```
|
|
25
|
+
cd path\to\captures # a folder of <Area>\<YYYY-MM-DD[_label]>\*.rdc
|
|
26
|
+
bobframes ingest . # export, parse, replay, parquetize, derive, render
|
|
27
|
+
bobframes serve . # open a local static preview of the reports
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
`ingest` writes Parquet under `_data/` and HTML under `_reports/`. Re-run `bobframes render .` any
|
|
31
|
+
time to rebuild the HTML from existing Parquet without re-replaying captures.
|
|
32
|
+
|
|
33
|
+
## Commands
|
|
34
|
+
|
|
35
|
+
| Command | Purpose |
|
|
36
|
+
|---|---|
|
|
37
|
+
| `ingest [root] [--area X] [--label Y] [--capture N] [--force] [--pixel-grid 4] [--render-only]` | Full pipeline: export, parse, replay, parquetize, derive, manifest, commit, catalog, render. |
|
|
38
|
+
| `render [root] [--area X] [--label Y]` | Rebuild HTML and catalog from existing Parquet. |
|
|
39
|
+
| `ab [root] --baseline-label X --compare-label Y` | All reports for one drop pair under `_reports/ab/<pair>/`. |
|
|
40
|
+
| `report [root] <name>` | Build one named report (draws-by-class, trend, instancing, pass-gpu, shader, overdraw, dashboard). |
|
|
41
|
+
| `catalog [root]` | Rebuild `_data/_catalog.parquet` only. |
|
|
42
|
+
| `lint <file>...` | Check HTML or markdown against the banlist. |
|
|
43
|
+
| `check` | Print resolved tool paths; non-zero when a tool is missing. |
|
|
44
|
+
| `serve [root] [--port 8000] [--bind 127.0.0.1]` | Static preview via the stdlib HTTP server. |
|
|
45
|
+
| `smoke [--data DIR]` | End-to-end check; render-only against the bundled fixture when `--data` is omitted. |
|
|
46
|
+
| `version` | Print `bobframes`, schema, and pyarrow versions. |
|
|
47
|
+
|
|
48
|
+
`<root>` is positional and defaults to `.`. Flags are long-form only. Exit codes: `0` success,
|
|
49
|
+
`1` pipeline or build failure, `2` usage error, `3` external tool missing, `4` interrupted.
|
|
50
|
+
|
|
51
|
+
## External tools
|
|
52
|
+
|
|
53
|
+
The export stage runs `renderdoccmd convert`; the replay stage runs `qrenderdoc --python`. v1 looks
|
|
54
|
+
for both at a baked Arm Performance Studio install path and on `PATH`. A config file and a
|
|
55
|
+
tool-resolver with version globbing arrive in v0.2; until then, install RenderDoc where v1 expects it
|
|
56
|
+
or put the executables on `PATH`. Run `bobframes check` to see what was resolved.
|
|
57
|
+
|
|
58
|
+
## Output layout
|
|
59
|
+
|
|
60
|
+
```
|
|
61
|
+
<root>/
|
|
62
|
+
index.html root catalog view
|
|
63
|
+
<area>/<drop>/ raw RDC inputs (left untouched)
|
|
64
|
+
_data/ pipeline outputs
|
|
65
|
+
_catalog.parquet (+ .csv, .json)
|
|
66
|
+
_global_entities.parquet (+ .csv)
|
|
67
|
+
_query_examples.md
|
|
68
|
+
<area>/<drop>/ per-drop data (29 Parquet tables)
|
|
69
|
+
*.parquet (+ matching .csv)
|
|
70
|
+
_manifest.json, _resource_labels.json
|
|
71
|
+
shader_src/*.glsl, jsonl sidecars
|
|
72
|
+
done.marker
|
|
73
|
+
_reports/ rendered HTML
|
|
74
|
+
*.html (dashboard + reports)
|
|
75
|
+
ab/<pair>/*.html
|
|
76
|
+
drill/<area>/<drop>/index.html per-drop browser
|
|
77
|
+
_cache/
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
The catalog stores each drop's path relative to `<root>` for portability.
|
|
81
|
+
|
|
82
|
+
## Migrating from `_analysis`
|
|
83
|
+
|
|
84
|
+
v1 is a hard rename of the older project-embedded `_analysis` package, with no compatibility shim:
|
|
85
|
+
`python -m _analysis.*` stops working once `bobframes` is installed. Map old invocations to new ones:
|
|
86
|
+
|
|
87
|
+
| Old (`_analysis`) | New (`bobframes`) |
|
|
88
|
+
|---|---|
|
|
89
|
+
| `python -m _analysis.run --root . --area X --label Y` | `bobframes ingest . --area X --label Y` |
|
|
90
|
+
| `python -m _analysis.reports.ab --root . --baseline-label X --compare-label Y` | `bobframes ab . --baseline-label X --compare-label Y` |
|
|
91
|
+
| `python -m _analysis.lint <file>` | `bobframes lint <file>` |
|
|
92
|
+
| `python -m _analysis.tests.smoke` | `bobframes smoke` |
|
|
93
|
+
|
|
94
|
+
## Troubleshooting
|
|
95
|
+
|
|
96
|
+
| Symptom | Resolution |
|
|
97
|
+
|---|---|
|
|
98
|
+
| `renderdoccmd not found` (exit 3) | Install RenderDoc or Arm Performance Studio, or put the executable on `PATH`; confirm with `bobframes check`. |
|
|
99
|
+
| `qrenderdoc` replay hangs | v1 kills the replay process tree on timeout and records `capture_status='replay_failed'` for that capture; the rest of the drop still completes. Re-run with `--force` to retry. |
|
|
100
|
+
| Lint failure during render | The emitted HTML contains a banned token. Run `bobframes lint <file>` to see the line and label. |
|
|
101
|
+
| `schema mismatch` (exit 1) | A drop's `_manifest.json` schema version differs from the installed schema. Rebuild it with `bobframes ingest --force` (the v1 schema-migration path; see G-3). |
|
|
102
|
+
| Permission denied on `_data` | Close any viewer holding a Parquet open, then re-run; writes are staged and renamed atomically. |
|
|
103
|
+
|
|
104
|
+
## Advanced
|
|
105
|
+
|
|
106
|
+
- A/B reports: `bobframes ab . --baseline-label OLD --compare-label NEW` builds a side-by-side set.
|
|
107
|
+
- Programmatic use: import `bobframes.schemas`, `bobframes.discovery`, and `bobframes.paths` to drive
|
|
108
|
+
table lookups, drop discovery, and path resolution from your own scripts.
|
|
109
|
+
- `bobframes/probes/whatif.py` is a manual qrenderdoc-side probe and is not wired as a CLI command.
|
|
110
|
+
- A TOML config file, an externalized draw classifier, and design-token theming are planned for v0.2.
|
|
111
|
+
|
|
112
|
+
## License
|
|
113
|
+
|
|
114
|
+
MIT. See [LICENSE](LICENSE).
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.1.0"
|
|
@@ -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))
|