105 lines
2.2 KiB
Dart
105 lines
2.2 KiB
Dart
|
|
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<FrameDiag> diag = ValueNotifier<FrameDiag>(
|
||
|
|
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<FrameTiming> 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';
|
||
|
|
}
|