Skip to content

`LiveDiagnosticsSheet`

LiveDiagnosticsSheet is the trainer-facing diagnostics widget you mount on a test page or behind a long-press gesture. It renders three sections, live tracker state, the pose engine per-kernel timing, backend parity, driven by a single ValueListenable<LiveDiagnosticsSnapshot> the host page updates per frame.

The sheet is tracker-agnostic, it doesn’t reach into MovementTracker or ShowcaseTracker directly. The host maintains a LiveDiagnosticsSnapshot and feeds it whatever signals it has wired; rows without a value show a dash.

final ValueNotifier<LiveDiagnosticsSnapshot> diag = ValueNotifier(
const LiveDiagnosticsSnapshot(),
);
LiveDiagnosticsSheet(snapshot: diag);

When to use it

For consumer-facing apps, gate it behind a debug flag or a long-press on a logo, it’s a developer / trainer tool, not a customer surface. A typical pattern is to mount it on an internal QA / test page behind a button so testers can verify hardware backends and FPS without attaching a debugger.

For Studio / showcase / CMS surfaces, mount it inline as a side panel while authoring, trainers benefit from seeing whether the camera or the inference pipeline is the bottleneck.

LiveDiagnosticsSnapshot shape

All fields are nullable. A field set to null renders as a dash.

class LiveDiagnosticsSnapshot {
// Frame state
final int? frameNumber;
final int? imageWidth;
final int? imageHeight;
final double? fps; // successful inference rate
final double? cameraFps; // camera-capture rate (BEFORE inference)
// the pose engine timing
final double? inferenceMs;
final bool? usedDetector;
// Per-stage Dart-side breakdown
final double? colorConvertMs;
final double? isolateSendMs;
final double? isolateReceiveMs;
final double? dartTrackingMs;
// Per-kernel C-side breakdown
final double? cBackboneMs;
final double? cLandmarkHeadMs;
final double? cDetectorMs;
// Backend negotiation
final String? backboneBackend;
final String? decoderBackend;
final String? landmarkBackend;
// Tracker state
final int? repCount;
final String? phaseId;
final double? formScore;
final double? lastRepFormScore;
}

copyWith is provided so you can patch individual fields without rebuilding the whole snapshot.

Wiring the snapshot

final diag = ValueNotifier(const LiveDiagnosticsSnapshot());
final fpsCounter = FpsCounter();
final cameraFpsCounter = FpsCounter();
TrackedMovementView(
// ...
onCameraFrame: () {
cameraFpsCounter.mark();
diag.value = diag.value.copyWith(cameraFps: cameraFpsCounter.value);
},
onPipelineTiming: (report) {
fpsCounter.mark();
diag.value = diag.value.copyWith(
fps: fpsCounter.value,
inferenceMs: report.totalMs,
colorConvertMs: report.colorConvertMs,
isolateSendMs: report.isolateSendMs,
isolateReceiveMs: report.isolateReceiveMs,
cBackboneMs: report.cBackboneMs,
cLandmarkHeadMs: report.cLandmarkHeadMs,
cDetectorMs: report.cDetectorMs,
usedDetector: report.usedDetector,
);
},
onTrackerTiming: (ms) {
diag.value = diag.value.copyWith(dartTrackingMs: ms);
},
onBackends: (backbone, decoder, landmark) {
diag.value = diag.value.copyWith(
backboneBackend: backbone,
decoderBackend: decoder,
landmarkBackend: landmark,
);
},
onImageSize: (w, h) {
diag.value = diag.value.copyWith(imageWidth: w, imageHeight: h);
},
onTrackingResult: (result) {
diag.value = diag.value.copyWith(
repCount: result.repCount,
phaseId: result.phaseId,
formScore: result.formScore,
lastRepFormScore: result.lastRepQuality?.formScore,
);
},
);
LiveDiagnosticsSheet(snapshot: diag);

Reading the panel

RowWhat it tells you
Camera FPS vs Inference FPSIf they’re equal, you’re keeping up. If camera FPS > inference FPS, inference is the cap and frames are being dropped. If camera FPS < inference FPS, the camera itself is the cap (typical: package:camera 30 fps Android cap with a faster pipeline).
InferencePer-frame total pose-engine C-pipeline time + a flag for heavy detector frames vs cheap tracking reuse. Detector frames dominate at startup or when tracking is lost.
Per-stage waterfallColor convert → isolate send → C backbone → C landmark head → isolate receive → Dart tracking. Lets you spot which stage spiked when overall FPS dropped.
Backend rowThe backends each segment of the pipeline negotiated, cpu, metal, nnapi, opencl, vulkan. A silent drop to CPU (driver-bind failure) is otherwise invisible behind a green FPS number.
Tracker stateRep count, current phase, current form score, last rep’s form score. Sanity check against the trainer’s actual movement.

FpsCounter

A rolling 30-sample FPS counter ships alongside LiveDiagnosticsSheet for convenience:

final fps = FpsCounter();
// per-frame
fps.mark();
// for rendering
final current = fps.value; // double, 0 until 2 frames recorded

Constant-time per mark(), no allocation, just one integer write.