Documentation
ViewAlyzer CLI (headless)
viewalyzer-cli is the command-line front of ViewAlyzer: everything the GUI can do with a target or a recording, as one process per job with JSON on stdout. The GUI binary forwards viewalyzer --headless ... and any command verb to the same code, so hosts can spawn either binary.
host / script / agent --argv--> viewalyzer-cli <command> [flags] --> JSON on stdout
|
v
<recording>.vadb (SQLite; also queryable directly)
Two spellings reach the same commands. The verb form is the primary one; the --headless flag form is contract for existing hosts (the Python SDK, BKPT Studio, CI harnesses) and every entry below keeps working:
| VERB FORM | --HEADLESS FLAG FORM |
|---|---|
viewalyzer-cli capture --config b.vacf --output run.vadb --duration 10 | viewalyzer --headless --config b.vacf --output run.vadb --duration 10 |
viewalyzer-cli probes | viewalyzer --headless --list-probes |
viewalyzer-cli snapshot --config b.vacf --output crash.vadb | viewalyzer --headless --snapshot --config b.vacf --output crash.vadb |
viewalyzer-cli query timeline --recording <id> --tier summary | viewalyzer --headless --query timeline --recording <id> --tier summary |
viewalyzer-cli map summary --recording run.vadb | viewalyzer --headless map summary --recording run.vadb |
viewalyzer-cli replay run.vadb --perf | viewalyzer --headless --replay run.vadb --perf |
viewalyzer-cli version | viewalyzer --headless --version |
viewalyzer-cli recordings | viewalyzer --headless --list-recordings |
viewalyzer-cli memory --elf f [--map f] | viewalyzer --headless --analyze-memory --elf f [--map f] |
viewalyzer-cli symbols --elf f | viewalyzer --headless --list-symbols --elf f |
viewalyzer-cli poll ... | viewalyzer --headless --record-polls ... |
viewalyzer-cli license [get|activate <key>|validate|deactivate|install <file>] | viewalyzer --headless --get-license / --activate-license KEY / --validate-license / --deactivate-license / --install-license FILE (see docs/LICENSING.md) |
RTOS analysis queries
query task-states, query resources, query queue-failures, and query semaphores expose recorded scheduler and IPC evidence. Use --task-id N or --resource-id N to select a recording actor, --t-start-us/--t-end-us to select a time window, and --tier raw for state, priority, resource or semaphore wait history. queue-failures always lists individual failures. Missing evidence stays unknown; incomplete waits do not feed duration statistics.
query slice-details adds the selected slice's RTOS context. query timers identifies measured, estimated and unknown callback durations, including missing traced exits. Existing commands, schema version 2 and replay metric names remain compatible. See RTOS states and IPC evidence for fields, scope rules, limits and examples, and query schemas for machine-readable shapes.
Runtime map database for agents and CI
map derives a queryable runtime graph from a saved .vadb, using the same Rust projector as the GUI's architecture map. It preserves observed task/object connections, per-operation evidence, device timestamps, activity fields and task state history. Zephyr, FreeRTOS and bare-metal canonical events share this path. No GUI, probe connection, recorder changes or .vamap export are needed.
Keep and share the **.vadb**. The map database is a disposable SQLite cache; queries create it on demand and reuse it on subsequent calls. The source recording is never modified or registered by these commands. The GUI continues to use its in-memory projection; this cache is the headless/SDK access path, not a second recording that users must manage or open alongside the .vadb.
viewalyzer-cli map build --recording run.vadb
viewalyzer-cli map summary --recording run.vadb
viewalyzer-cli map nodes --recording run.vadb --name mutex --kind mutex
viewalyzer-cli map edges --recording run.vadb --tags contended,failed
viewalyzer-cli map activity --recording run.vadb --node actor:12 --tags contended --t-start-us 100000 --t-end-us 200000 --limit 100
viewalyzer-cli map states --recording run.vadb --node actor:7
viewalyzer-cli map sql --recording run.vadb --sql "SELECT tag, SUM(count) AS operations FROM map_edges GROUP BY tag"
viewalyzer-cli map schema
Use the exact node ID returned by map nodes; IDs belong to that recording. --recording accepts an indexed recording ID or a .vadb path. A positional path after the action also works. The default action is summary. Convert byte logs to .vadb with load --output first. map schema returns the cache DDL without loading a recording. This command family uses the verb map; the existing memory --map flag still means a firmware linker map.
| ACTION | FILTERS / RESULT |
|---|---|
build, summary | Ensure the cache exists; return provenance and whole-recording counts. |
nodes | --node exact ID, --name substring (ASCII case-insensitive), --kind exact recorded/projected kind; returns identity, actor ID, name, kind and whole-recording operation count. |
edges | --node either endpoint, --tags, time window; returns connections with operation, tag, attribution, window-scoped count and first/last timestamps. |
activity | Same filters as edges; returns individual observations, source event ID, timestamp, endpoints, operation, tag, attribution, summary and decoded fields. |
states | --node, time window; returns observed state transitions, not a sampled or inferred state timeline. |
sql | One read-only SELECT or WITH query; returns columns and rows. Put SQL predicates and OFFSET in the statement. |
--tags is a comma-separated OR list: failed, contended, timeout, waiting, armed, expired, stopped, begin, end, sample, other. Omit it for all observed traffic. Node, tags and time predicates combine with AND. Filters that do not apply to an action are rejected rather than silently ignored. Node kinds come from the recording/projector (for example task, mutex, queue, isr); they are not limited to a particular RTOS's object names.
Time: CLI window bounds --t-start-us / --t-end-us are inclusive integer microseconds. Database timestamps and JSON time_ns, first_time_ns, last_time_ns, range_ns and state-history times are device nanoseconds on the recording's time axis. The envelope explicitly reports "time_unit":"ns". Counts and the envelope's summary always describe the complete map, even when the returned rows use a time or tag filter.
List responses contain data, row_count, limit, offset, truncated and next_offset. The default limit is 200, maximum 10,000; follow next_offset while truncated is true. Ordering is deterministic: nodes by ID, edges by decreasing filtered count then ID, activity/states by time then ID. SQL returns arrays under rows with column names under columns, plus row_count, limit, truncated. SQL callers paginate using their own stable ORDER BY / OFFSET.
Each result also includes:
{
"schema_version": 3,
"command": "map",
"query": "activity",
"time_unit": "ns",
"cache": {
"path": "/tmp/viewalyzer-runtime-map/<hash>.sqlite",
"reused": true,
"format": "viewalyzer.runtime-map.sqlite",
"version": 1,
"source": {
"path": "/captures/run.vadb",
"sha256": "<recording content SHA-256>",
"size_bytes": 123456,
"engine_version": "1.3.0",
"cache_version": 1
}
},
"summary": { "nodes": 8, "edges": 12, "activities": 1000 }
}
The example abbreviates summary. It also reports operations, node_states, range_ns, source_events, recorded os, unknown_context, integrity_markers, unsupported_operations, skipped_legacy_duplicates, history_dropped, topology_omitted, state_history_dropped, state_history_start_ns and a semantics explanation. Check these before drawing completeness conclusions. Unsupported events remain in the source .vadb; this map is a projection of supported operations, not a replacement for the full event database. The saved map uses the entire recording, without live-history or canvas display limits.
SQLite tables and useful queries
map schema is the authoritative DDL. Cache schema version 1 provides:
| TABLE | COLUMNS |
|---|---|
map_meta | key, value (text; format, cache version, time unit, source JSON, summary JSON). |
map_nodes | id (text PK), actor_id, name, kind, operations. Synthetic context nodes can have no actor ID. |
map_edges | id (integer PK), from_node, to_node, operation, tag, attribution, count, first_event_id, first_time_ns, last_event_id, last_time_ns. Counts here cover the full recording. |
map_activity | id (integer PK), edge_id, event_id, time_ns, kind, summary, fields_json. |
map_node_states | id (integer PK), node_id, activity, label, event_id, time_ns, kind. |
Edges reference node IDs; activity references edge IDs; states reference node IDs. Indices cover node kind, edge endpoints/tag, activity time and edge/time, and state node/time. Activity/state row IDs are local projection IDs; event_id points back to the canonical source event. One source event can produce more than one projection row. attribution is explicit, scheduler_context, or unknown.
For example, pass this as --sql to identify the observed contention connections:
SELECT src.name AS source, dst.name AS destination, e.operation,
e.attribution, e.count
FROM map_edges e
JOIN map_nodes src ON src.id = e.from_node
JOIN map_nodes dst ON dst.id = e.to_node
WHERE e.tag = 'contended'
ORDER BY e.count DESC, e.id
For activity in a selected interval, aggregate evidence rows, not capture-wide edge totals:
SELECT e.id, e.operation, COUNT(*) AS observed_operations
FROM map_activity a JOIN map_edges e ON e.id = a.edge_id
WHERE a.time_ns BETWEEN 100000000 AND 200000000
GROUP BY e.id ORDER BY observed_operations DESC, e.id
To inspect a task at a cursor, select its last observed state at or before that device timestamp. No row means no prior state evidence. Preserve an explicit unknown state following a gap instead of carrying an older running state across it:
SELECT * FROM map_node_states
WHERE node_id = 'actor:7' AND time_ns <= 200000000
ORDER BY time_ns DESC, id DESC LIMIT 1
Relations describe observed operations. They do not establish successful delivery, ownership, end-to-end causality, missed deadlines or static code dependencies. Keep attribution and integrity evidence with any derived answer.
Cache lifecycle and query limits
The default directory is the OS temporary directory's viewalyzer-runtime-map subdirectory. --cache-dir <directory> on any recording-based action overrides it. Cache identity includes the canonical source path, full-file SHA-256, byte size, engine version and map cache version. Changing the source or version makes a new cache; deleting a cache simply causes regeneration. Old entries are disposable and can be removed when readers finish. The CLI does not delete them on exit; an ephemeral CI directory or the SDK context below gives deterministic cleanup. Treat the cache as read-only and archive the .vadb instead.
Builds publish complete SQLite files atomically; concurrent jobs do not share a partially populated file. Every CLI query validates the source identity before reusing its cache. The first build loads the recording and projects its full history in memory, so build cost scales with capture size. Existing caches avoid re-projecting history, though hashing still reads the source file. A nonempty SQLite WAL is rejected as recording_busy; finalize/close its writer first. A recording changed during generation/lookup reports recording_changed.
CLI SQL runs with read-only SQLite/query-only enforcement. Only one SELECT or WITH statement is accepted; embedded semicolons (including in literals) are not supported. Queries have a five-second SQLite execution budget, a 4 MiB serialized-row budget and an 8 MiB SQLite value limit, in addition to the row cap. truncated reports row/byte truncation; a single oversized row gives response_too_large, timeout gives query_timeout, and invalid/write SQL gives bad_sql. Narrow the selection, page, or select fewer fields. Missing source, invalid arguments, malformed recording and cache I/O failures use no_such_recording, bad_arguments, bad_recording, map_cache_error or map_database_error as appropriate. Direct read-only SQLite clients are not subject to CLI query limits.
The map schema/projection version is separate from CLI envelope version 2 and the source .vadb format. Engine changes that alter cached projection semantics or tables must bump va_store::runtime_map::database::CACHE_VERSION.
Python SDK
With a CLI build supporting map, viewalyzer-sdk exposes the same operations:
from viewalyzer_sdk import ViewAlyzer
va = ViewAlyzer()
with va.temporary_map("run.vadb", timeout_s=120) as graph:
print(graph.summary()["summary"])
failures = graph.edges(tags=["contended", "failed"])
print(failures["data"])
with graph.connect() as db: # read-only, closed on exit
print(db.execute("SELECT COUNT(*) FROM map_activity").fetchone())
# Private cache directory removed here, including on exceptions.
va.runtime_map(path, cache_dir=...) or recording.runtime_map() uses a reusable cache instead. Handles expose nodes, edges, activity, states, sql, summary, path and info. Each CLI-backed call revalidates the cache; a direct connect() connection reads a fixed cache snapshot. Close connections before leaving temporary_map, particularly on Windows. See the SDK README's runtime map section for pagination and CI examples.
Output conventions
| CHANNEL | CONTENTS |
|---|---|
| stdout | query / list commands: exactly one JSON object. capture / snapshot: human progress lines plus the machine-parseable contracts below, then one JSON envelope as the last line. |
| stderr | --stream JSON lines during a capture (see below). |
| exit code | 0 success; 1 failure. parse-trace-domain additionally uses 2 for unreadable files or invalid arguments; see its report contract below. |
Every JSON payload carries "schema_version": 3 (2 since the hardware-trace queries and stream events were added; additive), except the bare hwtrace --dry-run image. Errors use one envelope:
{ "schema_version": 3, "error": "window_too_wide",
"message": "634 events in the window exceed the 200 row cap for budget low",
"suggestion": { "t_end_us": 31545, "or": "filter with --kinds, or raise --budget" },
"limits": { "max_rows": 200, "would_return_rows": 634 } }
Error codes: bad_arguments, bad_config, no_such_recording, bad_recording, window_too_wide, bad_sql, symbol_not_found, map_not_found, no_hw_trace, etm_not_present, empty_capture, capture_failed, snapshot_failed, empty_snapshot, reset_failed, cooldown_active (carries retry_after_s), license_file_rejected, internal.
Capture and snapshot print two stable lines hosts parse:
[headless] Recording saved: C:\...\run.vadb (2508 KB)
[headless] Recording registered: id=fc6de594af4d
[headless] ERROR: ... precedes any failure (exit 1). [headless] State: ..., [headless] t=... events ... and [capture] ... lines are progress, not contract.
Connecting to a target
Connection settings are flat kebab-case keys, either flags (--key value) or a JSON config file (.vacf) with the same names; flags win over the file. Unknown keys are errors; comment / _* keys are notes.
{
"transport": "stlink-rambuf", // stlink-swo | stlink-rambuf | stlink-rtt | jlink-swo | jlink-rtt | jlink-rambuf | udp | serial | swo-tcp
"stlink-serial": "0033004B3033510735393935",
"target-device": "STM32G474RE", // probe-rs target name
"speed-khz": 4000,
"rambuf-scan-start": 536870912, // 0x20000000
"rambuf-scan-size": 131072,
"no-reset": false // default: reset after attach, capture from boot
}
| TRANSPORT | KEYS THAT MATTER |
|---|---|
stlink-rambuf / jlink-rambuf | --rambuf-address (skips the scan) or --rambuf-scan-start/size, --rambuf-poll-ms, --no-reset |
stlink-rtt / jlink-rtt | --rtt-channel, --rtt-address (pins _SEGGER_RTT), --no-reset |
stlink-swo / jlink-swo | --cpu-clock-hz (required; or --trace-clock-hz), --swo-freq-hz (2 MHz is the safe J-Link value), --itm-port, --init-swo |
udp | --udp-ip, --udp-port, --cobs |
serial | --serial-port, --baud, --cobs (ports lists the names) |
swo-tcp | --swo-tcp-port (a simulator or debug server's raw-SWO side channel on 127.0.0.1, --swo-tcp-host to override), optional --cpu-clock-hz, --itm-port, --swo-freq-hz; recorder streams take the authoritative clock from their CLK: setup. Pass the clock explicitly for accurate hardware-only ITM/DWT timestamps |
All debug-probe transports take --target-device, --speed-khz, --stlink-serial / --jlink-serial. help prints every key with its meaning; config --config b.vacf [flags] echoes the effective configuration without touching hardware.
For J-Link SWO, --jlink-engine auto|segger-dll|probe-rs selects the capture engine on Windows, Linux and macOS. The default auto prefers the installed SEGGER library and logs a fallback to probe-rs if acquisition fails. The probe-rs J-Link SWO buffer is small and can lose bytes under load. Use --jlink-engine segger-dll --jlink-serial SERIAL to require the SEGGER engine and an exact probe serial; a selection or library error then fails capture instead of falling back. probe-rs explicitly bypasses the SEGGER engine.
--jlink PATH selects an installed SEGGER directory or the library file for that direct SWO engine. An explicit path is authoritative for library discovery; omit it to search installed locations. This setting does not select an external server executable, and does not change RTT or RAM-buffer capture. Legacy server/tool keys such as arm-gdb, jlink-gdb-port and stlink-gdb-port remain accepted and reported as "no effect"; these capture paths do not spawn debug servers.
Hardware trace (ITM / DWT)
Hardware trace (ITM console, DWT PC sampling and data watches, exception trace, SWO load) is set up and started through this CLI, with the flags and config keys below; hosts such as BKPT Debug drive it this way. The desktop app's GUI does not offer controls to start a hardware-trace capture.
SWO transports carry the full DWT stream; rambuf and RTT transports sample the PC by polling DWT_PCSR over the debug port (--dwt --dwt-pc N; on RTT pass --cpu-clock-hz so the cadence matches). The flat keys are the C++-era spelling and still work; the nested hardware-trace block is the full Eclipse-level set:
"hardware-trace": {
"itm": { "ports": 3, "privilege": 0, "timestamps": 1 }, // ITM_TER mask (default: recorder port | port 0), ITM_TPR, TSPrescale 0|1|4|16|64
"dwt": { "enable": true, "exception-trace": true, "pc-sample-cyc": 16384,
"counters": ["cpi","exc","sleep","lsu","fold","cyc"],
"watch": [ { "addr": "0x20000410", "size": 4, "function": "data-rw", "pc": true, "name": "g_temp" } ] },
"trace-port": { "swo-hz": 2000000, "protocol": "nrz" }
}
| KEY / FLAG | MEANING |
|---|---|
--dwt, --dwt-exc, --dwt-pc N | DWT on, exception trace, PC sampling every N cycles (rounded onto the 64..16384 POSTCNT grid; the applied value lands in pc_sample_interval_cycles) |
--dwt-watch spec[,spec] | data-watch comparators (up to 4, positional): [name@]0xADDR[:size][:data-rw|data-r|data-w|pc|address][:pc]; default data-w, size 4; :pc adds the PC of the access |
--dwt-counters cpi,exc,sleep,lsu,fold,cyc | event counters (cyc is refused while PC sampling is on: both use POSTCNT) |
--itm-ports, --itm-privilege, --itm-timestamps off|1|4|16|64 | stimulus port mask (bit n = port n; the recorder port and port 0 by default, so a firmware printf on port 0 keeps flowing), privilege mask, timestamp prescaler |
--dwt-path auto|poll|swo | poll forces DWT_PCSR polling on an SWO session (SWO-borne sources off) |
--hardware-trace <json | @file> | the whole block at once |
Every write goes through one register recipe shared with bkpt_gdbserver; what the core lacks is refused with the reason in the capture log ([hwtrace] ... not enabled: ...) and in the recording meta hardware_trace (refused[]), never silently. config echoes the block.
Probe selection: probes lists connected probes with serials. With one probe of the kind attached the serial may be omitted; with several, pass the serial or the capture refuses (no guessing). targets --filter STM32G474 lists probe-rs target names.
capture
viewalyzer-cli capture --config b.vacf --output run.vadb --duration 10 [--stream] [--stop-file f] [--keep-va] [--no-register]
--output: a.vadb(default:<app dir>/recordings/capture-<stamp>.vadb; the extension is adjusted and logged if something else is given), or a.vato keep only the byte-log.--duration: seconds of recording, counted from the moment the transport reportsRecording; omit it to run until Ctrl-C / SIGTERM or--stop-file(the file's existence ends the capture; content is ignored). Early stop printsStop requested at N s. Finalizing partial recording...and then the two contract lines: a partial recording is a normal recording.- A capture that ends with 0 events is an error (
empty_capture, exit 1) and leaves no file behind; look at the[capture]lines for the reason. - On
swo-tcp, an orderly server EOF ends and finalizes the capture immediately, even when--durationhas not elapsed. A missing listener reports onlyno SWO server at <address>; the source may be any simulator or debug server. --streamemits JSON lines on stderr while running, one event per line, its kind int:stream_initonce,stream_meta({"id":42,"name":"Sine Wave","display":"graph"}) as channels are discovered, thenstream_sample({"id":42,"t_us":1234,"value":98.0}) per point.idis the firmware's trace id.itm_text{port, t_us, text}: text the firmware wrote on an ITM stimulus port (SWO transports), as it arrives.swo_load: received byte rate and share of the configured SWO byte budget, with overflow, corruption, framing and timestamp-retention counters.pc_samples{total, sleep, pcs}: the on-chip PC samples since the previous line. Explicit DWT sleep packets contribute tosleep; PCSR zero addresses do not. Qualification and exclusion counts accompanypcs.dwt_data{rows: [{cmp, t_us, v, size, w, pc}]}: DWT data-trace packets, one row per packet with the raw comparator valuev(cmpis the comparator,wtrue on a write,pcthe traced PC when the watch asked for it; the consumer types the value).exc{total, max_depth, exceptions: [{num, name, enter, exit, return, max_depth}]}: the exception-trace table, cumulative over the capture, sent at most every 200 ms. Exact depths are null.framing_unverified_eventsandcounts_notequalify the aggregate and per-vector packet counts.
- The recorder byte-log (
.va) is written during capture and removed after the.vadbis written unless--keep-va. It does not preserve native hardware observations, so a host crash can still lose those observations. --instrument-cmd "<cmd>"spawns an external instrument recorder (a power analyzer, a DAQ) for the capture window;--instrument-warmup-swaits before touching the target,--instrument-series <f>imports the instrument's series file after the capture, and--instrument-livemerges a stdout stream live. The sync recipe and the NDJSON series contract are indocs/EXTERNAL-SERIES.md.- The final stdout line is a JSON envelope:
{"schema_version":3,"recording_id":..., "path":..., "summary":{events, objects, bytes, lost_events, corrupt_bytes, duration_us, wall_s, os, cpu_hz, transport}}. - Free mode (no license): the capture is capped at 5 s, a 5 s cooldown follows (
cooldown_activewithretry_after_sif you come back too soon), and the recording keeps its first 10 task/ISR lanes. Each cap prints a[headless]line naming it;licenseshows the caps in force. Seedocs/LICENSING.md.
snapshot
viewalyzer-cli snapshot --config b.vacf --output crash.vadb
Reads the firmware's RAM ring through the probe without resetting the target (the core is halted for the dump and resumed). Needs a rambuf transport. Both ring shapes are recognised: the post-mortem ring (exact window, ring: "post-mortem", discarded_packets, frozen, wrapped) and the live drop-mode ring (single-shot dump, ring: "live-drop"). An empty ring or a window that parses to 0 events exits 1 (empty_snapshot / snapshot_failed). The envelope:
{ "schema_version": 3, "recording_id": "a82ee8857447", "path": "...\\crash.vadb",
"summary": { "ring": "live-drop", "events": 1895, "window_bytes": 16380, "packets": null,
"discarded_packets": 0, "wrapped": false, "frozen": false,
"wire_version": 1, "recorder_version": 256, "cpu_hz": 170000000,
"control_block": "0x2000E338", "lost_events": 0, "corrupt_bytes": 0 } }
reset --config b.vacf resets the target behind the probe (halt, reset, run) without capturing.
--elf <firmware.elf> on capture / snapshot / poll pins the control block from the image instead of scanning RAM (_VA_RAMBUF for rambuf transports, _SEGGER_RTT for RTT; logged as Control block at 0x... (resolved from ELF symbol ...)). Default RAM metadata capture requires the matching firmware ELF. Add --no-reset to attach to a running target. See RAM metadata attachment.
poll (memory polling, no firmware instrumentation)
viewalyzer-cli poll --config b.vacf --elf firmware.elf --symbols tick_counter,adc_value:u16 --poll-hz 100 --duration-s 10 [--stream] [--output p.vadb]
viewalyzer-cli symbols --elf firmware.elf [--filter motor] # the pollable symbol_legend
Samples the listed variables over the probe at a fixed rate. Each --symbols entry is name[:type], type one of u8|u16|u32|i8|i16|i32|f32 (default: the symbol's size, signed). Unknown symbols or types error out (symbol_not_found / bad_arguments) instead of polling the rest. Samples are poll_trace rows on a 1 MHz wall clock (meta.poll_clk_hz), served by query user-traces like any channel; --stream emits the same stream_init / stream_meta / stream_sample lines on stderr. --coalesce-gap <bytes> merges registers into ranged reads across up to N undeclared bytes (0 = only exactly-adjacent registers merge; passing more asserts the gap bytes are side-effect-free to read). The envelope: {"summary": {"span_seconds", "sample_count", "sample_loss_percent", "symbols_polled", "poll_hz"}}.
load, convert, export
viewalyzer-cli load run.vadb # summary JSON (events, lanes, channels, load time)
viewalyzer-cli load run.va --output run.vadb # convert a byte-log or raw wire dump into a .vadb
viewalyzer-cli load run.vadb --export events --format csv --out events.csv
viewalyzer-cli load run.vadb --export spans|tasks|channels|traces [--format json|csv] [--out file|-]
load accepts .vadb, .va byte-logs and raw wire dumps (no container header); all decode to the same recording. Any recording can be named by path or by the 12-hex recording_id from the index.
import <recording> --series <file.ndjson> merges an external instrument's series into an existing .vadb after the fact; the sync flags (--sync-name, --sync-tol-ms, --offset-s, ...) and the NDJSON contract are in docs/EXTERNAL-SERIES.md.
replay
viewalyzer-cli replay run.vadb [--perf] # flag form: viewalyzer --headless --replay run.vadb --perf
Re-parses a recording and prints one [metric] category,name,value,unit line per metric: the machine-parseable surface CI harnesses parse, same categories and names as the C++ app's --replay: info (fileSize KB, cpuFrequency, os), data (taskSlices, userTraces, syncEvents, timeSpan, corruptBytes, corruptRuns, seqSupported, lostEvents, seqGaps, priorityInversions, commFlows, cpuAnomalies), timer/work lanes, sync category counters, per-channel trace and per-lane task.slices / task.cpu rows, flow summaries, and result,status. --perf adds perf,<stage>,<ms>,ms timings for the same pipeline stages the GUI runs on open (parse, task stats, inversions, comm flows, CPU stats). Exit 0 when the recording carries any task slices or user traces, 1 otherwise. The C++ app's per-operation sync rows (give/take/…) are not emitted.
query
viewalyzer-cli query <verb> --recording <id|path> [--tier summary|bucketed|raw] [--budget low|med|high]
[--t-start-us N --t-end-us N] [--bucket-us N] [--kinds a,b] [--channels a,b]
All times in responses are microseconds since the recording start. Tasks are referenced by a compact code (T1, T2, ...) defined in the response's task_legend; data channels by C1, C2, ... in trace_legend. The draft 2020-12 schema catalog has a named schema for every verb at #/$defs/<verb>.
--tier summary(default) needs no window;bucketedneeds the window and--bucket-us;rawneeds the window.--budgetscales the caps (rows 200 / 1000 / 5000, buckets 64 / 256 / 1024, serialized size 12 / 48 / 200 KB). Exceeding a cap returnswindow_too_widewith asuggestion(a narrowert_end_us, a wider bucket, or a filter).
| VERB | SUMMARY | BUCKETED | RAW | |
|---|---|---|---|---|
summary | whole-recording scalars plus execution_ended_abnormally (boolean), ended_abnormally (fault/stack-trap time, task, and last event; otherwise null), execution_end (last event/task), recording integrity, and separate pre_sync acquisition counters | |||
timeline | per-lane cards: cpu_percent, slice_count, mean/p50/p95/p99/max slice, preemptions, observed period p50/p95, jitter, anomalies[] | per-lane CPU% per bucket | slices: [[task_code, t_start_us, dur_us], ...] | |
events | counts by_kind, top_objects (--kinds filters) | kind counts per bucket | individual events `{t_us, kind, start_end, code | obj, value_i, value_f, text}` |
user-traces | per channel {count, min, max, mean, last, rms, std_dev, rate_hz, min_at_us, max_at_us} (--channels by name or code) | per channel `[{min,max,mean,count} | null]` per bucket | samples: [[channel_code, t_us, value], ...] |
sql | --sql "SELECT ...": read-only, one statement, row cap by budget, BLOBs as "<blob N bytes>" | |||
inversions | scope: "mutexes_only"; every mutex wait includes waiter/holder names, effective priorities, holder base priority, comparison basis and observed times. type is inversion, contention or undetermined; compare waiter effective priority to holder base, with OS-aware ordering. If base is unknown, outranking holder effective still proves inversion. Confirmed and undetermined counts are separate. Semaphore ownership is absent from the wire format, so semaphore inversion is not inferred | |||
verdicts | unified verdicts[] from native CPU/fatal-end detectors and optional Trace Domain rules; detectors distinguishes missing descriptors, disabled descriptors, no rules, ready detectors, and true zero findings | |||
cpu | top-level CPU model fields (no cpu wrapper): busy_pct/idle_pct, sliding-window extremes, context_switches, preemptions, tasks[], and findings[]; --t-start-us/--t-end-us scope everything (window.scoped) | |||
timers | per kernel timer: id, measured, fires (explicit observed expiries), predicted_fires (nominal schedule arithmetic, excluded from statistics), arms/stops, signed mean/p99/max_offset_us, over_threshold vs --threshold-us (500), 12-bin histogram with hist_min_us/hist_max_us, cb_mean_us (measured elapsed callback time, null without both boundaries); work[] k_work request lanes (submits/schedules/cancels) | fire_records[] (+observation over threshold), marks[] | ||
comms | paths[] (producer>via>consumer, kind, count, rate, median/p99/max latency, blocked) and resources[] (sends/receives/matched/still_pending/empty_receives/contentions/peak_pending); a window recomputes path stats, --bucket-us adds per-resource backlog | |||
series | compact curves use fields + values: [[t_us,value]]; --kind stack also exposes object rows as stack: [{t_us,total_stack_used_bytes}]. Other kinds: cpu-load, event-rate, heap, task-timing, interval | |||
slices, slice-details, events-all, user-traces-all | unbounded variants for consumers that manage their own payload size (--max-slices n, --kinds, --channels, optional window) |
Timer arms counts schedule changes processed by the kernel, including internal catch-up reloads on FreeRTOS through 10.4.3. It is not an API-call count; use accepted_requests for explicit accepted caller requests (FreeRTOS and RTX5). A fresh observed FreeRTOS arm restores schedule association after catch-up. RTX callbacks can remain queued across restart/stop, so their association may remain unknown. due_unobserved means a nominal due time passed without an observed expiry; it does not prove a missed deadline. Timer offsets include unknown tick phase and any pre-arm hook delay.
Mutex timings use observed recorder hooks on a single core: successful acquisition to final release, and contention to successful acquisition. They do not measure scheduling delay before an acquisition hook runs. A timeout ends that attempt; a later retry is not its completion. Lost events end pairing, and unknown endpoints remain null.
FreeRTOS's shared queue hooks report only the first take and final give of a recursive mutex. Current Zephyr and RTX5 recorders use explicit actor packets for ownership transitions and failed operations. Older Zephyr recordings require re-recording for mutex ownership timing; the host explains this through limitations and the GUI. sends/receives count recorded release/acquire events; matched counts measured holds. A mutex has no queue backlog, so its requested backlog is null with a reason.
For all supported adapters, mutex start_end=false means acquisition and true means release. This differs from execution spans, where true means entry. The normalized event kind determines the flag's meaning. Use a host that supports the explicit MUTEX_ACTOR packet (the RTX5-capable decoder) with the current Zephyr recorder.
summary.transport_dropped_records retains increases in transport drop counters after the first observed counter baseline; transport_drop_counter_total is the last reported counter value. These can include losses before the first decoded event and can overlap sequence-detected lost_events. Do not add the counts. A null value means no counter report was retained, rather than proof of zero transport loss. transport_drop_counter_discontinuities counts observed decreases (reset or wrap); in that case the reported increases are only a lower bound. The same fields are retained in va_summary and the final capture JSON. Overview and Details display reported transport drops separately.
Hardware-trace queries (no_hw_trace when the recording has none of the rows):
| VERB | PAYLOAD (DATA) |
|---|---|
profile [--elf f] | PC-sample hotspots: total_samples, sleep_samples, span_s, sample_rate_hz, hotspots[], source (dwt-swo | pcsr-poll), interval_cycles |
itm-console [--port N] | ports[] of {port, lines: [{t_us, text}], bytes} (lines split on newline, an unterminated tail is partial, lines_truncated when the cap cut a port); --port N (0..31) keeps only that port, an empty ports[] when it has no text; out of range is bad_arguments |
dwt-data | watches[] of {cmp, name, address, function, count, first: [{t_us, value, rw, pc?}]} (up to 256 samples each) |
dwt-exc | exceptions[] of {num, name, enter, exit, return, max_depth} plus events[] {t_us, num, func} (up to 4096) |
dwt-counters | counters.{cpi,exc,sleep,lsu,fold,cyc} = {wraps, cycles, rate_per_s}, span_s, cpu_load_pct (null; wrap notifications alone do not measure CPU load) |
swo-load | {bytes, seconds, bytes_per_s, swo_hz, share_pct, overflows} |
Not available (they error naming the reason): irq (no_hw_trace), etm (etm_not_present), target-caps.
hwtrace (dry run)
viewalyzer-cli hwtrace --dry-run --arch v7m|v8m|v6m --caps '{"numcomp":4,"notrcpkt":0,"nocyccnt":0,"noprfcnt":0,"itm":1,"tpiu":1}'
--cpu-clock-hz N --swo-freq-hz N --itm-port P [--no-init-swo] --hardware-trace <json|@file> [flat --dwt* flags]
Prints the ordered register image the capture would program on that core: {schema, arch, cpu_hz, swo_hz, writes: [{reg, addr, value}], refused: [{feature, reason}], applied}. Exit 2 with {"error": ...} on a configuration error. bkpt_gdbserver --dry-run prints the same image for the same inputs.
Fingerprint baselines: query fingerprint / query compare
A fingerprint is a .vafp.json file containing selected measurements, tolerances, recording metadata (provenance) and compatibility rules. A baseline is the fingerprint used as the reference. An envelope is one fingerprint learned from several runs; each metric stores its mean and observed min/max range. The same file works in the desktop Fingerprint view, CLI and Python SDK.
# One known-good saved recording.
viewalyzer-cli query fingerprint --recording good.vadb --out app.vafp.json
# Three independent saved recordings become one envelope file.
viewalyzer-cli query fingerprint --recording good1.vadb --runs good2.vadb,good3.vadb --out envelope.vafp.json
# Gate a candidate using the saved rules and strict measurement checks.
viewalyzer-cli query compare --recording candidate.vadb --baseline envelope.vafp.json --ci
--runs reads existing recordings; it does not schedule or start captures. Use representative runs of the same workload. Each metric is weighted by its own observed run count; missing measurements are never filled with zero. Units, exact values, tolerances, selections and required provenance must be compatible before runs can be merged. Ignored differences remain recorded.
Metric selection and compatibility are independent
Metric selection asks: what behavior should be judged? For example, select summary,traces,health to leave per-task measurements out of the baseline:
viewalyzer-cli query fingerprint --recording good.vadb --sections summary,traces,health --out app.vafp.json
Compatibility asks: were the recordings made with matching settings? For example, after reviewing an intentional clock change, permit that field to differ:
viewalyzer-cli query fingerprint --recording good.vadb --ignore-provenance cpu_hz --out clock-reviewed.vafp.json
Ignoring trace_categories does not exclude task metrics or disable task tracing. It only stops that configuration difference making the runs incomparable. Conversely, removing task metrics does not relax compatibility rules, and Summary metrics such as total task count remain if that section is selected. Neither control changes what firmware records. Ignoring configuration differences or excluding health metrics cannot bypass the strict integrity checks.
| FLAG | APPLIES TO | MEANING |
|---|---|---|
--runs a,b | fingerprint | Extra recordings learned into the envelope |
--sections summary,tasks,traces,timers,comms,health | fingerprint | Sections to include; default is all |
--tolerance-pct n | fingerprint | Default FAIL threshold, 25 percent |
--warn-pct n | fingerprint | Default WARN threshold, 80 percent of the fail threshold |
--require-provenance field,... | fingerprint | Require known, matching values for these fields |
--ignore-provenance field,... | fingerprint | Deliberately permit differences or unknown values for these fields |
--out f.vafp.json | fingerprint | Write the fingerprint as readable JSON |
--baseline f | compare | Reference fingerprint/envelope, or a recording fingerprinted on the fly |
--ci | compare | Require explicit compatibility policy and usable measurement evidence |
Supported provenance fields: os, cpu_hz, capture_source, recorder_version, trace_categories, build_flags. Defaults require RTOS, timestamp clock and capture source; categories and build flags are required when known in the baseline. Recorder version defaults to Ignore. cpu_hz is the recorder timestamp clock, which need not be the core clock. These fields do not prove board identity, workload identity or every RTOS/build configuration setting; validate those in the harness when they are not represented in the recording.
Rules are saved in the baseline and applied during comparison. There are no comparison-time flags to bypass those rules. In the GUI, Save Fingerprint... saves current measurements, whereas Save baseline copy... preserves the loaded baseline's measurements with edited rules.
File formats and individual options
Current fingerprints use format: "vafp" and metric_semantics: 2. Whole-field policies use file schema_version: 2. Individual category/build-flag exceptions use file schema_version: 3 and compatibility.ignored_bits. The GUI offers named per-option switches; save that baseline and use it unchanged with query compare or Python assert_matches(). CLI authoring flags select whole fields, not individual bits. See the policy format for reviewed JSON editing and mixed envelope rules.
The CLI response envelope's schema_version: 3 is separate from the fingerprint file version. With --out, fingerprint_schema_version identifies the written file's version. Older readers reject policies they cannot interpret.
Schema-1 fingerprints with current metric semantics remain readable in normal comparison mode, where provenance differences are warnings. Strict --ci rejects legacy files without explicit policy. Rebuild from usable recordings and review the rules; changing a version number does not create missing measurement evidence. Older metric semantics also require a rebuilt baseline.
Read the decision and preserve the report
| VERDICT | CLI EXIT | MEANING |
|---|---|---|
pass | 0 | Selected measurements and required settings match |
warn | 0 | Ordinary tolerance/advisory warnings; the CI gate accepts these |
fail | 2 | A metric regressed or a required baseline measurement/item is missing |
incomparable | 3 | Required provenance differs/is unknown, or strict mode lacks an explicit policy |
invalid | 4 | Strict measurement checks found unusable baseline or candidate evidence |
| Command/input error | 1 | Invalid arguments, unreadable input or another command failure |
Required provenance mismatches fail the comparison even without --ci for format-2/3 baselines. Strict mode additionally rejects empty captures/windows, loss/corruption, reported transport drops, recorder/timebase errors, unknown integrity counters, empty metric selections and selected unmeasured/lower-bound values. Compatibility takes precedence over measurement validity, then metric regression; the report keeps all findings.
Do not accept every verdict except fail: that would let incomparable and invalid through. Use the exit status, or explicitly accept only pass and warn. To save stdout while keeping the failing status in Bash:
set -o pipefail
viewalyzer-cli query compare --recording candidate.vadb --baseline app.vafp.json --ci | tee compare.json
metric_verdict and counts describe the metric results independently of compatibility. compatibility.fields explains required and ignored settings; measurement_issues explains invalid evidence. Metric rows use current for the candidate and baseline for the reference, with min/max for its envelope. Readable baseline_display/current_display and named bit rows accompany raw provenance values. An incomparable run can still show individual PASS/FAIL rows; those do not override the overall verdict.
A metric passes inside its min/max envelope or warn band, warns up to its fail band, and fails beyond it. Exact metrics must match. Missing required baseline items fail; newly observed items warn. Advisory measurements do not fail a run on their own. Unknown/lower-bound values do not train numeric envelopes. Selected ambiguous object identities fail comparison because they cannot be matched reliably across runs. See Regression tests in CI for the metric list, selection file fields and a complete job example, and the Fingerprint view for GUI authoring.
The recording index
recordings lists {recording_id, path, schema_name, duration_us, size_bytes, created_utc} for every registered file that still exists. The id is a stable hash of (absolute path, mtime). --delete-recording <id> removes the entry and the file; --delete-all-recordings empties the index. The index lives at <app dir>/recordings/index.json (%APPDATA%\ViewAlyzer-GPUI, ~/Library/Application Support/ViewAlyzer-GPUI, ~/.config/ViewAlyzer-GPUI). Hosts that manage their own files can pass paths and --no-register.
Inside a .vadb
One SQLite file per recording, the schema the GUI and the Python SDK read: meta (key/value: format, version, capture_source, capture_utc, va_cpu_hz, va_os, ...), va_objects (id, ns, va_id, name, extra), va_events (id, t_cycles, kind, start_end, obj, obj2, value_i, value_f, text; t_cycles in CPU cycles, seconds = t_cycles / va_cpu_hz; text is payload text and does not duplicate an object's name—join va_events.obj to va_objects.id), health (stage='recorder': corrupt_bytes, corrupt_runs, lost_events, seq_gaps, pre_sync_corrupt_bytes, pre_sync_corrupt_runs, pre_sync_lost_events, pre_sync_seq_gaps, main_trace_bytes), raw_log (the byte-log, section='va_file'), va_summary, va_task_stats, va_trace_stats, va_timer_stats, va_work_stats, va_comm_paths, va_comm_resources, and va_signal_blocks (full-rate waveforms, present only when a file has any).
VADB version 2 stores native hardware observations in va_hw_observations. Each JSON observation preserves its kind, payload, native clock and any verified recording-time bounds. Uncorrelated observations do not extend the recorder timeline or contribute to RTOS CPU calculations. Use query hardware, query native-signals and the hardware-specific queries instead of querying old hardware event kinds. Meta keys include hardware_trace (the applied image, found registers and swo_load), pc_sample_source, pc_sample_interval_cycles, va_itmhw_bytes, va_itmhw_overflows.
Utilities
| COMMAND | PURPOSE |
|---|---|
version | {schema_version, app, version, core: "rust", edition, license: {type, tier, licensed, partner?, expires?}, transports[]} |
license [get|activate <key>|validate|deactivate|install <file>] | local license state, effective caps and cooldown_remaining_s (get, never online); the lifecycle verbs are the only online calls; install verifies and copies an OEM file. docs/LICENSING.md has the envelopes |
probes | {probes: [{type, serial, description, vid, pid}]} |
ports | {ports: ["COM7", ...]} |
targets [--filter s] | probe-rs target names |
symbols --elf f [--filter s] | data symbols of a firmware image (symbol_legend) |
callgraph --elf f [--root fn] [--depth n] [--direction out|in|both] [--entries] | static call graph decoded from the image's code (bl and tail b.w, no toolchain needed): nodes[] with BFS layout, edges[], decode stats; --entries lists entry points (main, reset, vector-table handlers) |
memory --elf f [--map f] | flash/RAM usage by section; with the linker MAP: region capacities (memory_regions[] with used/free/percent), MAP-placed sections[], and discarded_sections[] (map_not_found if the file is missing) |
reset | reset the target behind the configured probe |
doctor | probes per kind, serial ports, recordings dir, target registry, license, caps_in_effect, caps_source (free, license, or none), and effective_caps (also repeated on the license check row); always exits 0 |
config | effective connection config for the given --config / flags |
help | flag reference |
Validate a custom Trace Domain
viewalyzer-cli parse-trace-domain my-domain.vadomain --pretty
viewalyzer --headless --parse-trace-domain my-domain.vadomain --pretty
viewalyzer-cli parse-trace-domain --schema
This offline check uses the same parser as the applications. No SDK, probe or target connection is required. Pass one or more filenames; JSON output includes schema_version, domain_api, valid, and a files array with per-file results. Errors name the precise path, JSON Pointer pointer and message; syntax errors include line and column. Exit status is 0 for valid files, 1 for rejected content, or 2 for unreadable files or invalid arguments. Batch validation checks every file; status 2 takes precedence over 1. The --schema form takes no files and returns the structural schema in the response's schema property. Complete validation also checks semantic constraints and references.
See Creating custom Trace Domains for the full format, steps, examples and the optional Python SDK wrapper.