settleLive function

Future<LiveSettleResult> settleLive({
  1. Duration budget = const Duration(milliseconds: 800),
  2. Duration frameTimeout = const Duration(milliseconds: 250),
})

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 enterText reads the tree from before the text.
  • Each endOfFrame wait 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,
  );
}