[iOS] Migrate to FlutterFMLTaskRunner(s)#185815
Conversation
There was a problem hiding this comment.
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
1938d76 to
751f56d
Compare
| for (int64_t viewId : self.compositionOrder) { | ||
| std::vector<int64_t> compositionOrder = self.compositionOrder; | ||
| [self.taskRunner runNowOrPostTask:^{ | ||
| for (int64_t viewId : compositionOrder) { |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 { |
There was a problem hiding this comment.
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(); |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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. |
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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:
- If delayTime is not an inverse power of two, there's floating point truncation error.
- 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]; |
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
This will go away in the future.
| callback(start_time_seconds, target_time_seconds); | ||
| }; | ||
| return [self initWithTaskRunnerPtr:runner callback:cpp_callback]; | ||
| } |
There was a problem hiding this comment.
Eventually this will be the only initializer.
| auto runner = thread->GetTaskRunner(); | ||
| return runner; | ||
| } | ||
| } // namespace |
There was a problem hiding this comment.
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. |
There was a problem hiding this comment.
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.
98294f5 to
027987c
Compare
027987c to
85f79f3
Compare
| XCTAssertEqual(engine.rasterTaskRunner.get(), nullptr); | ||
| XCTAssertNil(engine.platformTaskRunner); | ||
| XCTAssertNil(engine.uiTaskRunner); | ||
| XCTAssertNil(engine.rasterTaskRunner); |
| XCTAssertNotNil(engine.uiTaskRunner); | ||
| XCTAssertEqual(engine.uiTaskRunner, engine.uiTaskRunner); | ||
| XCTAssertNotNil(engine.rasterTaskRunner); | ||
| XCTAssertEqual(engine.rasterTaskRunner, engine.rasterTaskRunner); |
There was a problem hiding this comment.
maybe add a comment since the it reads a bit odd. what is this trying to verify?
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
FlutterOwned sounds a bit unclear to me when I first read it. How about FakeFlutterFMLThreadTaskRunner?
There was a problem hiding this comment.
What about FlutterFMLThreadTaskRunner since it's a real task runner implemented around fml::Thread?
There was a problem hiding this comment.
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; |
| for (int64_t viewId : self.compositionOrder) { | ||
| std::vector<int64_t> compositionOrder = self.compositionOrder; | ||
| [self.taskRunner runNowOrPostTask:^{ | ||
| for (int64_t viewId : compositionOrder) { |
There was a problem hiding this comment.
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]() { |
There was a problem hiding this comment.
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(); |
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
the first 2 funcs seems to be sharable to non-platform view tests in the future? Maybe move them to a separate file?
There was a problem hiding this comment.
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]; |
There was a problem hiding this comment.
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> |
There was a problem hiding this comment.
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.
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) ...
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-assistbot 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.