Skip to content

iOS: Reject merged-platform-ui-thread=mergeAfterLaunch - #190051

Merged
cbracken merged 1 commit into
flutter:masterfrom
cbracken:embedder-pw-4
Jul 28, 2026
Merged

cbracken merged 1 commit into
flutter:masterfrom
cbracken:embedder-pw-4

Conversation

@cbracken

Copy link
Copy Markdown
Member

iOS runs with the platform and UI threads merged from launch and only implements fully-merged (kEnabled) threading. The mergeAfterLaunch approach starts the engine on a separate UI thread and merges it onto the platform thread once the root isolate is running.

The mergeAfterLaunch merge/unmerge code lives entirely in shell/common. The iOS embedder has never implemented it, and no iOS code has ever referenced it. On iOS, the flag was never wired up, and it isn't reachable through any supported configuration. The FLTEnableMergedPlatformUIThread Info.plist key only ever selects kEnabled or kDisabled.

The only way to reach it is the raw command-line flag, which nothing rejects today, so passing --merged-platform-ui-thread=mergeAfterLaunch slips through and produces a broken engine since:

  • Shell::Spawn refuses kMergeAfterLaunch and returns null, so FlutterEngineGroup fails to spawn secondary engines outright.
  • The CADisplayLink-backed vsync waiter registers on the run loop of whichever thread services the UI task runner when it's constructed. Under the merged model iOS actually ships (kEnabled) that's the platform thread, which is correct. Under mergeAfterLaunch it would be the temporary UI thread that the model abandons after launch, and which won't work.

This has been unsupported since mergeAfterLaunch was introduced.

Since every iOS Settings setting passes through FlutterDartProject, we can fail startup there with an FML_CHECK if this configuration is set. Normally this would live in shell/common/switches.cc but Android also passes require_merged_platform_ui_thread to SettingsFromCommandLine and still supports mergeAfterLaunch as a launch-latency optimization used by internal customers.

Also added a test that hints that mergeAfterLaunch is an intentional, supported configuration so that people like me don't try to lock it down again. See: #189893

This is part of cleanup work intended to simplify the iOS embedder prior to an eventual migration to the embedder API.

Issue: #112232

Pre-launch Checklist

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. 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.

iOS runs with the platform and UI threads merged from launch and only
implements fully-merged (`kEnabled`) threading. The `mergeAfterLaunch`
approach starts the engine on a separate UI thread and merges it onto
the platform thread once the root isolate is running.

The `mergeAfterLaunch` merge/unmerge code lives entirely in
`shell/common`. The iOS embedder has never implemented it, and no iOS
code has ever referenced it. On iOS, the flag was never wired up, and it
isn't reachable through any supported configuration. The
`FLTEnableMergedPlatformUIThread` Info.plist key only ever selects
`kEnabled` or `kDisabled`.

The only way to reach it is the raw command-line flag, which nothing
rejects today, so passing `--merged-platform-ui-thread=mergeAfterLaunch`
slips through and produces a broken engine since:

* `Shell::Spawn` refuses `kMergeAfterLaunch` and returns null, so
  `FlutterEngineGroup` fails to spawn secondary engines outright.
* The CADisplayLink-backed vsync waiter registers on the run loop of
  whichever thread services the UI task runner when it's constructed.
  Under the merged model iOS actually ships (`kEnabled`) that's the
  platform thread, which is correct. Under `mergeAfterLaunch` it would
  be the temporary UI thread that the model abandons after launch, and
  which won't work.

This has been unsupported since `mergeAfterLaunch` was introduced.

Since every iOS `Settings` setting passes through `FlutterDartProject`,
we can fail startup there with an `FML_CHECK` if this configuration is
set. Normally this would live in `shell/common/switches.cc` but Android
also passes `require_merged_platform_ui_thread` to
`SettingsFromCommandLine` and still supports `mergeAfterLaunch` as a
launch-latency optimization used by internal customers.

Also added a test that hints that `mergeAfterLaunch` is an intentional,
supported configuration so that people like me don't try to lock it down
again. See: flutter#189893

This is part of cleanup work intended to simplify the iOS embedder prior
to an eventual migration to the embedder API.

Issue: flutter#112232
@cbracken
cbracken requested a review from jason-simmons July 27, 2026 04:05
@cbracken
cbracken requested a review from a team as a code owner July 27, 2026 04:05
@flutter-dashboard flutter-dashboard Bot added the CICD Run CI/CD label Jul 27, 2026
@github-actions github-actions Bot added platform-ios iOS applications specifically engine flutter/engine related. See also e: labels. team-ios Owned by iOS platform team labels Jul 27, 2026

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request adds a unit test to verify that the mergeAfterLaunch option for the merged-platform-ui-thread switch is correctly parsed. Additionally, it introduces an assertion in the iOS platform code to explicitly disallow this option on iOS, causing a crash if it is specified. There are no review comments, and I have no feedback to provide.

@cbracken
cbracken added this pull request to the merge queue Jul 28, 2026
Merged via the queue into flutter:master with commit 7870373 Jul 28, 2026
19 of 20 checks passed
@cbracken
cbracken deleted the embedder-pw-4 branch July 28, 2026 04:03
auto-submit Bot pushed a commit to flutter/packages that referenced this pull request Jul 28, 2026
flutter/flutter@9988960...0f02463

2026-07-28 [email protected] Roll Fuchsia Test Scripts from E8hJ1AfK8CtGtaES0... to 1frGe_KltAJKkeyPg... (flutter/flutter#190134)
2026-07-28 [email protected] iOS: Reject merged-platform-ui-thread=mergeAfterLaunch (flutter/flutter#190051)
2026-07-28 [email protected] iOS: Migrate TaskRunner tests to Swift Testing (flutter/flutter#190055)
2026-07-28 [email protected] Run Mac golden tests on ARM bots (flutter/flutter#189465)
2026-07-28 [email protected] iOS,macOS: Rename Swift test files to end in Tests.swift (flutter/flutter#190063)
2026-07-28 [email protected] Fix hcpp cliprect being behind by 1 frame when scrolling (flutter/flutter#189946)
2026-07-28 [email protected] Roll Fuchsia Linux SDK from vpboK5fPPIoFteqRq... to OZkZC_2CZ_G5rbMIS... (flutter/flutter#190115)
2026-07-27 [email protected] Add Ishaq Hassan to AUTHORS (flutter/flutter#190064)
2026-07-27 [email protected] [wimp] fixes ubo padding size issue (flutter/flutter#189958)
2026-07-27 [email protected] Roll pub packages (flutter/flutter#189872)
2026-07-27 [email protected] Move tool host_cross_arch tests into different shards (flutter/flutter#189470)
2026-07-27 49699333+dependabot[bot]@users.noreply.github.com Bump actions/labeler from 6.2.0 to 7.0.0 in the all-github-actions group (flutter/flutter#190099)
2026-07-27 [email protected] [ios]do not nuke user input path when running uiscene integration test (flutter/flutter#186436)
2026-07-27 [email protected] ci: verify_binaries_pre_codesigned part 2 (flutter/flutter#190078)
2026-07-27 [email protected] Roll Abseil to ff6e8ce3e932 (flutter/flutter#189998)
2026-07-27 [email protected] Android_hardware_smoke_test: clean up golden copy in CI (flutter/flutter#189948)

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 [email protected] 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
cbracken added a commit to cbracken/flutter that referenced this pull request Aug 2, 2026
-waitForFirstFrameSync: is unconditionally a no-op on iOS, and that is
enforced by the tree rather than by convention. The only constructible
threading configuration merges the platform and UI task runners onto
the platform thread (`FlutterDartProject.mm` passes
`require_merged_platform_ui_thread` to `SettingsFromCommandLine`, and
flutter#190051 rejects `mergeAfterLaunch` as well), so the
thread calling this method at first layout is the thread responsible
for producing the frame, and `Shell::WaitForFirstFrame` always takes
its `kFailedPrecondition` early return without waiting. Removes the
method and its only call site in FlutterViewController's first layout;
the surface creation that shared the call site is unchanged.

This was the main-thread wait introduced in flutter-team-archive/engine#9506 to
avoid presenting a black first frame when transitioning into a
FlutterViewController with a prewarmed engine (flutter#32937).
It has been inert since the platform and UI threads merged: a thread
cannot block waiting for a frame it must itself produce. If the black
flash proves reproducible under the merged configuration it will need
a mechanism other than blocking, e.g. deferring presentation until a
frame is available. Verified on a physical device (release mode) that
repeatedly presenting a FlutterViewController on a prewarmed engine
shows no flash of black with the wait removed.

No test changes: the wait and its call site had no coverage, and the
removed behaviour is unreachable in any constructible iOS
configuration.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CICD Run CI/CD engine flutter/engine related. See also e: labels. platform-ios iOS applications specifically team-ios Owned by iOS platform team

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants