reworked developer tools with diagnostics, storage viewers and release-capable debug overlays
This commit is contained in:
@@ -0,0 +1,205 @@
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
import 'dart:math';
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:filesize/filesize.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/scheduler.dart';
|
||||
|
||||
/// Release-capable frame and memory statistics drawn above [child]. Frame data
|
||||
/// comes from [SchedulerBinding.addTimingsCallback], which the engine reports
|
||||
/// in batches (about once per second in release builds).
|
||||
class DebugStatsOverlay extends StatefulWidget {
|
||||
final bool showFrameStats;
|
||||
final bool showMemoryStats;
|
||||
final Widget child;
|
||||
|
||||
const DebugStatsOverlay({
|
||||
required this.showFrameStats,
|
||||
required this.showMemoryStats,
|
||||
required this.child,
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
State<DebugStatsOverlay> createState() => _DebugStatsOverlayState();
|
||||
}
|
||||
|
||||
class _DebugStatsOverlayState extends State<DebugStatsOverlay> {
|
||||
static const _sampleWindow = 240;
|
||||
|
||||
final List<FrameTiming> _frames = [];
|
||||
int _jankTotal = 0;
|
||||
int _frameTotal = 0;
|
||||
Timer? _ticker;
|
||||
bool _listening = false;
|
||||
|
||||
bool get _active => widget.showFrameStats || widget.showMemoryStats;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_sync();
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(DebugStatsOverlay oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
_sync();
|
||||
}
|
||||
|
||||
void _sync() {
|
||||
if (widget.showFrameStats && !_listening) {
|
||||
SchedulerBinding.instance.addTimingsCallback(_onTimings);
|
||||
_listening = true;
|
||||
} else if (!widget.showFrameStats && _listening) {
|
||||
SchedulerBinding.instance.removeTimingsCallback(_onTimings);
|
||||
_listening = false;
|
||||
_frames.clear();
|
||||
_jankTotal = 0;
|
||||
_frameTotal = 0;
|
||||
}
|
||||
if (_active) {
|
||||
_ticker ??= Timer.periodic(
|
||||
const Duration(seconds: 1),
|
||||
(_) => setState(() {}),
|
||||
);
|
||||
} else {
|
||||
_ticker?.cancel();
|
||||
_ticker = null;
|
||||
}
|
||||
}
|
||||
|
||||
Duration get _budget {
|
||||
final rate = View.maybeOf(context)?.display.refreshRate ?? 60;
|
||||
return Duration(microseconds: (1e6 / (rate > 0 ? rate : 60)).round());
|
||||
}
|
||||
|
||||
void _onTimings(List<FrameTiming> timings) {
|
||||
final budget = _budget;
|
||||
for (final t in timings) {
|
||||
_frameTotal++;
|
||||
if (t.buildDuration > budget || t.rasterDuration > budget) _jankTotal++;
|
||||
}
|
||||
_frames.addAll(timings);
|
||||
if (_frames.length > _sampleWindow) {
|
||||
_frames.removeRange(0, _frames.length - _sampleWindow);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
if (_listening) SchedulerBinding.instance.removeTimingsCallback(_onTimings);
|
||||
_ticker?.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (!_active) return widget.child;
|
||||
return Stack(
|
||||
textDirection: TextDirection.ltr,
|
||||
children: [
|
||||
widget.child,
|
||||
Positioned(
|
||||
left: 4,
|
||||
bottom: MediaQuery.paddingOf(context).bottom + 4,
|
||||
child: IgnorePointer(
|
||||
child: MediaQuery.withNoTextScaling(
|
||||
child: DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.black.withValues(alpha: 0.7),
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 6,
|
||||
vertical: 4,
|
||||
),
|
||||
child: DefaultTextStyle(
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 10,
|
||||
height: 1.3,
|
||||
fontFamily: 'monospace',
|
||||
decoration: TextDecoration.none,
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (widget.showFrameStats) ..._frameLines(),
|
||||
if (widget.showMemoryStats) ..._memoryLines(),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
List<Widget> _frameLines() {
|
||||
final budgetMs = _budget.inMicroseconds / 1000;
|
||||
if (_frames.isEmpty) {
|
||||
return [Text('Frames: – (Budget ${budgetMs.toStringAsFixed(1)} ms)')];
|
||||
}
|
||||
final build = _sortedMs((t) => t.buildDuration);
|
||||
final raster = _sortedMs((t) => t.rasterDuration);
|
||||
final jankPct = _frameTotal == 0 ? 0 : _jankTotal * 100 / _frameTotal;
|
||||
final lastWallclock = _frames.last.timestampInMicroseconds(
|
||||
FramePhase.rasterFinish,
|
||||
);
|
||||
final recent = _frames
|
||||
.where(
|
||||
(t) =>
|
||||
lastWallclock -
|
||||
t.timestampInMicroseconds(FramePhase.rasterFinish) <
|
||||
1000000,
|
||||
)
|
||||
.length;
|
||||
return [
|
||||
Text(
|
||||
'Budget ${budgetMs.toStringAsFixed(1)} ms · letzte Sek. $recent Frames',
|
||||
),
|
||||
Text(
|
||||
'Build p50 ${_p(build, 50)} p90 ${_p(build, 90)} max ${_ms(build.last)}',
|
||||
),
|
||||
Text(
|
||||
'Raster p50 ${_p(raster, 50)} p90 ${_p(raster, 90)} max ${_ms(raster.last)}',
|
||||
),
|
||||
Text('Jank $_jankTotal/$_frameTotal (${jankPct.toStringAsFixed(1)} %)'),
|
||||
];
|
||||
}
|
||||
|
||||
List<Widget> _memoryLines() {
|
||||
final cache = PaintingBinding.instance.imageCache;
|
||||
return [
|
||||
Text(
|
||||
'RSS ${filesize(ProcessInfo.currentRss)} · max ${filesize(ProcessInfo.maxRss)}',
|
||||
),
|
||||
Text(
|
||||
'Bilder ${cache.currentSize}/${cache.maximumSize} · '
|
||||
'${filesize(cache.currentSizeBytes)}/${filesize(cache.maximumSizeBytes)} · '
|
||||
'live ${cache.liveImageCount}',
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
List<double> _sortedMs(Duration Function(FrameTiming) pick) =>
|
||||
_frames.map((t) => pick(t).inMicroseconds / 1000).toList()..sort();
|
||||
|
||||
String _p(List<double> sorted, int percentile) {
|
||||
final i = min(
|
||||
sorted.length - 1,
|
||||
(sorted.length * percentile / 100).floor(),
|
||||
);
|
||||
return _ms(sorted[i]);
|
||||
}
|
||||
|
||||
String _ms(double v) => v.toStringAsFixed(1).padLeft(5);
|
||||
}
|
||||
Reference in New Issue
Block a user