Decode and presentation telemetry¶
The framework measures local receive-to-decode and receive-to-presentation latency, throughput, frame intervals, observed stops and known local discards. These measurements are observations. They do not classify a stream as passing or failing, reconnect a source, or change frame admission policy.
Timing boundaries¶
| Stage | Observation |
|---|---|
input |
First RTP packet of this frame observed at rtpbin's receive sink, before its jitter buffer |
access_unit |
Parser's compressed access unit reaches the configured pad |
decode_input |
Compressed buffer reaches the decoder sink pad |
decode_output |
Raw buffer reaches the decoder source pad |
preview_ready |
NVR's VPE preview processing completes |
draw |
NVR submits the image's GL drawing commands |
swap |
QOpenGLWindow emits frameSwapped for the paint containing this image |
The input clock is the receiver process's monotonic clock. This is a userspace RTP observation, not a NIC hardware timestamp, camera capture timestamp or wall clock. TCP framing and earlier transport processing are outside this boundary. RTP receipt before jitter buffering includes jitter-buffer residence in the receive-to-decode measurement. Parser reception and decoder input are recorded separately, so queueing before the decoder can be distinguished from its input- to-output path, which includes reordering and internal queues.
Decoder output means buffer delivery, not an independently synchronized measure
of physical GPU decode completion. swap means the Qt buffer-swap notification,
not monitor scanout or photon emission. No glFinish, VA surface sync, HIP sync,
or CPU pixel mapping is added for timing. Existing processing synchronization is
unchanged. A frame's first drawing and first observed swap are counted once;
painting the same image again does not increase the new-image FPS.
SDK and GStreamer use¶
amdva/telemetry.hpp exposes a Qt/GStreamer-independent Telemetry collector,
FrameTrace, TraceToken, stage availability and TelemetrySnapshot.
Telemetry::monotonic_ns() is the production clock. Applications manually mark
stages using this same clock, or inject a clock in deterministic tests. Never
subtract a PTS, RTP timestamp, NTP timestamp or foreign process clock from these
monotonic observations.
amdva/gst_telemetry.hpp adds PipelineTelemetry and a value-copying
FrameTraceMeta. Adapters own their pad probes and signal handlers. Attach them
before PLAYING; stop their pipelines before destroying the adapters. An adapter
retains the observed GStreamer objects until detachment. Frame metadata can
outlive both pipeline and adapter, retaining its collector without retaining
image buffers.
auto collector = std::make_shared<amdva::Telemetry>("camera-1");
collector->begin_generation(1);
amdva::PipelineTelemetry timing(collector);
timing.attach_rtsp(rtsp_source);
timing.attach_access_units(parser_src_pad);
timing.attach_decoder(va_decoder);
// Set PLAYING, periodically read collector->snapshot().json(), then set NULL.
For RTP pipelines without rtspsrc, attach_rtp(before_jitter, after_jitter)
provides equivalent correlation. attach_stage(pad, stage) marks an existing
trace at other processing boundaries. A renderer retrieves a TraceToken with
frame_trace(buffer) and calls mark(Draw) and then mark(Swap) at its actual
backend notifications. A sink probe alone cannot measure presentation.
For separate appsrc decode pipelines, copy buffer headers/metadata, rebase
PTS/DTS as needed and call the decode adapter's submit(buffer) before pushing.
The submitted PTS is associated with the original trace. The decoder sink probe
marks actual decoder entry. The source probe first uses the frame identity
preserved by the decoder, then falls back to unambiguous output PTS. It never
uses FIFO order or a nearest-PTS guess. This also handles decoders adjusting an
output PTS while retaining the exact frame metadata. Native NVR already integrates these operations. See the external
SDK telemetry example for a standalone H.264 or
H.265 RTSP pipeline; it requires the compatible VA/GStreamer runtime but no Qt.
The RTSP adapter expects one video track per RTSP source. For multitrack RTP pipelines, bind separate collectors/adapters to the selected track pads. Matching uses SSRC/RTP timestamps to relate first arrival to post-jitter PTS and then parser AU PTS. Reordering and timestamp/sequence wrap are supported within the retention window. Missing or ambiguous PTS, unsupported timestamp rewriting and unmatched outputs cannot produce an invented receive latency. They increase counters; unavailable distributions have zero samples and null durations. The local decoder-output count is still measured for unmatched outputs.
Correspondence tables are bounded to 4,096 entries and approximately 30 seconds;
expiry and capacity eviction are visible. Decoder submissions retired without
an observed matching output have their own counter: they are not proof of a
hardware decoder drop. Flush and replacement stream-start events retire timing
generations; explicit reconnects call begin_generation. Generation numbers
are monotonically advanced within a collector, even when a caller supplies an
older requested number. Old-generation stage notifications are rejected. Timing generations are
collector-local and may advance on decoder flush independently of the
application source generation; use timing tokens for correspondence.
Statistics and NVR integration¶
Each snapshot contains continuous and background groups. Input and AU
counts cover continuous reception for all sources, including hidden sources;
sparse background decode outputs are kept in the background group. Audio RTP
is excluded. A partially received frame still counts as input even if it never
produces an AU. The groups provide per-stage
counts, FPS, age, stop statistics and inter-frame interval distributions.
Latency distributions cover input→decode, decoder input→output, input→draw,
input→swap, decode→preview, preview→draw and draw→swap. Durations are milliseconds.
Counts last for the collector's lifetime; reconnects reset recent windows and
frame correlations. Removing and re-adding a source creates a new collector.
Recent distributions retain up to 4,096 samples in ten seconds and use the
nearest-rank P95/P99. samples, effective_window_ms and truncated describe
coverage. FPS is a rolling ten-second arrival count divided by the elapsed
active observation window. No sample is represented by a false zero latency.
Copies recover known ancestor timestamps from the collector when a later stage
is marked, so a renderer may pass token copies between its draw and swap hooks.
Snapshots sort only bounded metadata samples; the media hot path performs no
per-frame filesystem I/O. Call snapshots at a monitoring rate such as once per
second, rather than once per packet/frame.
After an active stage has produced a first frame, an update gap of at least
stall_ms is recorded as one stop. The full gap duration is counted, including
the time before the threshold. Ongoing stops update through snapshots. Pausing
or retiring a stage ends its current observation interval. Stages without any
first observation expose an unavailable age rather than guessing a stop time.
Sparse background keyframe inference has separate rates/intervals and does not
trigger continuous-stage stop detection.
The NVR sample exposes Controller::telemetry(), shows actual decode/swap/AI FPS
and input→decode/input→swap P99 columns, and provides full statistics in the FPS
cell tooltip. Display observation pauses while hidden, unexposed or minimized.
Headless mode leaves preview/draw/swap unavailable. Existing Status::decoded
and display_fps retain their legacy preview-processing meaning for API
compatibility; use the new decode_output and swap stage counters instead.
"telemetry": { "enabled": true, "stall_ms": 1000 }
This NVR configuration is optional and defaults as shown. Disabling telemetry
removes measurement probes; snapshots explicitly report enabled=false.
Counters distinguish compressed queue discards, overwritten unprocessed latest
frames, undisplayed previews, unobserved presentation submissions, unmatched
RTP/decode correspondence, ambiguous/missing PTS, expiry, capacity eviction,
stale generations and invalid monotonic stage ordering. These counters have no
operational pass/fail interpretation.
amdva-streams and the GUI validator add a separate top-level telemetry array
to their periodic JSON. AI result publication is unaffected. nvr_soak.py
preserves the detailed JSON-lines log and includes final telemetry in its
summary without adding latency/FPS acceptance checks. Generate a compact report:
python3 tools/timing_report.py artifacts/timing-nvr-hour-final \
--start=720 --output=artifacts/timing-hour-report.json
The report summarizes rolling-window P99s, not a whole-run P99. It also records FPS, interval P99s and process CPU/RSS/GPU usage. CPU percent is relative to one logical core. Compare telemetry-on/off runs on the same fixtures to observe measurement overhead without imposing a threshold.
Validation tools¶
telemetry-contract uses an injected clock for intervals, percentiles, rolling
bounds, stop accounting, duplicate notifications, generation retirement and
disabling. gst-telemetry-contract exercises controlled stage delays, RTP wrap,
partial video frames, audio exclusion, metadata ownership, adjusted output PTS,
reordered output, missing metadata, ambiguous PTS, flush, table bounds and
copy-on-write buffer lists.
The framework pipeline test also checks timing metadata through AMD task
elements, watermark, metadata aggregation and heterogeneous mux/demux.
amdva-validate-timing-rtp INPUT.mkv h264|h265 normal|reorder|loss repacketizes a
local Matroska B-frame fixture, injects packet reordering/duplication or loss,
and decodes through the actual VA decoder. It checks correspondence correctness
without an operational latency threshold. Fixture timestamps come from the
container; the encoded-only NVR RTSP fixture server deliberately does not
support B-frame timing and must not be used for that test. RTP timestamp and
sequence offsets exercise wrap in this tool.
nvr-timing-tests CONFIG renders actual RTSP sources, hides the window,
re-exposes it, minimizes it and remaps it. It checks that decode continues while
presentation is paused and that repeated drawing never inflates the new-image
count. The explicit hide/remap restoration accommodates Wayland compositors
that do not implement a programmatic unminimize request.
Recorded hardware validation¶
The machine-readable validation report records fixtures, runtime hashes, stage counts, correspondence counters and observed latency/FPS/resource statistics on Ryzen AI 9 HX 370 / Radeon 890M.
All 16 CTest contracts passed. The framework GPU copy audit retained timing metadata through processing, watermark, aggregation and mux/demux without a copy-audit violation. H.264 and H.265 B-frame fixtures each produced 60 decoded frames with normal delivery and with injected reordering/duplication; injected packet loss produced 59. All six cases retained exact input/decode correspondence. A separate live renderer test covered hiding, showing, minimizing, remapping and resizing. Installed standalone SDK examples processed both RTSP codecs, and a Qt-free headless build reported presentation unavailable.
The NVR regression used 20 received/recorded sources, 16 selected sources and four background slots, dynamic selection/add/remove, RTSP restart and codec replacement. The 90-second regression finalized 45 valid recordings and recovered receive/record activity 7.50 seconds after restart. The one-hour run completed 100 dynamic cycles, finalized 1,315 valid recordings and recovered 7.00 seconds after restart. Maximum post-warmup growth was 30,284 KiB RSS, 27,456 KiB GPU allocation and four FDs. It uses a frozen intermediate runtime; its hashes and the later changes covered by separate final-build tests are recorded explicitly. The intermediate runtime lacked input correspondence after background keyframe header prepending; the final comparison verifies the repaired path for all four hidden sources. Concurrent regression tests ran during part of that long run. The sequential telemetry-on/off comparison uses the final runtime after those jobs have finished.
The report's latency statistics summarize ten-second window P99 observations; they are not whole-run percentiles. They and the single telemetry-on/off pair are observations, without latency/FPS acceptance thresholds or a performance guarantee. Existing soak recording and resource guards remain in place.
In the isolated final-runtime comparison, selected preview FPS medians were 29.88 with telemetry and 29.93 without it. Process CPU averaged 130.12% versus 123.94% of one logical core, a 6.18 percentage-point difference (4.99% relative). These runs include the validator's snapshot/JSON output. The telemetry-on input→decode and input→swap window-P99 medians were 31.75 ms and 66.21 ms across selected sources and observation windows, respectively. Both 90-second runs passed the existing recording/resource checks. All four hidden sources produced background input→decode samples with zero unmatched decoder observations.