-
Notifications
You must be signed in to change notification settings - Fork 28.9k
Description
Update 5/20/2025
As of Flutter 3.32 stable, threads are merged by default on iOS and Android with a flag available to opt-out. In Flutter 3.33 beta changes have landed so that threads will also be merged by default on Windows and macOS. Work to merge threads on Linux is WIP. We expect that the ability to opt-out of merged threads will be removed in a release later this year once #163064 has been sufficiently addressed.
Currently Flutter has a separate UI thread (that runs dart code) and platform thread. I think the main purpose of the UI thread is to prevent blocking the platform thread, but the actual benefit of this eludes me. In most applications the platform thread stays idle apart from tiny amount of work during event processing and compositing. The work is moved to UI thread, but if the work doesn't complete in time, it causes jank just like it would when blocking the platform thread.
The drawbacks however seem pretty clear. The vast majority of platform APIs need to be accessed on platform thread. This means that instead of being able to call the API from Dart directly (i.e. through FFI), platform channels or something similar need to be used to marshal the call to platform thread. This adds some friction and asynchronous behavior to what could otherwise be simple synchronous call.
Situation gets even more complicated the other way around - when needed to call into Dart from platform thread. Platform callbacks are often synchronous (i.e. accessibility, event handling, IME, application delegate, and many more). Traditionally the approach here was to push enough state to platform thread to be able to provide synchronous response, but that's not always feasible or reliable enough. Eventual consistency fails in some cases, for example we're getting bitten by issue where IME on iOS generate fast sequence of events and then reads the text before the events have been processed by UI thread and sent back to platform thread.
On iOS for keyboard handling we need synchronous response, and to get it we keep pumping the CFRunLoop inside the event handler until we get it. This is a feasible approach for the problems above, but it's a tricky solution because of added reentrancy (the system code invoking the handler may not expect the runloop to be running while waiting for response) and edge cases (what if the isolate gets terminated while waiting for response?). With enough effort these are fixable (i.e. platform channels can have custom run loop mode (iOS, macOS) or custom execution so that it doesn't interfere with system run loop), but still, we're solving problem that probably shouldn't have existed in the first place.
Non-exhaustive list of issues that would improve with merged UI and Platform thread
- Calling platform APIs directly and synchronously from Dart without needing platform channels or platform isolate
- Ability to get synchronous response from Dart code for platform callbacks, which would help with
- accessibility
- event handling (keyboard events, drag & drop)
- IME (cursor position query, no need to replicate composited text, could properly support custom bindings in
DefaultKeyBinding.dict
, ...) - Ability to synchronously respond to platform callbacks (i.e. window draggable area,
NSApplicationDelegate
methods)
Possible drawbacks
- Non trivial change, would likely impact ecosystem (some plugins may need to get adapted, though most of plugins might just work if we keep platform channels working asynchronously)
- Possible performance regression with thread merging and shared raster/platform thread (linux). I think the proper solution here is to get rid of thread merging (adopt solution similar to what macOS is doing on all platforms) and implement separate raster thread in Linux embedder.