Repository navigation
Replace Shell::WaitForFirstFrame with an asynchronous API that matches the Shell threading model - #191841
Conversation
There was a problem hiding this comment.
Overall this looks really good.
Part of this is really similar to the hacked up local version I had. I had added a FlutterFirstFramewaiter class that tried to handle the cancellation relatively similarly to how it happens now (and cycling through them to cancel them all in destroyContext), but poking at this a bit more I think there might be a nicer solution that removes the need for all that and gets rid of the blocking (see my comment at the bottom).
| // Clear any tasks waiting on first frame prior to destroying _shell. | ||
| if (_shell) { | ||
| _shell->CancelWaitForFirstFrame(); | ||
| } |
There was a problem hiding this comment.
Thinking back to the issue this was added for, what happens if we have someone waiting? It we destroy the engine while we're waiting, I'm assuming we'll block for up to 3 seconds on the main thread?
I think we can probably delete _firstFrameWaiters and the wait below altogether, then the waiter would just stick around in the background until timeout.
There was a problem hiding this comment.
Removed _firstFrameWaiters and replaced its usage with the implementation in #191841 (comment)
| callback:(void (^_Nonnull)(BOOL didTimeout))callback { | ||
| auto first_frame_event = std::make_shared<fml::AutoResetWaitableEvent>(); | ||
|
|
||
| self.shell.AddFirstFrameCallback([weak_event = std::weak_ptr(first_frame_event)] { |
There was a problem hiding this comment.
self.shell has an FML_DCHECK in it so I think if we call this before FlutterEngine run or during shutdown, the assertion would fail (or null dereference in non-unopt builds), so we should check shell.
Not sure what the right value to set didTimeOut is in that case... YES seems more like what I'd expect since NO suggests the first frame rendered and someone might act on that.
There was a problem hiding this comment.
nit: With the change in destroyContext, nothing is cancelling these guys anymore so we should update the comment.
| @@ -529,14 +525,8 @@ class Shell final : public PlatformView::Delegate, | |||
| // waiting_for_first_frame_mutex_ in WaitForFirstFrame. | |||
| std::atomic<bool> waiting_for_first_frame_ = true; | |||
There was a problem hiding this comment.
s/WaitForFirstFrame/AddFirstFrameCallback/
|
|
||
| std::mutex waiting_for_first_frame_mutex_; | ||
| std::condition_variable waiting_for_first_frame_condition_; | ||
| std::vector<std::function<void()>> waiting_for_first_frame_callbacks_; |
There was a problem hiding this comment.
I'd be tempted to leave the "guarded by waiting_for_first_frame_mutex_" comment to make it easier for future us to put two and two together.
| std::vector<std::function<void()>> callbacks; | ||
| { | ||
| std::scoped_lock lock(waiting_for_first_frame_mutex); | ||
| std::swap(waiting_for_first_frame_callbacks, callbacks); |
There was a problem hiding this comment.
Wonder if it would be useful to leave a comment here about the relationship between waiting_for_first_frame and waiting_for_first_frame_callbacks and specifically the ordering guarantees these need.
The fact that the atomic bool is outside the lock but the callbacks vector is inside it got me stopping and thinking for longer than I'd like to admit; specifically what could happen in between. I wonder if we should just move the bool inside the lock to avoid anyone having to think about it.
I'm pretty convinced it's fine as-is, if someone registers in the gap they'll see it's false and invoke immediately.
There was a problem hiding this comment.
Yeah - I considered changing waiting_for_first_frame_ to a simple bool that is guarded by waiting_for_first_frame_mutex_. But given that
Shell::OnAnimatorDraw is called on every frame, I decided to keep the existing atomic bool waiting_for_first_frame_ to minimize the per-frame cost.
Added a comment and moved the waiting_for_first_frame.store(false) write into the OnAnimatorDraw block that holds the waiting_for_first_frame_mutex_. That should make this clearer.
This works because waiting_for_first_frame_ is set to true when the shell launches, but after the first frame it is set to false and remains false until the shell is suspended and resumed.
Users of waiting_for_first_frame_callbacks_ will also access
waiting_for_first_frame_ while holding the waiting_for_first_frame_mutex_. But OnAnimatorDraw can check whether waiting_for_first_frame_ has become false
without acquiring the mutex.
| /// GPU or UI thread, 'kDeadlineExceeded' if there is a timeout. | ||
| /// | ||
| fml::Status WaitForFirstFrame(fml::TimeDelta timeout); | ||
| void AddFirstFrameCallback(std::function<void()> callback); |
There was a problem hiding this comment.
We should probably state that any callbacks pending on teardown will never be invoked. Maybe worth mentioning that the callback may run on the raster thread so needs to be cheap.
consistency nit: The next frame callback uses fml::closure, which IIRC is just std::function<void()>. Doesn't bother me one way or another but noticed, since in my hacked-up attempt at this I went the other way. Up to you.
There was a problem hiding this comment.
Added a comment.
I considered using fml::closure, but the recent trend in the engine seems to be towards replacing usage of FML APIs with standard library or Abseil APIs.
| // Ensure CancelWaitForFirstFrame() correctly causes all tasks blocked on | ||
| // WaitForFirstFrame() to return kAborted. | ||
| // | ||
| // See: b/521830222 |
There was a problem hiding this comment.
Since this is the only test coverage we had for this bug, we should probably replace it with something roughly equivalent that verifies safe shutdown. Something where we register a callback, destroy the shell, and asserting the closure never ran.
| didTimeout = status.code() == fml::StatusCode::kDeadlineExceeded; | ||
| didTimeout = first_frame_event->WaitWithTimeout(waitTime); | ||
| dispatch_group_leave(firstFrameWaiters); | ||
| }); |
There was a problem hiding this comment.
I wonder if we could push this even further into async territory. What if when we register, we immediately kick off a timeout handler to fire on the main thread AND we register the completion handler (which will get called on the raster thread but post the callback back to the main thread) these guys then race each other and the winner grabs the completion handler to a local, nils it out to the other can't fire it, then invokes it.
The nice thing with this is we avoid the whole _firstFrameWaiters dispatch group and therefore avoid blocking the main thread for the timeout in destroyContext.
- (void)waitForFirstFrame:(NSTimeInterval)timeout
callback:(void (^_Nonnull)(BOOL didTimeout))callback {
// Set up a completion handler that will nil itself out when it fires.
__block void (^completion)(BOOL) = [callback copy];
void (^complete)(BOOL) = ^(BOOL didTimeout) {
if (completion) {
void (^cb)(BOOL) = completion;
completion = nil;
cb(didTimeout);
}
};
if (!_shell) {
// No shell. Bail out since there'll never be a first frame.
dispatch_async(dispatch_get_main_queue(), ^{
complete(YES);
});
return;
}
// Register the first-frame callback and fire the timeout completion handler.
// Both are scheduled on the main thread to guarantee ordering.
// The winner nils out the completion handler so the loser is a no-op.
_shell->AddFirstFrameCallback([complete] {
dispatch_async(dispatch_get_main_queue(), ^{
complete(NO);
});
});
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(timeout * NSEC_PER_SEC)),
dispatch_get_main_queue(), ^{
complete(YES);
});
}There was a problem hiding this comment.
Yes - that looks much cleaner! Replaced the timeout mechanism in FlutterEngine.waitForFirstFrame with this implementation.
There was a problem hiding this comment.
Code Review
This pull request replaces the blocking WaitForFirstFrame mechanism in the Shell class with an asynchronous callback-based approach via AddFirstFrameCallback, updating associated unit tests and the iOS platform integration. Feedback on these changes identifies a potential deadlock or 30-second hang during engine destruction if the Shell is destroyed before the first frame is rendered, because the registered callback would be destroyed without signaling the waiting background thread.
| } | ||
| }; | ||
|
|
||
| fml::TimeDelta waitTime = fml::TimeDelta::FromMilliseconds(timeout * 1000); | ||
| fml::Status status = strongSelf.shell.WaitForFirstFrame(waitTime); | ||
| didTimeout = status.code() == fml::StatusCode::kDeadlineExceeded; | ||
| dispatch_group_leave(firstFrameWaiters); | ||
| }); | ||
| if (!_shell) { | ||
| // No shell. Bail out since there'll never be a first frame. | ||
| dispatch_async(dispatch_get_main_queue(), ^{ | ||
| complete(YES); |
There was a problem hiding this comment.
If the Shell is destroyed before the first frame is rendered, the callback registered with AddFirstFrameCallback will be destroyed without being executed. Because nothing signals first_frame_event, the background thread waiting in first_frame_event->WaitWithTimeout(waitTime) (line 1593) will block for the entire timeout duration (which can be up to 30 seconds).
During engine destruction, destroyContext is called on the main thread and blocks waiting for all background waiters to finish:
dispatch_group_wait(_firstFrameWaiters, DISPATCH_TIME_FOREVER);This will cause the main thread to freeze/deadlock for up to 30 seconds during app shutdown or engine destruction, leading to severe UI hangs or ANRs.
To prevent this, we can capture a std::shared_ptr with a custom deleter in the callback. If the callback is destroyed without being executed, the custom deleter will run and signal the event, immediately unblocking the background thread.
auto first_frame_event = std::make_shared<fml::AutoResetWaitableEvent>();
auto signal_on_destroy = std::shared_ptr<void>(nullptr, [first_frame_event](void*) {
first_frame_event->Signal();
});
self.shell.AddFirstFrameCallback([weak_event = std::weak_ptr(first_frame_event), signal_on_destroy] {
if (auto event = weak_event.lock()) {
event->Signal();
}
});
There was a problem hiding this comment.
This comment is referring to an obsolete version of the PR. _firstFrameWaiters and first_frame_event have been removed.
…s the Shell threading model The Shell::WaitForFirstFrame API would block until the engine renders the first frame. This could potentially cause issues if the shell is destroyed while a thread is waiting within a call to WaitForFirstFrame. This PR replaces WaitForFirstFrame with an AddFirstFrameCallback API that invokes a callback when the first frame has been rendered. If the shell is destroyed before the first frame, then the callback will never be executed. See flutter#190132
261da90 to
967cb15
Compare
cbracken
left a comment
There was a problem hiding this comment.
I patched this in locally and created an app with a configurable RenderBinding.deferFirstFrame so I could test delivery before the timeout (~2900ms), after it (5000ms), and right at the boundary (2950 ~ 2990 ms). I stuck a .well-known/apple-app-site-association on a personal domain for testing this.
Everything looks good. I also tested the existing version and one thing I noticed is this performs a little better right at the boundary; I actually had to push the first frame delay by a few milliseconds to get it to timeout with the new code.
This is a really nice improvement. Thanks so much for landing this.
flutter/flutter@0cbd1a4...70797e1 2026-09-02 bkonyi@google.com [analysis] Upgrade package:analyzer to 14.3.0 and enable custom plugin compilation (flutter/flutter#191590) 2026-09-02 engine-flutter-autoroll@skia.org Roll Dart SDK from d8d7869d8031 to 9a8fbde80f6c (1 revision) (flutter/flutter#192179) 2026-09-02 engine-flutter-autoroll@skia.org Roll Skia from dec311244db4 to ae22da2e8308 (1 revision) (flutter/flutter#192177) 2026-09-02 bkonyi@google.com [tool] Migrate DoctorCommand to modular dependency injection (flutter/flutter#190758) 2026-09-02 engine-flutter-autoroll@skia.org Roll Skia from 482b7e285244 to dec311244db4 (3 revisions) (flutter/flutter#192161) 2026-09-02 engine-flutter-autoroll@skia.org Roll Skia from 28447e9fb8d5 to 482b7e285244 (3 revisions) (flutter/flutter#192157) 2026-09-02 34465683+rkishan516@users.noreply.github.com feat: Add placeholder to DecorationImage (flutter/flutter#191528) 2026-09-02 papmodern14@gmail.com [Android] Do not schedule engine frames on a detached FlutterJNI (flutter/flutter#191204) 2026-09-02 engine-flutter-autoroll@skia.org Roll Skia from 3a242fb7cdb3 to 28447e9fb8d5 (1 revision) (flutter/flutter#192145) 2026-09-02 engine-flutter-autoroll@skia.org Roll Dart SDK from 9164def35347 to d8d7869d8031 (2 revisions) (flutter/flutter#192144) 2026-09-02 74458687+anazr9@users.noreply.github.com Add FadeInImageTransition.fadeInOver to fade the image in over the placeholder (flutter/flutter#186246) 2026-09-02 engine-flutter-autoroll@skia.org Roll Skia from 63d13ec4df6d to 3a242fb7cdb3 (9 revisions) (flutter/flutter#192138) 2026-09-02 bkonyi@google.com [flutter_tools] Add --no-plugins flag and disable plugins for benchmark (flutter/flutter#192129) 2026-09-02 bkonyi@google.com [tool] Migrate ChannelCommand to modular dependency injection (flutter/flutter#190751) 2026-09-01 46920873+gabrimatic@users.noreply.github.com Add mouseCursor to RawScrollbar (flutter/flutter#185750) 2026-09-01 ksanaullah383.khan@gmail.com Fix self-comparison and assert typos in TestSemantics (flutter/flutter#191938) 2026-09-01 robert.ancell@canonical.com [Linux] Wait for frames to be rendered before using them in another context (flutter/flutter#192094) 2026-09-01 jacksongardner@google.com [wimp] Turn on multithreading for wimp. (flutter/flutter#191747) 2026-09-01 engine-flutter-autoroll@skia.org Roll Skia from 3911a1fe7f7a to 63d13ec4df6d (6 revisions) (flutter/flutter#192125) 2026-09-01 jason-simmons@users.noreply.github.com Remove the redundant flutter/shell/platform/android:robolectric_tests build target (flutter/flutter#192117) 2026-09-01 AfzalivE@users.noreply.github.com [iOS] Preserve semantics parents after reparenting (flutter/flutter#189686) 2026-09-01 bkonyi@google.com [flutter_tools] Report preview reload timing analytics in LspPreviewDetector (flutter/flutter#192120) 2026-09-01 bkonyi@google.com [flutter_tools] Implement Templates slice and flutter create integration (flutter/flutter#191748) 2026-09-01 louisehsu@google.com Uiscene migrate add2app hosts (flutter/flutter#191847) 2026-09-01 engine-flutter-autoroll@skia.org Roll Packages from d642322 to 7a7912f (8 revisions) (flutter/flutter#192119) 2026-09-01 zhongliu88889@gmail.com [web] Anchor flt-semantics-host at 0,0 to fix WebKit semantics offset (flutter/flutter#190486) 2026-09-01 jason-simmons@users.noreply.github.com Replace Shell::WaitForFirstFrame with an asynchronous API that matches the Shell threading model (flutter/flutter#191841) If this roll has caused a breakage, revert this CL and stop the roller using the controls here: https://autoroll.skia.org/r/flutter-packages Please CC stuartmorgan@google.com on the revert to ensure that a human is aware of the problem. To file a bug in Packages: https://github.com/flutter/flutter/issues/new/choose To report a problem with the AutoRoller itself, please file a bug: https://issues.skia.org/issues/new?component=1389291&template=1850622 Documentation for the AutoRoller is here: https://skia.googlesource.com/buildbot/+doc/main/autoroll/README.md
…r#12727) flutter/flutter@0cbd1a4...70797e1 2026-09-02 bkonyi@google.com [analysis] Upgrade package:analyzer to 14.3.0 and enable custom plugin compilation (flutter/flutter#191590) 2026-09-02 engine-flutter-autoroll@skia.org Roll Dart SDK from d8d7869d8031 to 9a8fbde80f6c (1 revision) (flutter/flutter#192179) 2026-09-02 engine-flutter-autoroll@skia.org Roll Skia from dec311244db4 to ae22da2e8308 (1 revision) (flutter/flutter#192177) 2026-09-02 bkonyi@google.com [tool] Migrate DoctorCommand to modular dependency injection (flutter/flutter#190758) 2026-09-02 engine-flutter-autoroll@skia.org Roll Skia from 482b7e285244 to dec311244db4 (3 revisions) (flutter/flutter#192161) 2026-09-02 engine-flutter-autoroll@skia.org Roll Skia from 28447e9fb8d5 to 482b7e285244 (3 revisions) (flutter/flutter#192157) 2026-09-02 34465683+rkishan516@users.noreply.github.com feat: Add placeholder to DecorationImage (flutter/flutter#191528) 2026-09-02 papmodern14@gmail.com [Android] Do not schedule engine frames on a detached FlutterJNI (flutter/flutter#191204) 2026-09-02 engine-flutter-autoroll@skia.org Roll Skia from 3a242fb7cdb3 to 28447e9fb8d5 (1 revision) (flutter/flutter#192145) 2026-09-02 engine-flutter-autoroll@skia.org Roll Dart SDK from 9164def35347 to d8d7869d8031 (2 revisions) (flutter/flutter#192144) 2026-09-02 74458687+anazr9@users.noreply.github.com Add FadeInImageTransition.fadeInOver to fade the image in over the placeholder (flutter/flutter#186246) 2026-09-02 engine-flutter-autoroll@skia.org Roll Skia from 63d13ec4df6d to 3a242fb7cdb3 (9 revisions) (flutter/flutter#192138) 2026-09-02 bkonyi@google.com [flutter_tools] Add --no-plugins flag and disable plugins for benchmark (flutter/flutter#192129) 2026-09-02 bkonyi@google.com [tool] Migrate ChannelCommand to modular dependency injection (flutter/flutter#190751) 2026-09-01 46920873+gabrimatic@users.noreply.github.com Add mouseCursor to RawScrollbar (flutter/flutter#185750) 2026-09-01 ksanaullah383.khan@gmail.com Fix self-comparison and assert typos in TestSemantics (flutter/flutter#191938) 2026-09-01 robert.ancell@canonical.com [Linux] Wait for frames to be rendered before using them in another context (flutter/flutter#192094) 2026-09-01 jacksongardner@google.com [wimp] Turn on multithreading for wimp. (flutter/flutter#191747) 2026-09-01 engine-flutter-autoroll@skia.org Roll Skia from 3911a1fe7f7a to 63d13ec4df6d (6 revisions) (flutter/flutter#192125) 2026-09-01 jason-simmons@users.noreply.github.com Remove the redundant flutter/shell/platform/android:robolectric_tests build target (flutter/flutter#192117) 2026-09-01 AfzalivE@users.noreply.github.com [iOS] Preserve semantics parents after reparenting (flutter/flutter#189686) 2026-09-01 bkonyi@google.com [flutter_tools] Report preview reload timing analytics in LspPreviewDetector (flutter/flutter#192120) 2026-09-01 bkonyi@google.com [flutter_tools] Implement Templates slice and flutter create integration (flutter/flutter#191748) 2026-09-01 louisehsu@google.com Uiscene migrate add2app hosts (flutter/flutter#191847) 2026-09-01 engine-flutter-autoroll@skia.org Roll Packages from d642322 to 7a7912f (8 revisions) (flutter/flutter#192119) 2026-09-01 zhongliu88889@gmail.com [web] Anchor flt-semantics-host at 0,0 to fix WebKit semantics offset (flutter/flutter#190486) 2026-09-01 jason-simmons@users.noreply.github.com Replace Shell::WaitForFirstFrame with an asynchronous API that matches the Shell threading model (flutter/flutter#191841) If this roll has caused a breakage, revert this CL and stop the roller using the controls here: https://autoroll.skia.org/r/flutter-packages Please CC stuartmorgan@google.com on the revert to ensure that a human is aware of the problem. To file a bug in Packages: https://github.com/flutter/flutter/issues/new/choose To report a problem with the AutoRoller itself, please file a bug: https://issues.skia.org/issues/new?component=1389291&template=1850622 Documentation for the AutoRoller is here: https://skia.googlesource.com/buildbot/+doc/main/autoroll/README.md
…r#12727) flutter/flutter@0cbd1a4...70797e1 2026-09-02 bkonyi@google.com [analysis] Upgrade package:analyzer to 14.3.0 and enable custom plugin compilation (flutter/flutter#191590) 2026-09-02 engine-flutter-autoroll@skia.org Roll Dart SDK from d8d7869d8031 to 9a8fbde80f6c (1 revision) (flutter/flutter#192179) 2026-09-02 engine-flutter-autoroll@skia.org Roll Skia from dec311244db4 to ae22da2e8308 (1 revision) (flutter/flutter#192177) 2026-09-02 bkonyi@google.com [tool] Migrate DoctorCommand to modular dependency injection (flutter/flutter#190758) 2026-09-02 engine-flutter-autoroll@skia.org Roll Skia from 482b7e285244 to dec311244db4 (3 revisions) (flutter/flutter#192161) 2026-09-02 engine-flutter-autoroll@skia.org Roll Skia from 28447e9fb8d5 to 482b7e285244 (3 revisions) (flutter/flutter#192157) 2026-09-02 34465683+rkishan516@users.noreply.github.com feat: Add placeholder to DecorationImage (flutter/flutter#191528) 2026-09-02 papmodern14@gmail.com [Android] Do not schedule engine frames on a detached FlutterJNI (flutter/flutter#191204) 2026-09-02 engine-flutter-autoroll@skia.org Roll Skia from 3a242fb7cdb3 to 28447e9fb8d5 (1 revision) (flutter/flutter#192145) 2026-09-02 engine-flutter-autoroll@skia.org Roll Dart SDK from 9164def35347 to d8d7869d8031 (2 revisions) (flutter/flutter#192144) 2026-09-02 74458687+anazr9@users.noreply.github.com Add FadeInImageTransition.fadeInOver to fade the image in over the placeholder (flutter/flutter#186246) 2026-09-02 engine-flutter-autoroll@skia.org Roll Skia from 63d13ec4df6d to 3a242fb7cdb3 (9 revisions) (flutter/flutter#192138) 2026-09-02 bkonyi@google.com [flutter_tools] Add --no-plugins flag and disable plugins for benchmark (flutter/flutter#192129) 2026-09-02 bkonyi@google.com [tool] Migrate ChannelCommand to modular dependency injection (flutter/flutter#190751) 2026-09-01 46920873+gabrimatic@users.noreply.github.com Add mouseCursor to RawScrollbar (flutter/flutter#185750) 2026-09-01 ksanaullah383.khan@gmail.com Fix self-comparison and assert typos in TestSemantics (flutter/flutter#191938) 2026-09-01 robert.ancell@canonical.com [Linux] Wait for frames to be rendered before using them in another context (flutter/flutter#192094) 2026-09-01 jacksongardner@google.com [wimp] Turn on multithreading for wimp. (flutter/flutter#191747) 2026-09-01 engine-flutter-autoroll@skia.org Roll Skia from 3911a1fe7f7a to 63d13ec4df6d (6 revisions) (flutter/flutter#192125) 2026-09-01 jason-simmons@users.noreply.github.com Remove the redundant flutter/shell/platform/android:robolectric_tests build target (flutter/flutter#192117) 2026-09-01 AfzalivE@users.noreply.github.com [iOS] Preserve semantics parents after reparenting (flutter/flutter#189686) 2026-09-01 bkonyi@google.com [flutter_tools] Report preview reload timing analytics in LspPreviewDetector (flutter/flutter#192120) 2026-09-01 bkonyi@google.com [flutter_tools] Implement Templates slice and flutter create integration (flutter/flutter#191748) 2026-09-01 louisehsu@google.com Uiscene migrate add2app hosts (flutter/flutter#191847) 2026-09-01 engine-flutter-autoroll@skia.org Roll Packages from d642322 to 7a7912f (8 revisions) (flutter/flutter#192119) 2026-09-01 zhongliu88889@gmail.com [web] Anchor flt-semantics-host at 0,0 to fix WebKit semantics offset (flutter/flutter#190486) 2026-09-01 jason-simmons@users.noreply.github.com Replace Shell::WaitForFirstFrame with an asynchronous API that matches the Shell threading model (flutter/flutter#191841) If this roll has caused a breakage, revert this CL and stop the roller using the controls here: https://autoroll.skia.org/r/flutter-packages Please CC stuartmorgan@google.com on the revert to ensure that a human is aware of the problem. To file a bug in Packages: https://github.com/flutter/flutter/issues/new/choose To report a problem with the AutoRoller itself, please file a bug: https://issues.skia.org/issues/new?component=1389291&template=1850622 Documentation for the AutoRoller is here: https://skia.googlesource.com/buildbot/+doc/main/autoroll/README.md
In flutter#191841, we replaced `Shell::WaitForFirstFrame` with `Shell::AddFirstFrameCallback` but didn't update the test names to match the new API. In particular, `WaitForFirstFrameTimeout` no longer tested that any caller-visible timeout fired, since the new API manages the timeout internally. The test now just verifies the first frame callback doesn't fire when no frame has rendered. Just naming changes, no semantic changes. Issue: b/521830222 ## Pre-launch Checklist - [X] I read the [Contributor Guide] and followed the process outlined there for submitting PRs. - [X] I read the [AI contribution guidelines] and understand my responsibilities, or I am not using AI tools. - [X] I read the [Tree Hygiene] wiki page, which explains my responsibilities. - [X] I read and followed the [Flutter Style Guide], including [Features we expect every widget to implement]. - [X] I signed the [CLA]. - [X] I listed at least one issue that this PR fixes in the description above. - [X] I updated/added relevant in-code documentation (doc comments with `///`). - [X] If this PR introduces a new feature or capability, I created and linked a website documentation issue or PR in [flutter/website] (or verified none is needed). - [X] I added new tests to check the change I am making, or this PR is [test-exempt]. - [X] I followed the [breaking change policy] and added [Data Driven Fixes] where supported. - [X] All existing and new tests are passing. If you need help, consider asking for advice on the #hackers-new channel on [Discord]. If this change needs to override an active code freeze, provide a comment explaining why. The code freeze workflow can be overridden by code reviewers. See pinned issues for any active code freezes with guidance. **Note**: The Flutter team is currently trialing the use of [Gemini Code Assist for GitHub](https://developers.google.com/gemini-code-assist/docs/review-github-code). Comments from the `gemini-code-assist` bot should not be taken as authoritative feedback from the Flutter team. If you find its comments useful you can update your code accordingly, but if you are unsure or disagree with the feedback, please feel free to wait for a Flutter team member's review for guidance on which automated comments should be addressed. <!-- Links --> [Contributor Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview [AI contribution guidelines]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#ai-contribution-guidelines [Tree Hygiene]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md [test-exempt]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests [Flutter Style Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md [Features we expect every widget to implement]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement [CLA]: https://cla.developers.google.com/ [flutter/tests]: https://github.com/flutter/tests [flutter/website]: https://github.com/flutter/website [breaking change policy]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes [Discord]: https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md [Data Driven Fixes]: https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md

The Shell::WaitForFirstFrame API would block until the engine renders the first frame. This could potentially cause issues if the shell is destroyed while a thread is waiting within a call to WaitForFirstFrame.
This PR replaces WaitForFirstFrame with an AddFirstFrameCallback API that invokes a callback when the first frame has been rendered. If the shell is destroyed before the first frame, then the callback will never be executed.
See #190132