Skip to content

[iOS] Migrate to FlutterFMLTaskRunner(s)#185815

Merged
cbracken merged 6 commits into
flutter:masterfrom
cbracken:use-flutterfmltaskrunners
May 1, 2026
Merged

[iOS] Migrate to FlutterFMLTaskRunner(s)#185815
cbracken merged 6 commits into
flutter:masterfrom
cbracken:use-flutterfmltaskrunners

Conversation

@cbracken

Copy link
Copy Markdown
Member

Migrates all direct users of the C++ fml::TaskRunner and flutter::TaskRunners to the Obj-C FlutterFMLTaskRunner and FlutterFMLTaskRunners equivalents.

This is part of a series of refactorings that aim to place a thin, lightweight layer of abstraction between the core C++ engine and the iOS embedder. Core to this effort are VSyncWaiterIOS, PlatformViewIOS, and IOSExternalViewEmbedder.

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

@cbracken cbracken requested a review from a team as a code owner April 30, 2026 08:12
@flutter-dashboard flutter-dashboard Bot added the CICD Run CI/CD label Apr 30, 2026
@github-actions github-actions Bot added platform-ios iOS applications specifically engine flutter/engine related. See also e: labels. a: accessibility Accessibility, e.g. VoiceOver or TalkBack. (aka a11y) team-ios Owned by iOS platform team labels Apr 30, 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 refactors the iOS platform code to utilize FlutterFMLTaskRunner wrappers instead of fml::RefPtrfml::TaskRunner in classes such as FlutterEngine, FlutterPlatformViewsController, and FlutterVSyncClient. It introduces a FlutterEngine (TaskRunners) category and new test helpers. Feedback indicates that public members in FlutterEngine+TaskRunners.h and FlutterPlatformViewsTestHelper.h lack required documentation, and suggests removing redundant method declarations from FlutterEngine_Internal.h.

Migrates all direct users of the C++ fml::TaskRunner and
flutter::TaskRunners to the Obj-C FlutterFMLTaskRunner and
FlutterFMLTaskRunners equivalents.

This is part of a series of refactorings that aim to place a thin,
lightweight layer of abstraction between the core C++ engine and the iOS
embedder. Core to this effort are VSyncWaiterIOS, PlatformViewIOS, and
IOSExternalViewEmbedder.

flutter#112232
@cbracken cbracken force-pushed the use-flutterfmltaskrunners branch from 1938d76 to 751f56d Compare April 30, 2026 08:33
@github-actions github-actions Bot removed the CICD Run CI/CD label Apr 30, 2026
@cbracken cbracken added the CICD Run CI/CD label Apr 30, 2026
@github-actions github-actions Bot removed the CICD Run CI/CD label Apr 30, 2026
@cbracken cbracken added the CICD Run CI/CD label Apr 30, 2026
for (int64_t viewId : self.compositionOrder) {
std::vector<int64_t> compositionOrder = self.compositionOrder;
[self.taskRunner runNowOrPostTask:^{
for (int64_t viewId : compositionOrder) {

@cbracken cbracken Apr 30, 2026

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

note: we intentionally pass a copy of compositionOrder by value here since the first thing we do after tossing this in the task queue is clear it. Passing by reference would result in a loop over nothing.

The review bot seems to really want to mechanically tell you to pass self as weakSelf, and do a check in the closure to capture it strongly. Fortunately it didn't on this pass. That would introduce a serious bug. The whole point of the -reset method is to remove any platform views from their superviews. We need that to happen whether or not FlutterPlatformViewsController is around (it almost always will be) or not (race condition during Flutter teardown).

The correct solution is to pass the set of platformviews to remove by value, as I've done here.

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.

I guess it worked before because of merged raster/platform thread, so that runNowOrPostTask runs immediately, before executing self.platformViews.clear()

viewsToRecomposite = self.viewsToRecomposite, //
compositionOrder = self.compositionOrder, //
unusedLayers = std::move(unusedLayers), //
surfaceFrames = std::move(surfaceFrames)]() mutable {

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

See below; this isn't a change -- I've just saved a line by doing fml::MakeCopyable here instead of below.

[self createLayerWithIosContext:iosContext
pixelFormat:((FlutterView*)self.flutterView).pixelFormat];
}
latch->CountDown();

@cbracken cbracken Apr 30, 2026

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

The strong self-capture in this block is safe (and required); this code is effectively synchronous. Notice that right below this we call latch->Wait() to block.

We don't retain this block and it finishes executing right below with the latch, so no extra retention.

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.

Can you put this in a comment? (In case someone "fix" the retain cycle into a wrong state.

/*raster=*/GetDefaultTaskRunner(),
/*ui=*/GetDefaultTaskRunner(),
/*io=*/GetDefaultTaskRunner());
FlutterFMLTaskRunners* runners = CreateTestTaskRunners(self.name);

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Consolidating this in a helper class isn't strictly-speaking required for this but this code was duplicated in basically every test case, so I think worth doing.

- (void)attachView;
@end

@implementation FlutterEnginePartialMock

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Yes, I hate that we're using OCMock too. I have a followup that migrates us off this nonsense onto real objects (and a couple small fakes).

// begin frame event has processed. And this test is to expect that the callback
// will sync with UI thread. So just simulate a lot of works on UI thread and
// test the keyboard animation callback will execute until UI task completed.
// test the keyboard animation callback will not execute until UI task completed.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Not a change; just fixing a typo.

[self waitForExpectationsWithTimeout:5.0 handler:nil];
XCTAssertTrue(fulfillTime - startTime > delayTime);
NSTimeInterval epsilon = 0.005;
XCTAssertGreaterThanOrEqual(fulfillTime - startTime, delayTime - epsilon);

@cbracken cbracken Apr 30, 2026

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

This is a semantic change, but a reasonable one based on @hellohuanlin's feedback on a previous patch. There are a couple potential sources of error with this sort of thing:

  1. If delayTime is not an inverse power of two, there's floating point truncation error.
  2. In the insanely rare case where an NTP clock sync happens during the delay, the wall clock difference might be different from the actual delay interval.


XCTAssertNotNil(manager.keyboardAnimationVSyncClient);
fml::MessageLoop::GetCurrent().RunExpiredTasksNow();
[manager invalidate];

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Cleanup that should have been here before.

- (instancetype)initWithTaskRunner:(fml::RefPtr<fml::TaskRunner>)task_runner
callback:(flutter::VsyncWaiter::Callback)callback;
- (instancetype)initWithTaskRunnerPtr:(fml::RefPtr<fml::TaskRunner>)task_runner
callback:(flutter::VsyncWaiter::Callback)callback;

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

This will go away in the future.

callback(start_time_seconds, target_time_seconds);
};
return [self initWithTaskRunnerPtr:runner callback:cpp_callback];
}

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Eventually this will be the only initializer.

auto runner = thread->GetTaskRunner();
return runner;
}
} // namespace

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Moved to FlutterVSyncClientTestHelper.

[self waitForExpectations:@[ vsyncExpectation ] timeout:1.0];

// Invalidate the client. This removes the CADisplayLink from the run loop
// and breaks the CADisplayLink -> FlutterVSyncClient retain cycle.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

I've added a bunch of comments with rationale because the behaviour of CADisplayLink and retention here is more complex than was initially obvious to me. Happy to remove them if they seem extraneous but this was kind of painful to work out, so seemed useful to leave breadcrumbs for others.

@cbracken cbracken force-pushed the use-flutterfmltaskrunners branch from 98294f5 to 027987c Compare April 30, 2026 11:12
@github-actions github-actions Bot removed the CICD Run CI/CD label Apr 30, 2026
@cbracken cbracken force-pushed the use-flutterfmltaskrunners branch from 027987c to 85f79f3 Compare April 30, 2026 11:19
@flutter-dashboard flutter-dashboard Bot added the CICD Run CI/CD label Apr 30, 2026
hellohuanlin
hellohuanlin previously approved these changes May 1, 2026
XCTAssertEqual(engine.rasterTaskRunner.get(), nullptr);
XCTAssertNil(engine.platformTaskRunner);
XCTAssertNil(engine.uiTaskRunner);
XCTAssertNil(engine.rasterTaskRunner);

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.

nice

XCTAssertNotNil(engine.uiTaskRunner);
XCTAssertEqual(engine.uiTaskRunner, engine.uiTaskRunner);
XCTAssertNotNil(engine.rasterTaskRunner);
XCTAssertEqual(engine.rasterTaskRunner, engine.rasterTaskRunner);

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.

maybe add a comment since the it reads a bit odd. what is this trying to verify?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

This is checking stable identity -- since these are wrappers, in theory we could create new throwaway wrapper each time we get it, but it's better to just do a single allocation per runner and keep em identical at the Obj-C level too.

#import "flutter/shell/platform/darwin/ios/framework/Source/FlutterFMLTaskRunner+FML.h"

// A FlutterFMLTaskRunner that owns the fml::Thread it runs on.
@interface FlutterOwnedThreadTaskRunner : FlutterFMLTaskRunner

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.

FlutterOwned sounds a bit unclear to me when I first read it. How about FakeFlutterFMLThreadTaskRunner?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

What about FlutterFMLThreadTaskRunner since it's a real task runner implemented around fml::Thread?

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.

I suggested Fake because it's used in test only, so maybe TestFlutterFMLTaskRunner? Naming is hard, up to you.

id<FlutterKeyboardInsetManagerDelegate> delegate = self.delegate;
auto vsyncCallback = ^(CFTimeInterval startTime, CFTimeInterval targetTime) {
CFTimeInterval frameInterval = targetTime - startTime;
CFTimeInterval projectedTargetTime = targetTime + frameInterval;

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.

good renaming

for (int64_t viewId : self.compositionOrder) {
std::vector<int64_t> compositionOrder = self.compositionOrder;
[self.taskRunner runNowOrPostTask:^{
for (int64_t viewId : compositionOrder) {

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.

I guess it worked before because of merged raster/platform thread, so that runNowOrPostTask runs immediately, before executing self.platformViews.clear()

// complete.
auto latch = std::make_shared<fml::CountDownLatch>(1u);
fml::TaskRunner::RunNowOrPostTask(
self.platformTaskRunner, [self, missingLayerCount, iosContext, latch]() {

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.

love to see these C++ closures gone. ObjC syntax looks so much more familiar to an iOS engineer!

[self createLayerWithIosContext:iosContext
pixelFormat:((FlutterView*)self.flutterView).pixelFormat];
}
latch->CountDown();

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.

Can you put this in a comment? (In case someone "fix" the retain cycle into a wrong state.

/**
* Returns a task runners wrapper where all task runners map to the default test thread runner.
*/
FlutterFMLTaskRunners* CreateTestTaskRunners(NSString* label);

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.

the first 2 funcs seems to be sharable to non-platform view tests in the future? Maybe move them to a separate file?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Seems reasonable; unless any objection, I'll do that in a followup. Probably a good fit for the FlutterFMLTaskRunnerTestHelpers.h except they're C++ and that file should be pure Obj-C. Will find a spot for them and move them.

NSTimeInterval epsilon = 0.005;
XCTAssertGreaterThanOrEqual(fulfillTime - startTime, delayTime - epsilon);
fml::MessageLoop::GetCurrent().RunExpiredTasksNow();
[(id)mockCADisplayLink stopMocking];

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.

why cast here?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Good point -- totally unnecessary. I was following the same approach as on line 609 where it was necessary. Removed.


#import "flutter/shell/platform/darwin/ios/framework/Source/FlutterVSyncClient.h"

#import <QuartzCore/QuartzCore.h>

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.

added by accident?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Thanks for spotting -- all these patches were carved out of one big mega-patch and this was probably leftover copied from the main (Obj-C header) which includes DisplayLinkManager (that holds a CADisplayLink). Removed.

@github-actions github-actions Bot removed the CICD Run CI/CD label May 1, 2026
@hellohuanlin hellohuanlin added the CICD Run CI/CD label May 1, 2026
@cbracken cbracken enabled auto-merge May 1, 2026 01:48
@github-actions github-actions Bot removed the CICD Run CI/CD label May 1, 2026
@cbracken cbracken added the CICD Run CI/CD label May 1, 2026
@cbracken cbracken added this pull request to the merge queue May 1, 2026
Merged via the queue into flutter:master with commit 87e2f2c May 1, 2026
197 checks passed
@cbracken cbracken deleted the use-flutterfmltaskrunners branch May 1, 2026 08:19
auto-submit Bot pushed a commit to flutter/packages that referenced this pull request May 1, 2026
Roll Flutter from 81bc3d69535f to 707dbc0420a3 (85 revisions)

flutter/flutter@81bc3d6...707dbc0

2026-05-01 [email protected] Removing TODOs from the WebParagraph code and addressing technical debts. (flutter/flutter#185168)
2026-05-01 [email protected] Ensure that vulkan_interface.h gets included before vk_mem_alloc.h (flutter/flutter#185777)
2026-05-01 [email protected] [flutter_tools] Bump dwds dependency to v27.1.1 (flutter/flutter#185357)
2026-05-01 [email protected] Roll Dart SDK from 6d4a319cbdac to 9aa7097f2e3e (3 revisions) (flutter/flutter#185888)
2026-05-01 [email protected] Roll Skia from fa1dcb289709 to 7ac6d42f2fd0 (1 revision) (flutter/flutter#185887)
2026-05-01 [email protected] Roll Skia from 54cc00adde38 to fa1dcb289709 (3 revisions) (flutter/flutter#185880)
2026-05-01 [email protected] [iOS] Migrate to FlutterFMLTaskRunner(s) (flutter/flutter#185815)
2026-05-01 [email protected] Remove material imports from navigator_on_did_remove_page_test and scrollable_in_overlay_test (flutter/flutter#182546)
2026-05-01 [email protected] Roll Skia from 2e279266f06a to 54cc00adde38 (3 revisions) (flutter/flutter#185872)
2026-05-01 [email protected] dev: Remove unused parameters (flutter/flutter#185345)
2026-05-01 [email protected] Roll Fuchsia Linux SDK from HN5VYzftnf_B8T-n9... to VnzuUefDQR0UhQ1L1... (flutter/flutter#185870)
2026-05-01 [email protected] Use g_free when using glib memory allocation (flutter/flutter#185519)
2026-05-01 [email protected] Roll Dart SDK from d30df3428f2e to 6d4a319cbdac (2 revisions) (flutter/flutter#185862)
2026-05-01 [email protected] Remove trivial test utility cross-imports from material and cupertino… (flutter/flutter#184295)
2026-05-01 [email protected] Inline test callback painter in tab scaffold test (flutter/flutter#184851)
2026-05-01 [email protected] [a11y] Add support for high contrast themes in the a11y assessments app  (flutter/flutter#185801)
2026-05-01 [email protected] [a11y assessment app] Use default color for banner (flutter/flutter#185804)
2026-04-30 [email protected] Added name to AUTHORS (flutter/flutter#185586)
2026-04-30 [email protected] Remove semantics_tester import from raw_material_button_test.dart (flutter/flutter#184806)
2026-04-30 [email protected] Remove semantics_tester import from user_accounts_drawer_header_test.dart (flutter/flutter#184809)
2026-04-30 [email protected] Roll Skia from 7b88c5c281e5 to 2e279266f06a (5 revisions) (flutter/flutter#185854)
2026-04-30 [email protected] Handle symmetric rectangular and elliptical round super ellipses in the uber SDF renderer  (flutter/flutter#185695)
2026-04-30 [email protected] Match on process name before killing for SwiftPM (flutter/flutter#185774)
2026-04-30 [email protected] Sync CHANGELOG.md from stable (flutter/flutter#185838)
2026-04-30 [email protected] Roll Dart SDK from 25910e31a6d2 to d30df3428f2e (5 revisions) (flutter/flutter#185839)
2026-04-30 [email protected] Check cross imports test subfolders (flutter/flutter#185493)
2026-04-30 [email protected] test: inline TestCallbackPainter in cupertino/picker_test.dart (flutter/flutter#185398)
2026-04-30 [email protected] Update customer testing version (flutter/flutter#185830)
2026-04-30 [email protected] Adapt the Metal shader library output list for debug versus release mode (flutter/flutter#185798)
2026-04-30 [email protected] [Impeller] Port a recent Vulkan swapchain fence waiting fix to the AHB version of the swapchain (flutter/flutter#185763)
2026-04-30 [email protected] Roll Skia from 26a59aa71eff to 7b88c5c281e5 (1 revision) (flutter/flutter#185821)
2026-04-30 [email protected] Roll Skia from 6b4167b4e204 to 26a59aa71eff (4 revisions) (flutter/flutter#185808)
2026-04-30 [email protected] [a11y] Mark SemanticsNode dirty when customSemanticsActions change  (flutter/flutter#185707)
2026-04-30 [email protected] Roll Skia from 1bd2f1cc2746 to 6b4167b4e204 (8 revisions) (flutter/flutter#185799)
2026-04-30 [email protected] [iOS] Extract FlutterVSyncClient from vsync_waiter_ios (flutter/flutter#185737)
2026-04-30 [email protected] Roll Fuchsia Linux SDK from nnv8-SSam6yE8dw4z... to HN5VYzftnf_B8T-n9... (flutter/flutter#185782)
2026-04-29 [email protected] [iOS] Soften TaskRunner.postTask(delay:task:) delay check (flutter/flutter#185729)
2026-04-29 [email protected] Add reportErrors to ImageStreamListener (flutter/flutter#180327)
2026-04-29 [email protected] Roll Skia from f5c781c083c7 to 1bd2f1cc2746 (5 revisions) (flutter/flutter#185761)
2026-04-29 [email protected] Update merge semantics logic to merge sibling nodes (flutter/flutter#183745)
2026-04-29 [email protected] Roll Packages from ba80f8f to cde5b36 (12 revisions) (flutter/flutter#185748)
2026-04-29 [email protected] examples: Remove unused parameters (flutter/flutter#185346)
2026-04-29 [email protected] Update TickerMode.getValuesNotifier to handle initialization during State.dispose (flutter/flutter#185248)
2026-04-29 [email protected] Update triage links (flutter/flutter#185714)
2026-04-29 [email protected] Add support for high contrast and color inversion on Android (flutter/flutter#182263)
2026-04-29 [email protected] Roll Skia from 05251260fda6 to f5c781c083c7 (2 revisions) (flutter/flutter#185743)
...
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

a: accessibility Accessibility, e.g. VoiceOver or TalkBack. (aka a11y) 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