import 'package:flutter/foundation.dart'; import 'package:flutter/scheduler.dart'; import '../../../shared/utils/app_logger.dart'; const _tag = 'FrameJank'; @immutable class FrameDiag { final double fps; final double lastBuildMs; final double lastRasterMs; final int jankCount; const FrameDiag({ required this.fps, required this.lastBuildMs, required this.lastRasterMs, required this.jankCount, }); static const zero = FrameDiag( fps: 0, lastBuildMs: 0, lastRasterMs: 0, jankCount: 0, ); } class FrameJankMonitor { FrameJankMonitor({this.jankThreshold = const Duration(milliseconds: 32)}); final Duration jankThreshold; final ValueNotifier diag = ValueNotifier( FrameDiag.zero, ); TimingsCallback? _callback; int _jankCount = 0; int _windowFrames = 0; final Stopwatch _windowClock = Stopwatch()..start(); double _fps = 0; void start() { if (kReleaseMode || _callback != null) return; final cb = _onTimings; _callback = cb; SchedulerBinding.instance.addTimingsCallback(cb); } void _onTimings(List timings) { if (timings.isEmpty) return; for (final t in timings) { if (t.buildDuration >= jankThreshold || t.rasterDuration >= jankThreshold) { _jankCount++; AppLogger.warn( _tag, 'janky frame build=${_ms(t.buildDuration)} ' 'raster=${_ms(t.rasterDuration)} total=${_ms(t.totalSpan)}', ); } } _windowFrames += timings.length; final elapsedMs = _windowClock.elapsedMilliseconds; if (elapsedMs >= 500) { _fps = _windowFrames * 1000 / elapsedMs; _windowFrames = 0; _windowClock.reset(); } final last = timings.last; diag.value = FrameDiag( fps: _fps, lastBuildMs: last.buildDuration.inMicroseconds / 1000, lastRasterMs: last.rasterDuration.inMicroseconds / 1000, jankCount: _jankCount, ); } void dispose() { final cb = _callback; if (cb != null) { SchedulerBinding.instance.removeTimingsCallback(cb); _callback = null; } diag.dispose(); } static String _ms(Duration d) => '${(d.inMicroseconds / 1000).toStringAsFixed(1)}ms'; }