Guide
Profile with watch
Find which part of a program is slow, then show that a change made it faster. You’ll finish with two run identifiers and one query that compares them.
The route
- Start the program under watch. RepoQL launches it with its telemetry pointed at your local host.
- Confirm telemetry is arriving. Check what was captured before you read any timing.
- Find the slow part. Read the summary, then rank the operations.
- Compare runs by run identity. Run the same workload after the change and query both runs side by side.
The example is a small script, tools/wristband_audit.py. It loads 20 wristbands (the team’s word for sessions), then re-derives each token hash. The hashing takes a number of rounds from its first argument.
Start the program under watch
Pick a workload you can repeat exactly: the same input, the same arguments. Put the program after --. It runs in the directory you start rql watch from.
Terminal
rql watch -- python3 tools/wristband_audit.py
Output
[rql watch] run: 193c379ad862446bbfb843663abc96c5
[rql watch] pid: 42664
[rql watch] schema: watch
[rql watch] endpoint: http://127.0.0.1:59279/
[rql watch] telemetry_schema: watch
…
audited 20 wristbands at 200000 rounds
The command stays attached and prints the program’s own output. Keep the run value. Every later query uses it. (The database, lock and query-hint lines are trimmed here.)
Confirm telemetry is arriving
A timing can only describe work the program reported. So check what arrived first.
Terminal
rql query "SELECT status, capture, spans FROM watch.summary('193c379ad862446bbfb843663abc96c5')"
Output
status capture spans
exited 0 · last event 43s ago 2 payloads (metrics=2), 2472 bytes, 09:04:01-09:04:02 none
Payloads arrived, but there are no spans. The two metric payloads are RepoQL’s own samples of the process’s CPU and memory. It takes these for every run, instrumented or not. This script has no OpenTelemetry SDK, so the samples are all you get: a whole-process picture, with nothing about which phase was slow.
To see phases, the program has to report them. The script creates spans when the OpenTelemetry SDK is importable. Here it runs from a virtual environment with opentelemetry-sdk and opentelemetry-exporter-otlp-proto-http installed. The script sets no endpoint: watch supplies that through OTEL_* environment variables.
Terminal
rql watch -- ../otelvenv/bin/python tools/wristband_audit.py
Output
[rql watch] run: fa71b1067be84e81bf027d670ce86f57
…
audited 20 wristbands at 200000 rounds
Terminal
rql query "SELECT status, capture, spans FROM watch.summary('fa71b1067be84e81bf027d670ce86f57')"
Output
status capture spans
exited 0 · last event 37s ago 4 payloads (metrics=2, traces=2), 3538 bytes, 09:04:07-09:04:08 3 spans, 1 traces, 100.0% ok, slowest audit 1196.0ms, top audit 1196.0ms, rehash_tokens 990.6ms, load_wristbands 205.1ms, 09:04:07-09:04:08
Now capture shows traces=2 and spans is filled in. You can trust timings from this run.
Find the slow part
The summary already names the slowest spans. To rank every operation with counts and percentiles, use span_stats.
Terminal
rql query "SELECT name, count, p50_ms, total_ms FROM watch.span_stats('fa71b1067be84e81bf027d670ce86f57')"
Output
name count p50_ms total_ms
audit 1 1195.970048 1195.970048
rehash_tokens 1 990.571008 990.571008
load_wristbands 1 205.104896 205.104896
audit encloses the other two, so don’t add the rows together. Of its 1.2 seconds, hashing takes about 1 second and loading about 0.2. Hashing is where a change will pay off. To see how spans nest, pass a trace id to watch.trace_tree(). The full span_stats output includes one for each row.
Also compare count with total_ms. A cheap operation called thousands of times can cost more than one slow call.
Compare runs by run identity
Make the change, then run the same workload again. Here the change is fewer hashing rounds.
Terminal
rql watch -- ../otelvenv/bin/python tools/wristband_audit.py 50000
Output
[rql watch] run: 8da7b9d9c774497ebd11759ec25c8009
…
audited 20 wristbands at 50000 rounds
Every run shares the watch schema, and run_id keeps them apart. Name both runs in the query.
Terminal
rql query "SELECT name, run_id, round(duration_ms) AS ms FROM watch.otel_span WHERE run_id IN ('fa71b1067be84e81bf027d670ce86f57', '8da7b9d9c774497ebd11759ec25c8009') ORDER BY name, run_started_at"
Output
name run_id ms
audit fa71b1067be84e81bf027d670ce86f57 1196
audit 8da7b9d9c774497ebd11759ec25c8009 452
load_wristbands fa71b1067be84e81bf027d670ce86f57 205
load_wristbands 8da7b9d9c774497ebd11759ec25c8009 205
rehash_tokens fa71b1067be84e81bf027d670ce86f57 991
rehash_tokens 8da7b9d9c774497ebd11759ec25c8009 247
Hashing fell from 991 ms to 247 ms. Loading stayed at 205 ms, so the rest of the workload was comparable. Before you claim the gain, check that both runs did the same successful work. Both printed 20 wristbands and exited 0. Repeat the pair if machine load or a warm cache could explain the difference.
When it goes wrong
| Symptom | Likely cause | What to do |
|---|---|---|
spans is none, capture shows only metrics | The program has no OpenTelemetry SDK; the metrics are RepoQL’s process samples | Add the SDK and an OTLP exporter, or work from watch.available_metrics() |
| No payloads at all | The program doesn’t export OTLP, or overrides the OTEL_* variables | Query watch.payload; check the program’s exporter settings |
| Metrics missing for a very short program | It exited before one export interval | Let it run longer |
| Totals look too big | The query has no run_id filter, so other runs joined in | Pass the run id, or filter WHERE run_id = … |
A memory spike doesn’t show in mem peak | Memory is sampled once a second, for the process itself | Query repoql.watch.process.tree.memory.usage for the whole process tree |
| A run has disappeared | Completed runs older than six hours are cleaned up | Save the queries and numbers you need while the run exists |
What you’ve done
You ran a program under watch and checked that spans arrived before reading any timing. You found the phase that dominated, changed it, and compared the two runs by their identifiers. The summary’s capture column tells you whether a run has anything to profile.
Further reference
The watch tool page. In the installed manual: help:///tools/watch/watch.md, help:///tools/watch/watching-claude.md, help:///commands/watch.md.