parseFrames method

List<FrameData> parseFrames(
  1. Timeline timeline
)

Fallback: parse raw timeline — use async phase ('b'/'e') for Dart Frame and sync phase ('B'/'E') for Embedder raster events.

Implementation

List<FrameData> parseFrames(Timeline timeline) {
  final events = timeline.traceEvents ?? [];

  final uiFrames = <_RawFrame>[];
  final rasterFrames = <_RawFrame>[];
  final Map<String, int> asyncBegin = {}; // async frames: key = 'name|id'
  final Map<String, int> syncBegin = {};  // sync frames: key = 'name|tid'

  for (final event in events) {
    final raw = event.json;
    if (raw == null) continue;
    final name = raw['name'] as String? ?? '';
    final ph = raw['ph'] as String? ?? '';
    final ts = (raw['ts'] as num?)?.toInt() ?? 0;
    final tid = raw['tid'].toString();
    final cat = raw['cat'] as String? ?? '';
    // Async events use 'id' for correlation, not tid
    final id = raw['id']?.toString() ?? raw['id2']?.toString() ?? tid;

    // ── UI frames ────────────────────────────────────────────────────────
    // [Dart] Frame is async (ph='b'/'e') — TimelineTask in scheduler/binding.dart
    if (name == 'Frame' && cat == 'Dart') {
      if (ph == 'b') {
        asyncBegin['frame|$id'] = ts;
      } else if (ph == 'e') {
        final begin = asyncBegin.remove('frame|$id');
        if (begin != null && ts > begin) {
          uiFrames.add(_RawFrame(begin, ts - begin));
        }
      }
    }

    // [Embedder] Animator::BeginFrame is sync — fallback if async Frame absent
    if (name == 'Animator::BeginFrame' && cat == 'Embedder') {
      if (ph == 'B') {
        syncBegin['animbegin|$tid'] = ts;
      } else if (ph == 'E') {
        final begin = syncBegin.remove('animbegin|$tid');
        if (begin != null && ts > begin) {
          uiFrames.add(_RawFrame(begin, ts - begin));
        }
      }
    }

    // ── Raster frames ─────────────────────────────────────────────────────
    // [Embedder] GPURasterizer::Draw — sync duration event on raster thread
    if ((name == 'GPURasterizer::Draw' ||
            name == 'CompositorContext::ScopedFrame::Raster') &&
        cat == 'Embedder') {
      if (ph == 'B') {
        syncBegin['raster|$tid'] = ts;
      } else if (ph == 'E') {
        final begin = syncBegin.remove('raster|$tid');
        if (begin != null && ts > begin) {
          rasterFrames.add(_RawFrame(begin, ts - begin));
        }
      } else if (ph == 'X') {
        final dur = (raw['dur'] as num?)?.toInt() ?? 0;
        if (dur > 0) rasterFrames.add(_RawFrame(ts, dur));
      }
    }
  }

  if (uiFrames.isEmpty) return [];

  uiFrames.sort((a, b) => a.start.compareTo(b.start));
  rasterFrames.sort((a, b) => a.start.compareTo(b.start));

  // Timestamp-based pairing: match each UI frame to nearest raster frame
  final frames = <FrameData>[];
  int rIdx = 0;

  for (int i = 0; i < uiFrames.length; i++) {
    final ui = uiFrames[i];
    while (rIdx < rasterFrames.length &&
        rasterFrames[rIdx].start < ui.start) {
      rIdx++;
    }
    int rasterDur = 0;
    if (rIdx < rasterFrames.length) {
      final r = rasterFrames[rIdx];
      if (r.start - ui.start < 33000) {
        rasterDur = r.duration;
        rIdx++;
      }
    }
    final total = ui.duration > rasterDur ? ui.duration : rasterDur;
    frames.add(FrameData(
      frameNumber: i + 1,
      uiStartMicros: ui.start,
      uiDurationMicros: ui.duration,
      rasterDurationMicros: rasterDur,
      totalDurationMicros: total,
    ));
  }

  return frames;
}