Queries and investigations
A recording already has the facts. Queries pull out the part you need. Investigations turn a useful line of inquiry into something you can run again and share.
Try one small project
We will make a game with one changing value, record eight frames, then ask what
happened. Create these three files in an empty HealthGame directory.
The manifest is wax.json.
{"name":"Game","version":"0.1.0"}
The application source is Src/App.wax.
static int32 health = 100;
api fn Main() : int32 {
health = health - 10;
return health;
}
api fn GetHealth() : int32 {
return health;
}
The investigation source is Queries/Health.wax. Query source lives in
Queries/, which Wax leaves out of normal application builds. You can write
the analysis after you make a recording. The queries come first in this file.
The investigation below them uses the same HealthState query.
import Frame, FrameRange from Wax::Recording;
import GetHealth from Game;
struct HealthRow {
int32 health;
bool critical;
}
struct ReviewArgs {
int64 frame;
int64 from;
int64 span;
}
struct HealthReport {
int64 frame;
int32? health;
int32 observedFrames;
}
query fn HealthState() : HealthRow {
int32 health = GetHealth();
return new HealthRow {
.health = health,
.critical = health <= 40
};
}
query fn HealthLow() : bool {
return GetHealth() <= 40;
}
investigation HealthReview("Review health in a recorded range") {
/** Review one frame in the context of a recorded range. */
fn Review(ReviewArgs args) : HealthReport {
// query.at replays one requested frame. Each named lane is optional
// because replay may not be able to reach that frame.
var at = query.at(new Frame(args.frame)) {
.row = HealthState()
};
// query.scan replays every available frame in the requested range and
// collects each query's typed results under its named lane.
var scan = query.scan(FrameRange.StartingAt(new Frame(args.from), args.span)) {
.rows = HealthState()
};
int32? health = null;
if (at.row) |row| {
health = row.health;
}
return new HealthReport {
.frame = args.frame,
.health = health,
.observedFrames = scan.rows.PresentFrames().size
};
}
case "Opening" => Review(new ReviewArgs { .frame = 0L, .from = 0L, .span = 8L });
}
Bind waxdbg to the project and make a short recording.
cd HealthGame
wax debug init
waxdbg recording create run.wxs --frames 8
recording create starts a fresh run of the project bound by waxdbg init.
This project has no configured runtime, so waxdbg calls Main once for each
requested frame and writes the recorded execution to run.wxs. It does not
take a snapshot for every frame. The successful command also selects run.wxs
as this session's active recording, so the analysis commands below do not need
to repeat its path. Pass a path explicitly when analyzing some other recording,
or use waxdbg recording select existing.wxs to select an existing one without
moving execution. waxdbg seek existing.wxs selects it and starts a replay.
This command is useful for tests, simulations, command line workloads, and small reproductions that can run on their own. Here it gives us a recording that anyone can recreate without setting up a host. The queries and investigations below work the same way on a recording captured from a full application with real input.
For a debugger run driven by channels, use waxdbg recording input-schema to
discover the project’s typed channel envelope, then pass a schedule to
recording create --inputs inputs.json. The Inspector guide
shows the explicit frameInputs format.
The application begins at 100 health. Main removes 10 during each frame, so
frame 0 has health 90 and frame 7 has health 20.
See what queries are available
Ask for the catalog before choosing a query.
waxdbg recording queries
The catalog names both queries and describes their return types. Here is the complete output for this project, formatted across a few lines.
{
"protocol": "wxdbg.recording-queries.v1",
"queryRoots": [
{
"ownerKind": "application",
"ownerIdentity": "Game",
"ownerVersion": null,
"name": "Game.HealthLow",
"signature": "() : bool",
"args": null,
"returns": { "kind": "bool" },
"source": { "path": "Queries/Health.wax", "line": 29, "column": 10 }
},
{
"ownerKind": "application",
"ownerIdentity": "Game",
"ownerVersion": null,
"name": "Game.HealthState",
"signature": "() : Game.HealthRow",
"args": null,
"returns": {
"kind": "struct",
"name": "Game.HealthRow",
"fields": [
{ "name": "health", "type": { "kind": "int32" } },
{ "name": "critical", "type": { "kind": "bool" } }
]
},
"source": { "path": "Queries/Health.wax", "line": 21, "column": 10 }
}
]
}
Game.HealthState is a catalog name. Game comes from wax.json, and
HealthState is the query fn name. Commands that select one query use this
catalog name exactly.
Read one frame
Run HealthState at frame 4.
waxdbg recording query --frame 4 --name Game.HealthState
The command returns a wxdbg.query.v1 document with the selected queries and
frame results. For this one-frame, one-root request,
result.frames[0].values[0].value has the shape declared by HealthRow:
{"health":50,"critical":false}
The adjacent present flag distinguishes an absent nullable result from a
present value. Omit --frame to evaluate the whole recording, or use
--range A:B for inclusive bounds.
A query gives one typed answer each time it runs. It can take no argument, one
json value, or one typed struct. Its return type tells the Inspector and other
tools how to read the answer.
Run the investigation
An investigation gathers related analysis under one name. Its public functions are choices you can run. A case keeps familiar arguments in the source.
Recordings from apps with host fn imports work here too: replay workers use
host results from the tape. The investigation itself is hosted by waxdbg and
cannot call the app's host imports. Compilation rejects any such import
reachable from its selected member, including through helpers or delegates;
query.scan and query.at keep worker execution separate.
List the available investigations, then run Review. It reads frame 4 and
scans all eight frames around it.
waxdbg investigation list
waxdbg investigation run --name Game.HealthReview --member Review \
--args '{"frame":4,"from":0,"span":8}'
For reusable automation, put that same JSON value in a file and pass
--args-file review.json. Inline and file arguments use the same published
type schema and diagnostics, and cannot be supplied together.
The result wraps the declared HealthReport value with completeness details.
{"protocol":"wxdbg.investigation-run.v1","value":{"frame":"4","health":50,"observedFrames":8},"complete":true,"scans":[]}
query.at asks replay for one frame. Its row field is optional because the
requested frame may not be available. The investigation keeps that fact in the
nullable HealthReport.health field. If frame 4 cannot be read, the same result
shape carries "health":null.
query.scan asks replay for every available frame in a range. Its rows lane
collects the typed HealthRow result from each frame. PresentFrames() then
counts the frames where that lane returned a value, which gives this report its
observedFrames field.
The outer complete value says whether every query.at and query.scan call
finished. The scans array describes any known shortfall. Here, complete is
true and the array is empty because frame 4 and the full scan range were
available.
You can also run the arguments saved by the Opening case.
waxdbg investigation run --name Game.HealthReview --case Opening
Search and scan the recording
recording find runs a query that returns bool and reports matching frames.
Ask for every frame where health is low.
waxdbg recording find --name Game.HealthLow --range 0:7 --all
{"protocol":"wxdbg.find.v1","count":3,"coverage":{"requested":{"from":0,"to":7},"evaluated":{"from":0,"to":7},"framesEvaluated":8,"complete":true},"result":{"kind":"matches","matches":[{"frame":5},{"frame":6},{"frame":7}]}}
recording query --columns collects row fields across a range. Select a root
with --name, or repeat it to evaluate several roots in one shared replay.
Leaving out --name works when the library has exactly one root; otherwise
the command lists the candidates.
waxdbg recording query --name Game.HealthState --range 0:7 --columns --text
frame health critical
0 90 false
1 80 false
2 70 false
3 60 false
4 50 false
5 40 true
6 30 true
7 20 true
For one root, columns use its returned field names, such as health. In a
batch, columns are prefixed with each root's fully qualified name, such as
Game.HealthState.health. File names do not determine column names.
To request a reduction instead of the full column table:
waxdbg recording query --name Game.HealthState --range 0:7 --reduce health:min
The projection reports a minimum of 20 across eight present values.
Handle command errors
An unavailable frame produces an error document on standard output and exits with status 1. For this eight-frame recording:
waxdbg recording query --frame 99 --name Game.HealthState
{"protocol":"wxdbg.error.v1","error":{"code":"frame_out_of_range","message":"range [99, 99] is outside recording window [0, 7]"}}
Typed investigation arguments are validated before execution. A string cannot
stand in for the integer frame field; this command also exits with status 1:
waxdbg investigation run --name Game.HealthReview --member Review \
--args '{"frame":"wrong","from":0,"span":8}'
{"protocol":"wxdbg.error.v1","error":{"code":"run_failed","message":"investigation invocation failed: Wax.QueryArgError: argument `frame`: expected an integer, but a string was given"}}
Check the exit status before consuming a result. Human-readable diagnostics may also appear on standard error; keep that stream separate when parsing JSON.
Grow the query library
Typed argument structs can contain arrays, enums, vectors, nullable fields, and
nested structs. StableRef<C> carries an object identity using the @<hex> ID
shown by the Inspector. Wax checks every argument before the query runs and
names a field or array item when a value has the wrong type.
A nullable row leaves a gap in a scan. A List<Row> query can return several
entries at one frame. Investigations can use query.scan with several query
lanes to recover events, find meaningful windows, or turn a long recording into
a small typed report.
The recording created in this walkthrough contains the application source and
type information needed for replay, but no query source. The bound Queries/
directory is therefore its default library. An Inspector export can additionally
embed its current investigation sources so the exported recording remains
portable. Embedded sources replace the disk library at the same logical path;
--file and --query-dir replace either. --no-query-library excludes only the
library on disk, not an embedded snapshot or sources you name explicitly.
By default, a query batch runs inside one isolation bracket per frame. Roots
run in the requested order, so later roots can see earlier roots' mutations in
that frame. Temporary allocations and mutations are rolled back after the
batch; they do not reach the next frame. --retain-mutations explicitly keeps
those changes across frames and requires one worker. The JSON column table
reports whether isolation used an undo journal, a snapshot, or both.
Open this result in the Inspector
Open the recording on HealthReview, then choose the Opening case.
waxdbg investigation open run.wxs --name Game.HealthReview
The waxdbg guide covers live debugging, recordings, and the Inspector. The agent integration guide explains how an agent can join the same Inspector and work with the same evidence.