133 lines
3.6 KiB
Dart
133 lines
3.6 KiB
Dart
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<FpsCallback> _fpsCallbacks = [];
|
|
|
|
/// Temporarily save 120 frames
|
|
static const int _queue_capacity = 120;
|
|
final ListQueue framesQueue = ListQueue<FrameTiming>(_queue_capacity);
|
|
|
|
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<FrameTiming> timings) async {
|
|
if (_fpsCallbacks.isNotEmpty) {
|
|
for (FrameTiming timing in timings) {
|
|
framesQueue.addFirst(timing);
|
|
}
|
|
while (framesQueue.length > _queue_capacity) {
|
|
framesQueue.removeLast();
|
|
}
|
|
|
|
List<FrameTiming> 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}';
|
|
}
|
|
} |