parseFrames method
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;
}