generateReport method
Implementation
String generateReport(List<FrameData> frames,
{int targetFps = 60, bool fromFrameTimings = false}) {
if (frames.isEmpty) {
return 'No frame data captured. Interact with the app during recording window.';
}
final budgetMicros = 1000000 ~/ targetFps;
final janky = frames.where((f) => f.isJanky(targetFps: targetFps)).toList();
final jankyPct = (janky.length / frames.length * 100).toStringAsFixed(1);
final uiJank =
janky.where((f) => f.uiDurationMicros > budgetMicros).length;
final rasterJank = janky
.where((f) =>
f.rasterDurationMicros > budgetMicros &&
f.uiDurationMicros <= budgetMicros)
.length;
final sorted = [...frames]
..sort((a, b) => b.workDurationMicros.compareTo(a.workDurationMicros));
String fpsNote = '';
if (frames.length >= 2) {
final windowMicros =
frames.last.uiStartMicros - frames.first.uiStartMicros;
if (windowMicros > 0) {
final rawFps = (frames.length - 1) / (windowMicros / 1e6);
// Cap at targetFps+10 — higher values indicate stale timeline events
final displayFps = rawFps > targetFps + 10
? targetFps.toDouble()
: rawFps;
fpsNote = ' (~${displayFps.toStringAsFixed(1)} fps)';
}
}
final source = fromFrameTimings ? 'Flutter.Frame events' : 'timeline parse';
final hasRaster = frames.any((f) => f.rasterDurationMicros > 0);
final sb = StringBuffer();
sb.writeln(
'Frame Analysis — ${frames.length} frames$fpsNote via $source');
sb.writeln('Budget: ${budgetMicros ~/ 1000}ms at ${targetFps}fps');
sb.writeln('━' * 60);
sb.writeln(
'Janky: ${janky.length}/${frames.length} ($jankyPct%) — ${_severity(janky.length, frames.length)}');
if (!hasRaster) {
sb.writeln(
'Raster timing: unavailable in debug mode (run --profile for raster data)');
}
if (uiJank > 0) {
sb.writeln('');
sb.writeln('UI jank: $uiJank frames over budget');
sb.writeln(' → Dart code slow. Check build(), layout, heavy compute.');
}
if (rasterJank > 0) {
sb.writeln('');
sb.writeln('Raster jank: $rasterJank frames');
sb.writeln(' → GPU slow. Check: large images, clips, opacity, shadows.');
}
sb.writeln('');
sb.writeln('Worst 5 frames:');
for (final f in sorted.take(5)) {
final r = hasRaster
? ', Raster: ${(f.rasterDurationMicros / 1000).toStringAsFixed(2)}ms'
: '';
sb.writeln(
' Frame ${f.frameNumber}: ${(f.workDurationMicros / 1000).toStringAsFixed(2)}ms work'
' (Build: ${(f.uiDurationMicros / 1000).toStringAsFixed(2)}ms$r)');
}
return sb.toString();
}