[iOS] Migrate VSyncClient and DisplayLinkManager to Swift#187001
Conversation
|
Warning Gemini encountered an error creating the review. You can try again by commenting |
| /// | ||
| /// - Parameter requestedRate: The target maximum refresh rate in Hertz. | ||
| @objc | ||
| public func setMaxRefreshRate(_ requestedRate: Double) { |
There was a problem hiding this comment.
Difference: I've renamed refreshRate here to requestedRate since it was shadowing self.refreshRate. This eliminates that potential trap for future changes.
| /// this property falls back to `defaultRefreshRate` (60Hz). | ||
| @objc | ||
| public var refreshRate: Double { | ||
| return _refreshRate > 0.0 ? _refreshRate : VSyncClient.defaultRefreshRate |
There was a problem hiding this comment.
Difference: Previously, this was done while setting refreshRate in onDisplayLink() but moving it here makes that code a bit more readable and ensures that we're always returning a valid value here even if onDisplayLink() has never been called. Further, it centralises the logic so that if we ever use this elsewhere, we get a safe rate automatically rather than having to apply this check everywhere.
|
/gemini review |
There was a problem hiding this comment.
Code Review
This pull request migrates the iOS VSync and display link management from Objective-C to Swift, introducing VSyncClient.swift and DisplayLinkManager.swift while removing the legacy FlutterVSyncClient files. Feedback identifies a potential retain cycle in VSyncClient due to CADisplayLink strongly retaining its target and suggests simplifying the displayRefreshRate property in DisplayLinkManager by removing redundant logic.
| public class VSyncClient: NSObject { | ||
| private static let defaultRefreshRate: Double = 60.0 | ||
|
|
||
| private let isVariableRefreshRateEnabled: Bool | ||
| private let callback: (CFTimeInterval, CFTimeInterval) -> Void | ||
|
|
||
| /// The display link used to coordinate vsync callbacks. | ||
| @objc | ||
| internal private(set) var displayLink: CADisplayLink? | ||
|
|
||
| private var _refreshRate: Double = defaultRefreshRate | ||
|
|
||
| /// The current display refresh rate in Hertz, rounded to the nearest integer value. | ||
| /// | ||
| /// This value is calculated during each vsync callback as the inverse of the frame duration (the | ||
| /// time between the current frame and the target next frame). The resulting frequency is rounded | ||
| /// to the nearest whole number to smooth out minor hardware timestamp variations. | ||
| /// | ||
| /// If the current refresh rate is unknown or invalid (e.g., during startup or a paused transition), | ||
| /// this property falls back to `defaultRefreshRate` (60Hz). | ||
| @objc | ||
| public var refreshRate: Double { | ||
| return _refreshRate > 0.0 ? _refreshRate : VSyncClient.defaultRefreshRate | ||
| } | ||
|
|
||
| /// Default value is `true`. Vsync client will pause vsync callback after receiving a vsync | ||
| /// signal. Setting this property to `false` can avoid this and vsync client will trigger vsync | ||
| /// callback continuously. | ||
| @objc | ||
| public var allowPauseAfterVsync: Bool = true | ||
|
|
||
| /// Initializes the vsync client. | ||
| /// | ||
| /// - Parameters: | ||
| /// - taskRunner: The task runner to use for posting tasks. | ||
| /// - isVariableRefreshRateEnabled: Whether variable refresh rate should be enabled. | ||
| /// - maxRefreshRate: The maximum refresh rate to configure the display link with. | ||
| /// - callback: The callback to invoke when a vsync signal is received. | ||
| @objc | ||
| public init( | ||
| taskRunner: TaskRunner, | ||
| isVariableRefreshRateEnabled: Bool, | ||
| maxRefreshRate: Double, | ||
| callback: @escaping (CFTimeInterval, CFTimeInterval) -> Void | ||
| ) { | ||
| self.isVariableRefreshRateEnabled = isVariableRefreshRateEnabled | ||
| self._refreshRate = maxRefreshRate | ||
| self.callback = callback | ||
|
|
||
| super.init() | ||
|
|
||
| let link = CADisplayLink(target: self, selector: #selector(onDisplayLink(_:))) | ||
| link.isPaused = true | ||
| self.displayLink = link | ||
|
|
||
| setMaxRefreshRate(maxRefreshRate) | ||
|
|
||
| // Capture a weak reference to self to ensure we don't add the display link to the run loop if | ||
| // the client has already been deallocated. | ||
| taskRunner.postTask { [weak self] in | ||
| guard let self else { return } | ||
| self.displayLink?.add(to: .current, forMode: .common) | ||
| } | ||
| } |
There was a problem hiding this comment.
The current implementation of VSyncClient has a retain cycle. CADisplayLink strongly retains its target, and VSyncClient strongly retains the displayLink. Because of this cycle, deinit will never be called, and the memory will leak unless invalidate() is called manually.
In Swift, this is typically solved by using a private proxy object as the target of the CADisplayLink, which holds a weak reference back to the client.
public class VSyncClient: NSObject {
private static let defaultRefreshRate: Double = 60.0
private let isVariableRefreshRateEnabled: Bool
private let callback: (CFTimeInterval, CFTimeInterval) -> Void
/// The display link used to coordinate vsync callbacks.
@objc
internal private(set) var displayLink: CADisplayLink?
private var displayLinkProxy: DisplayLinkProxy?
private var _refreshRate: Double = defaultRefreshRate
/// The current display refresh rate in Hertz, rounded to the nearest integer value.
///
/// This value is calculated during each vsync callback as the inverse of the frame duration (the
/// time between the current frame and the target next frame). The resulting frequency is rounded
/// to the nearest whole number to smooth out minor hardware timestamp variations.
///
/// If the current refresh rate is unknown or invalid (e.g., during startup or a paused transition),
/// this property falls back to `defaultRefreshRate` (60Hz).
@objc
public var refreshRate: Double {
return _refreshRate > 0.0 ? _refreshRate : VSyncClient.defaultRefreshRate
}
/// Default value is `true`. Vsync client will pause vsync callback after receiving a vsync
/// signal. Setting this property to `false` can avoid this and vsync client will trigger vsync
/// callback continuously.
@objc
public var allowPauseAfterVsync: Bool = true
/// Initializes the vsync client.
///
/// - Parameters:
/// - taskRunner: The task runner to use for posting tasks.
/// - isVariableRefreshRateEnabled: Whether variable refresh rate should be enabled.
/// - maxRefreshRate: The maximum refresh rate to configure the display link with.
/// - callback: The callback to invoke when a vsync signal is received.
@objc
public init(
taskRunner: TaskRunner,
isVariableRefreshRateEnabled: Bool,
maxRefreshRate: Double,
callback: @escaping (CFTimeInterval, CFTimeInterval) -> Void
) {
self.isVariableRefreshRateEnabled = isVariableRefreshRateEnabled
self._refreshRate = maxRefreshRate
self.callback = callback
super.init()
let proxy = DisplayLinkProxy(target: self)
self.displayLinkProxy = proxy
let link = CADisplayLink(target: proxy, selector: #selector(DisplayLinkProxy.onDisplayLink(_:)))
link.isPaused = true
self.displayLink = link
setMaxRefreshRate(maxRefreshRate)
// Capture a weak reference to self to ensure we don't add the display link to the run loop if
// the client has already been deallocated.
taskRunner.postTask { [weak self] in
guard let self else { return }
self.displayLink?.add(to: .current, forMode: .common)
}
}
private class DisplayLinkProxy: NSObject {
weak var target: VSyncClient?
init(target: VSyncClient) { self.target = target }
@objc func onDisplayLink(_ link: CADisplayLink) {
target?.onDisplayLink(link)
}
}
There was a problem hiding this comment.
Yes, the existing Obj-C code also had this retain cycle, and in fact this has existed all the way back to the original pure C++ implementation in the first versions of Flutter.
Because of this cycle, deinit will never be called, and the memory will leak unless invalidate() is called manually.
We do already call VSyncClient.invalidate in all code where we use this class.
That said, I do have a followup that introduces a private DisplayLinkRelay class and fixes this altogether, however this patch is big enough already and this is an existing issue in the code that is safely mitigated at every callsite, so not an issue in production. It is however, a fragile design, which is why I have a fix in a followup.
Followup for reference (it's even got a test): https://github.com/flutter/flutter/compare/master...cbracken:flutter:vsync-weak-relay?expand=1 (specifically the second commit here: dca7b2c)
There was a problem hiding this comment.
The full history for reference:
Commit 54ee61e (October 24, 2016) — "Migrate vsync away from Mojo services": The strong retain cycle was introduced here when it was first created. We were still using MRC and VSyncClient was a private class inside vsync_waiter_ios.mm. The client retains the CADisplayLink which strongly retains the client as its target. Note that we didn't manually invalidate it here, which was a bug!
Commit 5f95d3b (August 1, 2024) — "Migrate vsync_waiter_ios to ARC": The vsync waiter code was migrated to ARC. The circular reference and therefore the leak are still there.
Commit 6eea11a (April 30, 2026) — "[iOS] Extract FlutterVSyncClient from vsync_waiter_ios": A few weeks ago, I extracted the class to FlutterVSyncClient.mm. I noticed the circular reference and added a manual -[FlutterVSyncClient invalidate] method and specifically pointed out // Break retain cycle in there, where I unregister the display link from the run loop. Cleanup still isn't automatic, but at least it's happening now! This fixed the leak, but the code is still not as future-proof as it should be.
Commit d095fdee (May 2, 2026) (THIS PATCH!) — "[iOS] Migrate VSyncClient and DisplayLinkManager to Swift": This is this patch patch and I'm trying to stick to a 1:1 migration of these two classes Swift. We're still doing exactly the same invalidation as I introduced in the last patch -- just in Swift now.
Commit 83c2a7ff (May 23, 2026) (My upcoming patch I linked above) — "[iOS] Eliminate strong retain cycle from VSyncClient": This is my upcoming patch that introduces DisplayLinkRelay as a weak proxy, which breaks the retain cycle and we no longer need to worry about forgetting to call invalidate manually (though it's still a good idea to keep things 100% deterministic and clear it out when we want to).
I asked Gemini to generate Mermaid graphs of the retain graph before and after the upcoming patch.
Here's the current situation:
classDiagram
class Owner {
+FlutterVSyncClient client_
}
class VSyncClient {
+CADisplayLink displayLink
+onDisplayLink()
}
class CADisplayLink {
+id target
+SEL selector
}
class NSRunLoop {
+DisplayLink activeLinks
}
Owner --> VSyncClient : strong
VSyncClient --> CADisplayLink : strong (displayLink)
CADisplayLink --> VSyncClient : strong (target)
NSRunLoop --> CADisplayLink : strong (when active)
And here's what it'll look like after my patch:
classDiagram
class Owner {
+VSyncClient client_
}
class VSyncClient {
+CADisplayLink displayLink
+onDisplayLink()
}
class CADisplayLink {
+id target
}
class DisplayLinkRelay {
+weak VSyncClient target
+onDisplayLink()
}
class NSRunLoop {
+DisplayLink activeLinks
}
Owner --> VSyncClient : strong
VSyncClient --> CADisplayLink : strong (displayLink)
CADisplayLink --> DisplayLinkRelay : strong (target)
DisplayLinkRelay ..> VSyncClient : "weak (target)"
NSRunLoop --> CADisplayLink : strong (when active)
You might be asking about the strong NSRunLoop ownership. Calling invalidate() instructs the runloop to deregister it. That's why we manually call invalidate() today in all places we use it, but we also do it in deinit() (I added this in the previous commit) just to be 100% that if we forget to call it, it doesn't end up retained.
There was a problem hiding this comment.
Thanks for writing this up! I didn't know CADisplayLink retains its target.
There was a problem hiding this comment.
Yeah this was news to me too from poking at it a bunch in the previous patches, hence why I added the invalidate() calls.
Discussion on StackOverflow:
https://stackoverflow.com/questions/47368609/definitively-do-you-have-to-invalidate-a-cadisplaylink-when-the-controller-di
This article had a bunch of discussion of general Swift gotchas:
https://medium.com/fueled-engineering/memory-management-in-swift-common-issues-90dd7c08b77
Fortunately, we were never creating bajillions of these guys in the first place, but definitely nice to fix it.
| public static var displayRefreshRate: Double { | ||
| // TODO(cbracken): This code is incorrect. https://github.com/flutter/flutter/issues/185759 | ||
| // | ||
| // We create a new CADisplayLink, call `preferredFramesPerSecond` on it, then immediately throw | ||
| // it away. As noted below, the default value for `preferredFramesPerSecond` is zero, in which | ||
| // case, we just return UIScreen.main.maximumFramesPerSecond in all cases; everything before | ||
| // that line can be deleted. | ||
| // | ||
| // If we intend to support configurable preferred FPS, then we should provide API for it. We | ||
| // should delete this code either way. | ||
|
|
||
| let displayLink = CADisplayLink(target: self, selector: #selector(onDisplayLink(_:))) | ||
| displayLink.isPaused = true | ||
| let preferredFPS = displayLink.preferredFramesPerSecond | ||
|
|
||
| // From Docs: | ||
| // The default value for preferredFramesPerSecond is 0. When this value is 0, the preferred | ||
| // frame rate is equal to the maximum refresh rate of the display, as indicated by the | ||
| // maximumFramesPerSecond property. | ||
| if preferredFPS != 0 { | ||
| return Double(preferredFPS) | ||
| } | ||
|
|
||
| return Double(UIScreen.main.maximumFramesPerSecond) | ||
| } |
There was a problem hiding this comment.
As noted in the TODO and the linked issue, the current implementation of displayRefreshRate is redundant and incorrect. Creating a CADisplayLink just to read its default preferredFramesPerSecond (which is always 0 until set) is inefficient and doesn't provide the intended information. Since the fallback logic already handles returning the maximum refresh rate of the screen, the code creating the display link can be removed to simplify the property.
public static var displayRefreshRate: Double {
// TODO(cbracken): This code is incorrect. https://github.com/flutter/flutter/issues/185759
//
// If we intend to support configurable preferred FPS, then we should provide API for it.
// We should delete this code either way.
return Double(UIScreen.main.maximumFramesPerSecond)
}There was a problem hiding this comment.
This will be deleted two followups from now.
See this issue from when we last discussed this same feedback:
#185759
This also splits DisplayLinkManager to its own file. `(void)` casts needed to be added to OCMock usages of DisplayLinkManager because by default Swift warns on unused results and overall, it's safer to keep that behaviour than marking the method results as okay to discard. This renames the `setMaxRefreshRate` `refreshRate` parameter to `requestedRate` since it was shadowing `self.refreshRate`. Makes the code a bit more readable and a bit more future-proof. Finally, this also makes refreshRate a computed property, which ensures that any callers users get a rate within a valid range. It also makes the code in `setMaxRefreshRate` a little more readable. Issue: flutter#112232
1427463 to
5632af9
Compare
|
FYI this is the updated version of #186225 after my reland of the Obj-C-ification of FlutterVSyncClient. |
| // found in the LICENSE file. | ||
|
|
||
| import Foundation | ||
| import QuartzCore |
There was a problem hiding this comment.
these 2 are auto imported by UIKit, so can remove
There was a problem hiding this comment.
Oh good call -- thanks for spotting! Since it's small, to avoid the hours sitting in re-run, I'll sneak this into the followup that migrates this to a Swift 6 concurrency compliant shared instance that we can later make injectable to improve testability if that's alright.
| /// `true` in the application's `Info.plist`. iPad Pro devices will use 120Hz by default. | ||
| /// | ||
| /// - Note: This class contains only stateless `static` properties and class methods and should not | ||
| /// be instantiated. |
There was a problem hiding this comment.
Will testing be a problem in swift without ocmock?
There was a problem hiding this comment.
Oh hey I have a followup for exactly this issue :) I can't remember if I linked to it from here but I did a cleanup that makes it a shared instance and swift 6 concurrency safe. That will let us inject it (or a fake).
| public class VSyncClient: NSObject { | ||
| private static let defaultRefreshRate: Double = 60.0 | ||
|
|
||
| private let isVariableRefreshRateEnabled: Bool | ||
| private let callback: (CFTimeInterval, CFTimeInterval) -> Void | ||
|
|
||
| /// The display link used to coordinate vsync callbacks. | ||
| @objc | ||
| internal private(set) var displayLink: CADisplayLink? | ||
|
|
||
| private var _refreshRate: Double = defaultRefreshRate | ||
|
|
||
| /// The current display refresh rate in Hertz, rounded to the nearest integer value. | ||
| /// | ||
| /// This value is calculated during each vsync callback as the inverse of the frame duration (the | ||
| /// time between the current frame and the target next frame). The resulting frequency is rounded | ||
| /// to the nearest whole number to smooth out minor hardware timestamp variations. | ||
| /// | ||
| /// If the current refresh rate is unknown or invalid (e.g., during startup or a paused transition), | ||
| /// this property falls back to `defaultRefreshRate` (60Hz). | ||
| @objc | ||
| public var refreshRate: Double { | ||
| return _refreshRate > 0.0 ? _refreshRate : VSyncClient.defaultRefreshRate | ||
| } | ||
|
|
||
| /// Default value is `true`. Vsync client will pause vsync callback after receiving a vsync | ||
| /// signal. Setting this property to `false` can avoid this and vsync client will trigger vsync | ||
| /// callback continuously. | ||
| @objc | ||
| public var allowPauseAfterVsync: Bool = true | ||
|
|
||
| /// Initializes the vsync client. | ||
| /// | ||
| /// - Parameters: | ||
| /// - taskRunner: The task runner to use for posting tasks. | ||
| /// - isVariableRefreshRateEnabled: Whether variable refresh rate should be enabled. | ||
| /// - maxRefreshRate: The maximum refresh rate to configure the display link with. | ||
| /// - callback: The callback to invoke when a vsync signal is received. | ||
| @objc | ||
| public init( | ||
| taskRunner: TaskRunner, | ||
| isVariableRefreshRateEnabled: Bool, | ||
| maxRefreshRate: Double, | ||
| callback: @escaping (CFTimeInterval, CFTimeInterval) -> Void | ||
| ) { | ||
| self.isVariableRefreshRateEnabled = isVariableRefreshRateEnabled | ||
| self._refreshRate = maxRefreshRate | ||
| self.callback = callback | ||
|
|
||
| super.init() | ||
|
|
||
| let link = CADisplayLink(target: self, selector: #selector(onDisplayLink(_:))) | ||
| link.isPaused = true | ||
| self.displayLink = link | ||
|
|
||
| setMaxRefreshRate(maxRefreshRate) | ||
|
|
||
| // Capture a weak reference to self to ensure we don't add the display link to the run loop if | ||
| // the client has already been deallocated. | ||
| taskRunner.postTask { [weak self] in | ||
| guard let self else { return } | ||
| self.displayLink?.add(to: .current, forMode: .common) | ||
| } | ||
| } |
There was a problem hiding this comment.
Thanks for writing this up! I didn't know CADisplayLink retains its target.
|
I'll send the followup shortly that does the migration of DisplayLinkManager to an implementation that makes it fully Swift 6 concurrency compliant, and makes it an instance so it could in theory be injected to tests in future. |
flutter/flutter@f3a4b98...c8f2f16 2026-05-27 98614782+auto-submit[bot]@users.noreply.github.com Reverts "[Flutter GPU] Add explicit draw counts (#186639)" (flutter/flutter#187170) 2026-05-27 engine-flutter-autoroll@skia.org Roll Dart SDK from 7dcea971af6b to f3db7b7d9801 (2 revisions) (flutter/flutter#187144) 2026-05-27 bdero@google.com [Flutter GPU] Add explicit draw counts (flutter/flutter#186639) 2026-05-27 engine-flutter-autoroll@skia.org Roll Skia from f9db7748563e to fa944af10f91 (1 revision) (flutter/flutter#187139) 2026-05-27 bdero@google.com [Flutter GPU] Flutter GPU ShaderLibrary in-place reload for shader hot reload (flutter/flutter#186793) 2026-05-27 engine-flutter-autoroll@skia.org Roll Skia from a692cbf38952 to f9db7748563e (8 revisions) (flutter/flutter#187135) 2026-05-27 burak.karahan@mail.ru Use local semantics tester in Material button tests (flutter/flutter#186667) 2026-05-27 rmolivares@renzo-olivares.dev Filter out `a: text input` from `team-framework` PR triage (flutter/flutter#187129) 2026-05-27 bkonyi@google.com [Engine] Fix silent buffer mismatch bug in FML Win32 CommandLineFromPlatform (flutter/flutter#187120) 2026-05-27 srawlins@google.com examples: Remove unused parameters (flutter/flutter#185819) 2026-05-27 116356835+AbdeMohlbi@users.noreply.github.com Remove another `String.valueOf` (flutter/flutter#186628) 2026-05-27 alex.medinsh@gmail.com Print trace when skipping flavor-specific and platform-specific assets (flutter/flutter#187045) 2026-05-26 adilhanney@disroot.org Fix `InteractiveViewer` memory leak from undisposed `CurvedAnimation`s (flutter/flutter#185826) 2026-05-26 meylis@divine.video Fix AccessibilityBridge startup crash on vendor-modified Android (flutter/flutter#184630) 2026-05-26 keertip@users.noreply.github.com Update data driven fixes docs (flutter/flutter#186217) 2026-05-26 ahmedsameha1@gmail.com Add more 0x0 size tests part10 (flutter/flutter#186201) 2026-05-26 46920873+gabrimatic@users.noreply.github.com Disable spell check for obscured text (flutter/flutter#186329) 2026-05-26 chris@bracken.jp [iOS] Migrate VSyncClient and DisplayLinkManager to Swift (flutter/flutter#187001) 2026-05-26 bkonyi@google.com [flutter_tools, devicelab] Add safety filesystem guard to tests (flutter/flutter#186748) 2026-05-26 engine-flutter-autoroll@skia.org Roll Fuchsia Linux SDK from Itd2Jq_ZIABH2rW7B... to k9EizfEGJO4WwQN9-... (flutter/flutter#187115) 2026-05-26 engine-flutter-autoroll@skia.org Roll Dart SDK from 00e625453c43 to 7dcea971af6b (1 revision) (flutter/flutter#187117) 2026-05-26 rmacnak@google.com Remove use of deprecated copy_trees. (flutter/flutter#187091) 2026-05-26 15619084+vashworth@users.noreply.github.com Use resolved implementation plugins with SwiftPM (flutter/flutter#187097) 2026-05-26 engine-flutter-autoroll@skia.org Roll Skia from 27a819894f7c to a692cbf38952 (3 revisions) (flutter/flutter#187109) 2026-05-26 engine-flutter-autoroll@skia.org Roll Packages from 69cf959 to fc795e5 (4 revisions) (flutter/flutter#187114) 2026-05-26 mvincentong@gmail.com Handle simctl process exceptions during discovery (flutter/flutter#186501) 2026-05-26 mvincentong@gmail.com Clarify precache enabled platforms help (flutter/flutter#186878) 2026-05-26 bkonyi@google.com [tool] Refactor artifacts.dart to use enhanced enums and reduce duplication (flutter/flutter#187063) 2026-05-26 jason-simmons@users.noreply.github.com Build script updates for syncing engine sources and building artifacts on linux-arm64 (flutter/flutter#186917) 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 boetger@google.com,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#11795) flutter/flutter@f3a4b98...c8f2f16 2026-05-27 98614782+auto-submit[bot]@users.noreply.github.com Reverts "[Flutter GPU] Add explicit draw counts (#186639)" (flutter/flutter#187170) 2026-05-27 engine-flutter-autoroll@skia.org Roll Dart SDK from 7dcea971af6b to f3db7b7d9801 (2 revisions) (flutter/flutter#187144) 2026-05-27 bdero@google.com [Flutter GPU] Add explicit draw counts (flutter/flutter#186639) 2026-05-27 engine-flutter-autoroll@skia.org Roll Skia from f9db7748563e to fa944af10f91 (1 revision) (flutter/flutter#187139) 2026-05-27 bdero@google.com [Flutter GPU] Flutter GPU ShaderLibrary in-place reload for shader hot reload (flutter/flutter#186793) 2026-05-27 engine-flutter-autoroll@skia.org Roll Skia from a692cbf38952 to f9db7748563e (8 revisions) (flutter/flutter#187135) 2026-05-27 burak.karahan@mail.ru Use local semantics tester in Material button tests (flutter/flutter#186667) 2026-05-27 rmolivares@renzo-olivares.dev Filter out `a: text input` from `team-framework` PR triage (flutter/flutter#187129) 2026-05-27 bkonyi@google.com [Engine] Fix silent buffer mismatch bug in FML Win32 CommandLineFromPlatform (flutter/flutter#187120) 2026-05-27 srawlins@google.com examples: Remove unused parameters (flutter/flutter#185819) 2026-05-27 116356835+AbdeMohlbi@users.noreply.github.com Remove another `String.valueOf` (flutter/flutter#186628) 2026-05-27 alex.medinsh@gmail.com Print trace when skipping flavor-specific and platform-specific assets (flutter/flutter#187045) 2026-05-26 adilhanney@disroot.org Fix `InteractiveViewer` memory leak from undisposed `CurvedAnimation`s (flutter/flutter#185826) 2026-05-26 meylis@divine.video Fix AccessibilityBridge startup crash on vendor-modified Android (flutter/flutter#184630) 2026-05-26 keertip@users.noreply.github.com Update data driven fixes docs (flutter/flutter#186217) 2026-05-26 ahmedsameha1@gmail.com Add more 0x0 size tests part10 (flutter/flutter#186201) 2026-05-26 46920873+gabrimatic@users.noreply.github.com Disable spell check for obscured text (flutter/flutter#186329) 2026-05-26 chris@bracken.jp [iOS] Migrate VSyncClient and DisplayLinkManager to Swift (flutter/flutter#187001) 2026-05-26 bkonyi@google.com [flutter_tools, devicelab] Add safety filesystem guard to tests (flutter/flutter#186748) 2026-05-26 engine-flutter-autoroll@skia.org Roll Fuchsia Linux SDK from Itd2Jq_ZIABH2rW7B... to k9EizfEGJO4WwQN9-... (flutter/flutter#187115) 2026-05-26 engine-flutter-autoroll@skia.org Roll Dart SDK from 00e625453c43 to 7dcea971af6b (1 revision) (flutter/flutter#187117) 2026-05-26 rmacnak@google.com Remove use of deprecated copy_trees. (flutter/flutter#187091) 2026-05-26 15619084+vashworth@users.noreply.github.com Use resolved implementation plugins with SwiftPM (flutter/flutter#187097) 2026-05-26 engine-flutter-autoroll@skia.org Roll Skia from 27a819894f7c to a692cbf38952 (3 revisions) (flutter/flutter#187109) 2026-05-26 engine-flutter-autoroll@skia.org Roll Packages from 69cf959 to fc795e5 (4 revisions) (flutter/flutter#187114) 2026-05-26 mvincentong@gmail.com Handle simctl process exceptions during discovery (flutter/flutter#186501) 2026-05-26 mvincentong@gmail.com Clarify precache enabled platforms help (flutter/flutter#186878) 2026-05-26 bkonyi@google.com [tool] Refactor artifacts.dart to use enhanced enums and reduce duplication (flutter/flutter#187063) 2026-05-26 jason-simmons@users.noreply.github.com Build script updates for syncing engine sources and building artifacts on linux-arm64 (flutter/flutter#186917) 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 boetger@google.com,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
…7001) This also splits DisplayLinkManager to its own file. `(void)` casts needed to be added to OCMock usages of DisplayLinkManager because by default Swift warns on unused results and overall, it's safer to keep that behaviour than marking the method results as okay to discard. This renames the `setMaxRefreshRate` `refreshRate` parameter to `requestedRate` since it was shadowing `self.refreshRate`. Makes the code a bit more readable and a bit more future-proof. Finally, this also makes refreshRate a computed property, which ensures that any callers users get a rate within a valid range. It also makes the code in `setMaxRefreshRate` a little more readable. Issue: flutter#112232 <!-- Thanks for filing a pull request! Reviewers are typically assigned within a week of filing a request. To learn more about code review, see our documentation on Tree Hygiene: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md --> ## 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 documentation (doc comments with `///`). - [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 [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
This also splits DisplayLinkManager to its own file.
(void)casts needed to be added to OCMock usages of DisplayLinkManager because by default Swift warns on unused results and overall, it's safer to keep that behaviour than marking the method results as okay to discard.This renames the
setMaxRefreshRaterefreshRateparameter torequestedRatesince it was shadowingself.refreshRate. Makes the code a bit more readable and a bit more future-proof.Finally, this also makes refreshRate a computed property, which ensures that any callers users get a rate within a valid range. It also makes the code in
setMaxRefreshRatea little more readable.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-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.