import 'dart:collection'; import 'dart:ui'; import 'package:core/core.dart'; import 'package:flutter/scheduler.dart'; /// FpsManager callback. typedef FpsCallback = void Function(FpsInfo fpsInfo); class FpsManager { FpsManager._(); factory FpsManager() => _instance ??= FpsManager._(); static FpsManager? _instance; /// Threshold time consuming per frame /// 1000/60hz ≈ 16.6ms 1000/120hz ≈ 8.3ms Duration _thresholdPerFrame = const Duration(microseconds: Duration.microsecondsPerSecond ~/ 60); /// Refresh rate, default 60 double _refreshRate = 60; /// Set refresh rate set refreshRate(double rate) { if (rate != _refreshRate && rate >= 60) { _refreshRate = rate; _thresholdPerFrame = Duration(microseconds: Duration.microsecondsPerSecond ~/ _refreshRate); } } bool _started = false; final List _fpsCallbacks = []; /// Temporarily save 120 frames static const int queueCapacity = 120; final ListQueue framesQueue = ListQueue(queueCapacity); void addFpsCallback(FpsCallback callback) { _fpsCallbacks.add(callback); } void removeFpsCallback(FpsCallback callback) { assert(_fpsCallbacks.contains(callback)); _fpsCallbacks.remove(callback); } void start() async { if (BuildUtils.isDebugMode || BuildUtils.isProfileMode) { if (!_started) { SchedulerBinding.instance.addTimingsCallback(_onTimingsCallback); _started = true; } } } void stop() { if (BuildUtils.isDebugMode || BuildUtils.isProfileMode) { if (_started) { SchedulerBinding.instance.removeTimingsCallback(_onTimingsCallback); _started = false; } } } _onTimingsCallback(List timings) async { if (_fpsCallbacks.isNotEmpty) { for (FrameTiming timing in timings) { framesQueue.addFirst(timing); } while (framesQueue.length > queueCapacity) { framesQueue.removeLast(); } List drawFrames = []; for (FrameTiming timing in framesQueue) { if (drawFrames.isEmpty) { drawFrames.add(timing); } else { int lastStart = drawFrames.last.timestampInMicroseconds(FramePhase.vsyncStart); int interval = lastStart - timing.timestampInMicroseconds(FramePhase.rasterFinish); if (interval > (_thresholdPerFrame.inMicroseconds * 2)) { // maybe in different set break; } drawFrames.add(timing); } } framesQueue.clear(); // compute total frames count. int totalCount = drawFrames.map((frame) { // If droppedCount > 0, int droppedCount = frame.totalSpan.inMicroseconds ~/ _thresholdPerFrame.inMicroseconds; return droppedCount + 1; }).fold(0, (a, b) => a + b); int drawFramesCount = drawFrames.length; int droppedCount = totalCount - drawFramesCount; double fps = drawFramesCount / totalCount * _refreshRate; FpsInfo fpsInfo = FpsInfo(fps, totalCount, droppedCount, drawFramesCount); for (var callBack in _fpsCallbacks) { callBack(fpsInfo); } } } } class FpsInfo { final double fps; final int totalFramesCount; final int droppedFramesCount; final int drawFramesCount; FpsInfo( this.fps, this.totalFramesCount, this.droppedFramesCount, this.drawFramesCount ); @override String toString() { return 'FpsInfo{' 'fps: $fps, ' 'totalFramesCount: $totalFramesCount, ' 'droppedFramesCount: $droppedFramesCount, ' 'drawFramesCount: $drawFramesCount}'; } }