generateReport method

String generateReport(
  1. List<FrameData> frames, {
  2. int targetFps = 60,
  3. bool fromFrameTimings = false,
})

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();
}