settleLive function
Pumps the live app until nothing is pending or budget runs out, and
never hangs.
A hidden window is the case that shapes everything here: the platform
disables frames, scheduleFrame no-ops, transitions wedge mid-flight with
their IgnorePointer up, and hasScheduledFrame reads false while tickers
are still waiting. So:
- Pending is
hasScheduledFrame || transientCallbackCount > 0 || images still decoding— tickers are the honest "something animates" probe when frames are disabled, and ImageCache.pendingImageCount is the window a capture must not fire in. - When frames are disabled, one frame is forced unconditionally first: a
dirty element is invisible to every probe, and without the flush an
observation after
enterTextreads the tree from before the text. - Each
endOfFramewait is capped, so an engine that refuses even forced frames makes this late, not stuck — and a wait the cap ended is counted as unanswered, never as a frame.
Implementation
Future<LiveSettleResult> settleLive({
Duration budget = const Duration(milliseconds: 800),
Duration frameTimeout = const Duration(milliseconds: 250),
}) async {
var binding = WidgetsBinding.instance;
var watch = Stopwatch()..start();
var frames = 0;
var unanswered = 0;
var forced = 0;
bool pending() =>
binding.hasScheduledFrame ||
binding.transientCallbackCount > 0 ||
PaintingBinding.instance.imageCache.pendingImageCount > 0;
Future<void> awaitFrame() async {
if (!binding.framesEnabled) {
binding.scheduleForcedFrame();
forced++;
}
var drawn = await Future.any([
binding.endOfFrame.then((_) => true),
Future<bool>.delayed(frameTimeout, () => false),
]);
if (drawn) {
frames++;
} else {
unanswered++;
}
}
if (!binding.framesEnabled) await awaitFrame();
while (watch.elapsed < budget) {
if (!pending()) {
// Give microtasks and just-completed futures one beat to schedule work
// before calling it settled.
await Future<void>.delayed(const Duration(milliseconds: 16));
if (!pending()) break;
continue;
}
await awaitFrame();
}
return LiveSettleResult(
settled: !pending(),
frames: frames,
unansweredFrames: unanswered,
forcedFrames: forced,
framesEnabled: binding.framesEnabled,
elapsed: watch.elapsed,
);
}