A fast, native Chrome trace viewer built with C++ and Dear ImGui. Also runs in the browser via WebAssembly.
- Timeline — Zoomable/pannable timeline with colored slices per process/thread, sub-pixel culling, and collapsible process sections
- Flame graph — Per-thread icicle chart with zoom/breadcrumb navigation, range scoping, search highlighting, and selection sync
- Counter tracks — Auto-scaled step-function line charts for counter events with hover tooltips and sub-pixel point merging
- Flow arrows — Bezier curves connecting flow events across threads (toggleable)
- Detail panel — Tabbed inspector with Call Stack (collapsible descendant tree with wall/self/child time), Children breakdown (aggregated count, total, avg, min, max with heat-colored bars), and Arguments view
- Range selection — Click+drag on ruler or Shift+drag in tracks to select a time range with aggregated statistics
- Instance browser — List and navigate all instances of a selected function
- Source viewer — Jump to source code for traced functions with syntax highlighting, selectable/copyable text, and configurable path remapping for CI/cross-platform builds
- Search — Case-insensitive search by event name or category with sortable results table, count/average columns, and unique-by-name deduplication
- SQL queries — Query trace events with SQLite, visual query builder with aggregate functions, schema inspector, multiple saved query tabs, and sortable results with async execution
- Statistics — Per-function aggregated timing with count, total, avg, min, max; CSV/TSV export
- Filtering — Toggle visibility of processes, threads, and categories via tree checkboxes
- Go to time — Jump to a specific timestamp (G key) supporting ns/us/ms/s units
- Configurable keyboard shortcuts — Rebindable primary and alternate bindings for all actions via Settings dialog
- Keyboard shortcuts — WASD call-stack navigation, arrow keys for pan/scroll, F to fit, G to go to time, Escape to deselect
- Diagnostics — Live render stats, FPS sparkline, memory usage history, sub-pixel culling metrics
- Loading progress — Four-phase progress display (reading file, parsing JSON, building index, building query DB)
- Native file dialog — OS file picker via SDL3, plus drag & drop support
- Resizable label gutter — Drag the splitter to resize thread labels
- Memory counter — Real-time RSS memory usage displayed in toolbar and as a counter track
- Themes — Dark and light themes with configurable font scale, track height, and selection border color
- Self-profiling — Emit an internal performance trace of the viewer itself with
--trace
Chrome JSON Trace Event Format — both array ([...]) and object ({"traceEvents": [...]}) formats.
Supports event phases: X (complete), B/E (duration begin/end), i (instant), C (counter), s/t/f (flow), M (metadata), b/e/n (async), N/O/D (object), P (sample), R (mark).
P records are point observations. A top-level stackFrames object maps string IDs to frames with name, optional category, and optional parent. An event's sf identifies the leaf. Integer IDs also work. stackFrames may appear before or after traceEvents.
{
"traceEvents": [
{"ph": "P", "name": "cpu", "ts": 100, "pid": 7, "tid": 11,
"sf": "leaf", "weight": 250, "weightUnit": "us"}
],
"stackFrames": {
"root": {"name": "main", "category": "cpu"},
"leaf": {"name": "work", "category": "cpu", "parent": "root"}
}
}Each P record counts as one sample. TraceRender keeps the raw weight; it does not assume an unqualified weight measures time. weightUnit is a TraceRender extension for converters that supply CPU-time weights. It accepts ns, us, ms, or s and normalizes estimates to microseconds. This unit is independent of the timestamp -ns option. Missing, negative, or unsupported time weights leave estimated CPU time unavailable. Zero is a valid explicit time weight. The interval to the next sample never becomes execution time, and dur on a P record is ignored. The Chrome format defines samples as zero-duration observations and supports sf ancestry. Chrome trace format
Missing frame IDs, invalid frame definitions, missing ancestors, and cycles leave the entire stack unresolved. The observation, timestamp, process/thread identity, raw reference, and weight remain available under the event's supplied name. Valid samples use the leaf frame's name for navigation. Duplicate frame IDs use the last definition. Inline stack program-counter arrays and the separate top-level samples format are not imported by this implementation.
Converted B/E or X records with the category token sampleEvent are sampled spans. Their supplied intervals are estimated sampled CPU time, and their original sample count is unavailable. In mixed B/E streams, mark both transitions with sampleEvent so measured and sampled pairs stay independent. This matches PerfView's Chromium exporter. Unmarked duration records retain measured-event semantics. Equal timestamps and intervals retain input order; same-name recursive frames remain distinct.
The flame graph creates separate measured, sample, and sampled-span trees per thread. Sample-tree widths represent sample counts, with inclusive/exclusive counts and available CPU estimates in tooltips. Time-weight coverage is shown when some samples lack estimates. Range selection includes observations in [start, end) and clips span intervals. Per-function range counts include a recursive function once per observation; flame trees preserve each recursive stack position. Search averages use only measured events. Details, instances, and timeline tooltips label estimates explicitly.
SQL exposes kind, sample_count, sample_weight, weight_unit, estimated_cpu_time, estimated_self_cpu_time, and the raw sf. dur and self_time are NULL for sampled records. Original sample counts are NULL for converted spans. CPU estimates for raw samples belong to the observed leaf; the flame and range views calculate inclusive ancestry. Group queries by kind to keep observations and converted spans separate. Existing duration queries continue to select measured events.
TraceRender opens .diagsession files directly on Linux through File > Open, drag and drop, or a command-line path. Import runs locally with progress and cancellation. File > Save Profile creates a versioned .trprofile that preserves CPU observations, resolved names and source locations, allocation histories, managed GC observations and snapshots, and capture quality. Reopening needs neither the original capture nor symbol files. See the desktop import workflow and the version 1 format.
Native CPU and allocation frames can share local PDB resolution through LLVM 18.1. Matching PDB GUID/age is required; missing symbols keep module offsets. Saved resolutions reopen without PDBs. Native flame and range views group by function identity, so unrelated functions with the same display name remain separate.
The committed fixtures in tests/fixtures/ use invented functions and timestamps:
sampled_stacks.jsoncontainsmain -> workat 100 us with 250 us weight andmainat 10,000 us with 500 us weight on thread 11. The root has two inclusive samples, one exclusive sample, and 750 us estimated CPU. Thread 12 has a separate unweighted observation. The gaps contribute no CPU time. Tests import both field orders.perfview_spans.jsoncontains a recursive outer frame from 0 to 10 us and two inner spans from 0 to 5 and 5 to 10 us. The outer estimate is 10 us inclusive and zero self; no original sample counts are inferred.
Supported .diagsession producers and recording modes are listed in the fixture corpus.
Requires CMake 3.22+, a C++17 compiler, and OpenGL development headers.
# Install dependencies (Ubuntu/Debian)
sudo apt install build-essential cmake libgl-dev
# Build
cmake -B build -DCMAKE_BUILD_TYPE=Release
cmake --build build -j$(nproc)All other dependencies (SDL3, Dear ImGui, nlohmann/json, SQLite3) are fetched automatically via CMake FetchContent.
Requires Emscripten.
./scripts/build_wasm.sh# Open with file dialog
./build/trace_render
# Open a trace file directly
./build/trace_render trace.json
# Interpret timestamps as nanoseconds (default is microseconds)
./build/trace_render -ns trace.json
# Explicitly interpret timestamps as microseconds
./build/trace_render -us trace.json
# Self-profiling: emit an internal trace of the viewer itself
./build/trace_render --trace output.json trace.jsonYou can also drag & drop a trace file onto the window.
The Memory tab uses recorded native or managed allocation histories from .diagsession and .trprofile files. It defaults to the capture end. Set T in microseconds or use a selected event's time, then select a function to inspect its allocation paths. The birth filter selects allocations born at or after its start and before its end that remain outstanding at T. You can copy the timeline range into that filter or clear it to include earlier allocations.
Known bytes and allocation counts include intentional retention. Records with an unknown start or end, sampled allocation events, and capture gaps are shown separately as uncertain. These numbers describe recorded allocations, not process RAM or total allocation traffic. Inclusive function rows overlap because each call path contributes to several functions; the summary counts each allocation once.
Managed GC checkpoints bound observed survival and absence; they do not supply exact release times. The checked Desktop CLR 4 capture supports survival analysis. CoreCLR allocation stacks remain available, but survival is unknown because the checked .NET 8 capture contradicts its retained-object checks. See managed allocation coverage.
Select an allocation path to see its frames. View source opens saved source locations in the existing Source tab, including in profiles without CPU events. Missing symbols remain visible as addresses, and source path remapping works as it does for trace events. Captures with several memory capabilities offer a view selector. Managed heap snapshots show recorded type counts and sizes; they cannot identify allocating functions or allocation birth times.
| Action | Input |
|---|---|
| Zoom in/out | Mouse wheel / =/- keys |
| Pan horizontally | Middle-click drag / Ctrl+left drag / Left/Right arrows |
| Scroll vertically | Shift+mouse wheel / Up/Down arrows |
| Navigate to parent | W |
| Navigate to first child | S |
| Navigate to prev sibling | A |
| Navigate to next sibling | D |
| Select event | Left click |
| Select time range | Click+drag on ruler / Shift+drag in tracks |
| Fit to selection/range | F |
| Fit entire trace | F (with nothing selected) |
| Go to time | G |
| Clear selection | Escape |
| Open file | Ctrl+O |
| Search | Ctrl+F |
| Settings | Ctrl+, |
| Run SQL query | Ctrl+Enter |
All keyboard shortcuts can be customized in Settings > Keyboard.
src/
main.cpp Entry point: SDL3/OpenGL init, main loop
app.h / app.cpp App shell: dockspace, panel orchestration, settings
tracing.h Self-profiling tracer (Chrome JSON output)
model/
trace_event.h TraceEvent, StackFrame, Phase and EventKind
trace_model.h / .cpp Events, string pool, processes/threads, indexes
block_index.h 256-event block spatial index for range queries
color_palette.h 48-color hash-based category coloring
query_db.h / .cpp SQLite database with async query execution
parser/
trace_parser.h / .cpp SAX streaming JSON parser (handles 100MB+ files)
platform/
platform.h Platform interface (GL, file dialogs, main loop)
platform_desktop.cpp SDL3/OpenGL 3.3 desktop implementation
platform_wasm.cpp Emscripten/WebGL2 implementation
file_loader.h Async file loading interface
file_loader_desktop.cpp Threaded background loader
file_loader_wasm.cpp Synchronous loader
memory.h Cross-platform RSS memory query
ui/
view_state.h Viewport, selection, filters, layout state
timeline_view.h / .cpp Time ruler, tracks, event rendering, zoom/pan
detail_panel.h / .cpp Call stack, children, arguments inspector
search_panel.h / .cpp Text search with sortable results
filter_panel.h / .cpp Process/thread/category visibility toggles
stats_panel.h / .cpp SQL editor, query builder, result tables
flame_graph_panel.h / .cpp Per-thread icicle charts
instance_panel.h / .cpp Function instance lister with navigation
source_panel.h / .cpp Source code viewer with path remapping
diagnostics_panel.h / .cpp FPS/memory sparklines, render stats
counter_track.h / .cpp Step-function counter visualization
flow_renderer.h / .cpp Bezier flow arrows across threads
toolbar.h / .cpp Menu bar: file open, zoom, settings
range_stats.h / .cpp Aggregated statistics for time ranges
key_bindings.h / .cpp Configurable keyboard shortcut system
format_time.h Auto-scaling time display (ns/us/ms/s)
export_utils.h CSV/TSV export with RFC 4180 quoting
string_utils.h Case-insensitive search
sort_utils.h ImGui table sort comparators
- SAX parser (not DOM) — Streams events without building a JSON tree, handles large traces (100MB+)
- String-interned events — Names and categories stored once in a pool, events reference by index
- Block-based spatial index — Fast visible-range queries with O(log N + K) performance using 256-event blocks with monotonic max_end_ts propagation
- Pre-computed derived data — Parent indices, self times, nesting depths, and category sets computed once during
build_index(), not per-frame - ImDrawList rendering — Direct draw commands for thousands of slices at 60fps, bypassing ImGui widget overhead
- Sub-pixel culling — Slices narrower than 1px merged into thin lines per-depth to prevent overdraw
- Lazy JSON parsing — Event arguments stored as raw JSON strings, deserialized on demand one event at a time
- Async SQL execution — Non-blocking query execution with progress tracking on a background thread
- Platform abstraction — Desktop (SDL3/OpenGL 3.3) and WebAssembly (Emscripten/WebGL2) share the same codebase via compile-time platform switching
- Flat node pools — Flame graph uses index-based trees (no pointers) for cache-friendly traversal and safe rebuilds
All fetched automatically via CMake FetchContent:
| Dependency | Version | Purpose |
|---|---|---|
| Dear ImGui | docking branch | UI framework |
| SDL3 | 3.2.8 | Windowing, input, OpenGL context |
| nlohmann/json | 3.11.3 | SAX JSON parsing |
| SQLite | 3.45.1 | SQL query engine |
| Google Test | 1.15.2 | Unit testing (desktop only) |
./scripts/run_tests.shUses Google Test covering: parser, trace model, spatial index, time formatting, viewport state, search, timeline hit testing, trace events, counter tracks, source path remapping, SQL queries, CSV/TSV export, self-profiling tracer, flame graph, sampled profiles and mixed metrics, and panel reset lifecycle.
# Generate a synthetic trace for stress testing
python3 scripts/gen_trace.py > trace.jsonMIT


