// Frame / hitch sampler for diagnostic packs. import 'badnote_log.dart'; class FrameSample { FrameSample({ required this.at, required this.label, required this.ms, this.dropped = false, }); final DateTime at; final String label; final double ms; final bool dropped; Map toJson() => { 'at': at.toIso8601String(), 'label': label, 'ms': ms, 'dropped': dropped, }; } class FrameSampler { FrameSampler._(); static final FrameSampler instance = FrameSampler._(); static const int capacity = 500; final List _samples = []; int overBudget = 0; int total = 0; /// Budget for a single frame at 60fps. static const double budgetMs = 16.6; void record(String label, double ms, {bool dropped = false}) { total++; final over = ms > budgetMs; if (over) overBudget++; final sample = FrameSample( at: DateTime.now(), label: label, ms: ms, dropped: dropped || over, ); _samples.add(sample); if (_samples.length > capacity) { _samples.removeRange(0, _samples.length - capacity); } if (over || dropped) { BadNoteLog.instance.warn( LogSubsystem.frame, 'slow_frame', fields: {'label': label, 'ms': ms, 'dropped': dropped}, ); } } void recordZoom({ required double rawScale, required bool scaleDrop, required bool focalDrop, required double focalJumpPx, }) { record( 'zoom', scaleDrop || focalDrop ? budgetMs + 1 : 8, dropped: scaleDrop || focalDrop, ); BadNoteLog.instance.debug( LogSubsystem.frame, 'zoom', fields: { 'rawScale': rawScale, 'scaleDrop': scaleDrop, 'focalDrop': focalDrop, 'focalJumpPx': focalJumpPx, }, ); } List> toJsonList() => _samples.map((s) => s.toJson()).toList(growable: false); Map summary() => { 'total': total, 'overBudget': overBudget, 'budgetMs': budgetMs, 'recent': toJsonList(), }; void reset() { _samples.clear(); overBudget = 0; total = 0; } }