> [!IMPORTANT] > > Human-written foreword from the repository owner: > > It's all sloppa beyond this point. Just wanted something better than the jank of > `sm prof dump vprof`-ing and capturing the text report. > > [A copy of the web viewer is available here.](https://csrd.science/misc/vprof/) # vprof-viewer Tools for reading Source Engine **VProf** profiling captures (`.vprof`) and turning them into JSON. * [`VPROF_FORMAT.md`](VPROF_FORMAT.md) — reverse-engineered binary format documentation (with references to the leaked engine source). * [`vprof_to_json.py`](vprof_to_json.py) — standalone Python 3 parser / converter. * [`index.html`](index.html) — self-contained browser viewer (no build step, no network access required). ## Requirements For the Python tool: Python 3.8+ (uses `dataclasses` and `statistics.fmean`). No third-party packages. The browser viewer needs only a modern browser. ## Web viewer Open `index.html` directly from disk or serve it as a static file. Choose (or drag & drop) a `.vprof` capture and the page parses it locally in a Web Worker — nothing is uploaded. It shows: * recording metadata and a warning when a capture contains no timing data; * a per-frame timeline (time, or call counts when times are all zero); * a sortable, filterable, collapsible call tree with total/self/avg/max time, call counts and percentages; * budget-group and top-node breakdowns; and * a draggable frame range so statistics can be scoped to part of a capture. ### Large captures and frame ranges The first parse records a tiny index: the byte offset of every frame plus the frame in which each tree node first appeared. Nothing per-sample is kept in memory for large files. When a frame range is selected, the viewer asks the worker to re-parse **only that byte span** from the source file and aggregates it, so scoping works on arbitrarily large recordings without a memory blow-up. (The tree order at any frame is the final order filtered by each node's add-frame, because VProf only ever prepends children — so no per-frame tree snapshots are needed.) When the browser supports the [File System Access API](https://developer.mozilla.org/en-US/docs/Web/API/File_System_Access_API), the source file handle is remembered in IndexedDB and a *Reopen last recording* button is offered next time, so on-demand range reads work without re-picking the file. Plain `` and drag & drop continue to work (within the session) when the API is unavailable. ## Python usage ```bash # Print JSON to stdout (compact) python vprof_to_json.py capture.vprof # Write pretty JSON to a file python vprof_to_json.py capture.vprof -o capture.json --indent 2 # Metadata, budget groups and the summary only (small, fast) python vprof_to_json.py capture.vprof --summary-only --indent 2 # Only the first 100 frames python vprof_to_json.py capture.vprof --max-frames 100 -o first100.json # Drop the per-frame samples or the node table python vprof_to_json.py capture.vprof --no-frames python vprof_to_json.py capture.vprof --no-nodes ``` Full options are available via `python vprof_to_json.py --help`. ## Output schema ```jsonc { "format": "source-vprof", "source": { "file": "capture.vprof", "size_bytes": 1310867, "version": 1, "root_node_id": 0 }, // All budget groups, including the 31 built-in defaults (is_default=true). "budget_groups": [ { "id": 0, "name": "Unaccounted", "flags": 6, "flags_decoded": ["SERVER", "OTHER"], "is_default": true } ], // Node table. `children` gives the final tree order (child IDs). "nodes": [ { "id": 100, "name": "CEngine::Frame", "parent_id": 0, "budget_group_id": 0, "budget_group": "Unaccounted", "children": [] } ], // One entry per recorded frame. "frames": [ { "index": 0, "tick": 54863, "total_microseconds": 0, // time of the root node "samples": { // node id -> timing for this frame "0": { "calls": 1, "microseconds": 0 }, "100": { "calls": 0, "microseconds": 0 } }, "added_nodes": [100], // nodes first seen in this frame "added_budget_groups": [31] // groups first seen in this frame } ], // Aggregate statistics. "summary": { "frame_count": 1303, "node_count": 352, "budget_group_count": 38, "tick_first": 54863, "tick_last": 56165, "tick_span": 1302, "total_recorded_microseconds": 0, "frame_time_microseconds": { "min": 0, "max": 0, "mean": 0.0, "median": 0, "total": 0, "p95": 0 }, "top_nodes_by_total_microseconds": [ { "node_id": 0, "name": "Root", "budget_group": "Unaccounted", "total_calls": 1303, "total_microseconds": 0, "avg_microseconds": 0.0, "max_microseconds": 0, "percent_of_total": 0.0 } ] } } ``` ## Notes * Timings are **per-frame deltas**, not cumulative totals. Times are stored in multiples of 4 microseconds. * A node's `children` list is the *final* order after all frames. Because `AddNode` prepends children, the per-frame sample values are keyed by node ID rather than nested, so the tree can always be rebuilt from `nodes`. * Budget group IDs 0–30 are the fixed defaults registered by `CVProfile`; the parser seeds them so files that only record the groups added *after* construction still resolve correctly. * **Captures may legitimately contain zero timing values.** In the engine's snapshot path (`PreUpdateProfile` → `MarkFrame()` → `VProfRecord_Snapshot()`), `MarkFrame()` copies the current frame's counters to the *previous* frame and clears them before the recorder runs, so the writer reads cleared `m_CurFrameTime` / `m_nCurFrameCalls` values. Such captures still contain the full call tree and per-frame call counts. * The parser handles non-zero timings and the `255` (extended calls) and `0xFFFF` (extended time) escape codes. * Very large captures can produce very large JSON. Use `--summary-only`, `--no-frames`, `--max-frames`, or `-o` to manage output size and memory.