AI-explored tools for the Source Engine profiling system.

nosoop 4f5b96be2f Add frame timeline zoom and pan to web viewer 1 hari lalu
server-patch 53f2c6e2e4 Initial commit 1 Minggu lalu
.gitignore 53f2c6e2e4 Initial commit 1 Minggu lalu
README.md 4f5b96be2f Add frame timeline zoom and pan to web viewer 1 hari lalu
VPROF_FORMAT.md 53f2c6e2e4 Initial commit 1 Minggu lalu
fix-vprof-timings.patch 53f2c6e2e4 Initial commit 1 Minggu lalu
index.html 4f5b96be2f Add frame timeline zoom and pan to web viewer 1 hari lalu
vprof_to_json.py 53f2c6e2e4 Initial commit 1 Minggu lalu

README.md

[!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.

vprof-viewer

Tools for reading Source Engine VProf profiling captures (.vprof) and turning them into JSON.

  • VPROF_FORMAT.md — reverse-engineered binary format documentation (with references to the leaked engine source).
  • vprof_to_json.py — standalone Python 3 parser / converter.
  • 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) that can be zoomed (mouse wheel, +/− buttons or Zoom to selection) and panned (shift+drag or shift+wheel) to inspect individual frames;
  • 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, 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 <input type=file> and drag & drop continue to work (within the session) when the API is unavailable.

Python usage

# 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

{
  "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.