Skip to content
This repository was archived by the owner on Feb 25, 2025. It is now read-only.

Commit 18750d2

Browse files
author
Chris Yang
authored
[ios] Lock refresh rate to 80fps when threads are merged (#39172)
When there are PlatformViews on the screen (threads merged), Flutter engine cannot consistently hit 120 fps, especially when the PlatformView is animating (scrolling etc), thus producing janks. This PR locks the refresh rate to 80fps to avoid janks. Fixes flutter/flutter#116640
1 parent 9dde156 commit 18750d2

File tree

3 files changed

+113
-21
lines changed

3 files changed

+113
-21
lines changed

shell/platform/darwin/ios/framework/Source/VsyncWaiterIosTest.mm

Lines changed: 66 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
#import <OCMock/OCMock.h>
66
#import <XCTest/XCTest.h>
77

8+
#include "flutter/fml/raster_thread_merger.h"
89
#include "flutter/fml/thread.h"
910

1011
#import "flutter/shell/platform/darwin/common/framework/Headers/FlutterMacros.h"
@@ -65,11 +66,11 @@ - (void)testSetCorrectVariableRefreshRates {
6566
callback:callback] autorelease];
6667
CADisplayLink* link = [vsyncClient getDisplayLink];
6768
if (@available(iOS 15.0, *)) {
68-
XCTAssertEqual(link.preferredFrameRateRange.maximum, maxFrameRate);
69-
XCTAssertEqual(link.preferredFrameRateRange.preferred, maxFrameRate);
70-
XCTAssertEqual(link.preferredFrameRateRange.minimum, maxFrameRate / 2);
69+
XCTAssertEqualWithAccuracy(link.preferredFrameRateRange.maximum, maxFrameRate, 0.1);
70+
XCTAssertEqualWithAccuracy(link.preferredFrameRateRange.preferred, maxFrameRate, 0.1);
71+
XCTAssertEqualWithAccuracy(link.preferredFrameRateRange.minimum, maxFrameRate / 2, 0.1);
7172
} else {
72-
XCTAssertEqual(link.preferredFramesPerSecond, maxFrameRate);
73+
XCTAssertEqualWithAccuracy(link.preferredFramesPerSecond, maxFrameRate, 0.1);
7374
}
7475
[vsyncClient release];
7576
}
@@ -88,11 +89,11 @@ - (void)testDoNotSetVariableRefreshRatesIfCADisableMinimumFrameDurationOnPhoneIs
8889
callback:callback] autorelease];
8990
CADisplayLink* link = [vsyncClient getDisplayLink];
9091
if (@available(iOS 15.0, *)) {
91-
XCTAssertEqual(link.preferredFrameRateRange.maximum, 0);
92-
XCTAssertEqual(link.preferredFrameRateRange.preferred, 0);
93-
XCTAssertEqual(link.preferredFrameRateRange.minimum, 0);
92+
XCTAssertEqualWithAccuracy(link.preferredFrameRateRange.maximum, 0, 0.1);
93+
XCTAssertEqualWithAccuracy(link.preferredFrameRateRange.preferred, 0, 0.1);
94+
XCTAssertEqualWithAccuracy(link.preferredFrameRateRange.minimum, 0, 0.1);
9495
} else {
95-
XCTAssertEqual(link.preferredFramesPerSecond, 0);
96+
XCTAssertEqualWithAccuracy(link.preferredFramesPerSecond, 0, 0.1);
9697
}
9798
[vsyncClient release];
9899
}
@@ -107,11 +108,11 @@ - (void)testDoNotSetVariableRefreshRatesIfCADisableMinimumFrameDurationOnPhoneIs
107108
callback:callback] autorelease];
108109
CADisplayLink* link = [vsyncClient getDisplayLink];
109110
if (@available(iOS 15.0, *)) {
110-
XCTAssertEqual(link.preferredFrameRateRange.maximum, 0);
111-
XCTAssertEqual(link.preferredFrameRateRange.preferred, 0);
112-
XCTAssertEqual(link.preferredFrameRateRange.minimum, 0);
111+
XCTAssertEqualWithAccuracy(link.preferredFrameRateRange.maximum, 0, 0.1);
112+
XCTAssertEqualWithAccuracy(link.preferredFrameRateRange.preferred, 0, 0.1);
113+
XCTAssertEqualWithAccuracy(link.preferredFrameRateRange.minimum, 0, 0.1);
113114
} else {
114-
XCTAssertEqual(link.preferredFramesPerSecond, 0);
115+
XCTAssertEqualWithAccuracy(link.preferredFramesPerSecond, 0, 0.1);
115116
}
116117
[vsyncClient release];
117118
}
@@ -135,4 +136,57 @@ - (void)testAwaitAndPauseWillWorkCorrectly {
135136
[vsyncClient release];
136137
}
137138

139+
- (void)testRefreshRateUpdatedTo80WhenThraedsMerge {
140+
auto platform_thread_task_runner = CreateNewThread("Platform");
141+
auto raster_thread_task_runner = CreateNewThread("Raster");
142+
auto ui_thread_task_runner = CreateNewThread("UI");
143+
auto io_thread_task_runner = CreateNewThread("IO");
144+
auto task_runners =
145+
flutter::TaskRunners("test", platform_thread_task_runner, raster_thread_task_runner,
146+
ui_thread_task_runner, io_thread_task_runner);
147+
148+
id mockDisplayLinkManager = [OCMockObject mockForClass:[DisplayLinkManager class]];
149+
double maxFrameRate = 120;
150+
[[[mockDisplayLinkManager stub] andReturnValue:@(maxFrameRate)] displayRefreshRate];
151+
[[[mockDisplayLinkManager stub] andReturnValue:@(YES)] maxRefreshRateEnabledOnIPhone];
152+
auto vsync_waiter = flutter::VsyncWaiterIOS(task_runners);
153+
154+
fml::scoped_nsobject<VSyncClient> vsyncClient = vsync_waiter.GetVsyncClient();
155+
CADisplayLink* link = [vsyncClient.get() getDisplayLink];
156+
157+
if (@available(iOS 15.0, *)) {
158+
XCTAssertEqualWithAccuracy(link.preferredFrameRateRange.maximum, maxFrameRate, 0.1);
159+
XCTAssertEqualWithAccuracy(link.preferredFrameRateRange.preferred, maxFrameRate, 0.1);
160+
XCTAssertEqualWithAccuracy(link.preferredFrameRateRange.minimum, maxFrameRate / 2, 0.1);
161+
} else {
162+
XCTAssertEqualWithAccuracy(link.preferredFramesPerSecond, maxFrameRate, 0.1);
163+
}
164+
165+
const auto merger = fml::RasterThreadMerger::CreateOrShareThreadMerger(
166+
nullptr, platform_thread_task_runner->GetTaskQueueId(),
167+
raster_thread_task_runner->GetTaskQueueId());
168+
169+
merger->MergeWithLease(5);
170+
vsync_waiter.AwaitVSync();
171+
172+
if (@available(iOS 15.0, *)) {
173+
XCTAssertEqualWithAccuracy(link.preferredFrameRateRange.maximum, 80, 0.1);
174+
XCTAssertEqualWithAccuracy(link.preferredFrameRateRange.preferred, 80, 0.1);
175+
XCTAssertEqualWithAccuracy(link.preferredFrameRateRange.minimum, 60, 0.1);
176+
} else {
177+
XCTAssertEqualWithAccuracy(link.preferredFramesPerSecond, 80, 0.1);
178+
}
179+
180+
merger->UnMergeNowIfLastOne();
181+
vsync_waiter.AwaitVSync();
182+
183+
if (@available(iOS 15.0, *)) {
184+
XCTAssertEqualWithAccuracy(link.preferredFrameRateRange.maximum, maxFrameRate, 0.1);
185+
XCTAssertEqualWithAccuracy(link.preferredFrameRateRange.preferred, maxFrameRate, 0.1);
186+
XCTAssertEqualWithAccuracy(link.preferredFrameRateRange.minimum, maxFrameRate / 2, 0.1);
187+
} else {
188+
XCTAssertEqualWithAccuracy(link.preferredFramesPerSecond, maxFrameRate, 0.1);
189+
}
190+
}
191+
138192
@end

shell/platform/darwin/ios/framework/Source/vsync_waiter_ios.h

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,13 @@
1515

1616
@interface DisplayLinkManager : NSObject
1717

18+
// Whether the max refresh rate on iPhone Pro-motion devices are enabled.
19+
// This reflects the value of `CADisableMinimumFrameDurationOnPhone` in the
20+
// info.plist file.
21+
//
22+
// Note on iPads that support Pro-motion, the max refresh rate is always enabled.
23+
@property(class, nonatomic, readonly) BOOL maxRefreshRateEnabledOnIPhone;
24+
1825
//------------------------------------------------------------------------------
1926
/// @brief The display refresh rate used for reporting purposes. The engine does not care
2027
/// about this for frame scheduling. It is only used by tools for instrumentation. The
@@ -51,6 +58,8 @@
5158

5259
- (double)getRefreshRate;
5360

61+
- (void)setMaxRefreshRate:(double)refreshRate;
62+
5463
@end
5564

5665
namespace flutter {
@@ -64,12 +73,17 @@ class VsyncWaiterIOS final : public VsyncWaiter, public VariableRefreshRateRepor
6473
// |VariableRefreshRateReporter|
6574
double GetRefreshRate() const override;
6675

67-
private:
68-
fml::scoped_nsobject<VSyncClient> client_;
76+
// Made public for testing.
77+
fml::scoped_nsobject<VSyncClient> GetVsyncClient() const;
6978

7079
// |VsyncWaiter|
80+
// Made public for testing.
7181
void AwaitVSync() override;
7282

83+
private:
84+
fml::scoped_nsobject<VSyncClient> client_;
85+
double max_refresh_rate_;
86+
7387
FML_DISALLOW_COPY_AND_ASSIGN(VsyncWaiterIOS);
7488
};
7589

shell/platform/darwin/ios/framework/Source/vsync_waiter_ios.mm

Lines changed: 31 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,12 @@
1212

1313
#include "flutter/common/task_runners.h"
1414
#include "flutter/fml/logging.h"
15+
#include "flutter/fml/memory/task_runner_checker.h"
1516
#include "flutter/fml/trace_event.h"
1617

18+
// When calculating refresh rate diffrence, anything within 0.1 fps is ignored.
19+
const static double kRefreshRateDiffToIgnore = 0.1;
20+
1721
namespace flutter {
1822

1923
VsyncWaiterIOS::VsyncWaiterIOS(const flutter::TaskRunners& task_runners)
@@ -26,6 +30,7 @@
2630
client_ =
2731
fml::scoped_nsobject{[[VSyncClient alloc] initWithTaskRunner:task_runners_.GetUITaskRunner()
2832
callback:callback]};
33+
max_refresh_rate_ = [DisplayLinkManager displayRefreshRate];
2934
}
3035

3136
VsyncWaiterIOS::~VsyncWaiterIOS() {
@@ -35,6 +40,19 @@
3540
}
3641

3742
void VsyncWaiterIOS::AwaitVSync() {
43+
double new_max_refresh_rate = [DisplayLinkManager displayRefreshRate];
44+
if (fml::TaskRunnerChecker::RunsOnTheSameThread(
45+
task_runners_.GetRasterTaskRunner()->GetTaskQueueId(),
46+
task_runners_.GetPlatformTaskRunner()->GetTaskQueueId())) {
47+
// Pressure tested on iPhone 13 pro, the oldest iPhone that supports refresh rate greater than
48+
// 60fps. A flutter app can handle fast scrolling on 80 fps with 6 PlatformViews in the scene at
49+
// the same time.
50+
new_max_refresh_rate = 80;
51+
}
52+
if (fabs(new_max_refresh_rate - max_refresh_rate_) > kRefreshRateDiffToIgnore) {
53+
max_refresh_rate_ = new_max_refresh_rate;
54+
[client_.get() setMaxRefreshRate:max_refresh_rate_];
55+
}
3856
[client_.get() await];
3957
}
4058

@@ -43,6 +61,10 @@
4361
return [client_.get() getRefreshRate];
4462
}
4563

64+
fml::scoped_nsobject<VSyncClient> VsyncWaiterIOS::GetVsyncClient() const {
65+
return client_;
66+
}
67+
4668
} // namespace flutter
4769

4870
@implementation VSyncClient {
@@ -64,7 +86,7 @@ - (instancetype)initWithTaskRunner:(fml::RefPtr<fml::TaskRunner>)task_runner
6486
};
6587
display_link_.get().paused = YES;
6688

67-
[self setMaxRefreshRateIfEnabled];
89+
[self setMaxRefreshRate:[DisplayLinkManager displayRefreshRate]];
6890

6991
task_runner->PostTask([client = [self retain]]() {
7092
[client->display_link_.get() addToRunLoop:[NSRunLoop currentRunLoop]
@@ -76,15 +98,12 @@ - (instancetype)initWithTaskRunner:(fml::RefPtr<fml::TaskRunner>)task_runner
7698
return self;
7799
}
78100

79-
- (void)setMaxRefreshRateIfEnabled {
80-
NSNumber* minimumFrameRateDisabled =
81-
[[NSBundle mainBundle] objectForInfoDictionaryKey:@"CADisableMinimumFrameDurationOnPhone"];
82-
if (![minimumFrameRateDisabled boolValue]) {
101+
- (void)setMaxRefreshRate:(double)refreshRate {
102+
if (!DisplayLinkManager.maxRefreshRateEnabledOnIPhone) {
83103
return;
84104
}
85-
double maxFrameRate = fmax([DisplayLinkManager displayRefreshRate], 60);
105+
double maxFrameRate = fmax(refreshRate, 60);
86106
double minFrameRate = fmax(maxFrameRate / 2, 60);
87-
88107
if (@available(iOS 15.0, *)) {
89108
display_link_.get().preferredFrameRateRange =
90109
CAFrameRateRangeMake(minFrameRate, maxFrameRate, maxFrameRate);
@@ -170,4 +189,9 @@ - (void)onDisplayLink:(CADisplayLink*)link {
170189
// no-op.
171190
}
172191

192+
+ (BOOL)maxRefreshRateEnabledOnIPhone {
193+
return [[[NSBundle mainBundle] objectForInfoDictionaryKey:@"CADisableMinimumFrameDurationOnPhone"]
194+
boolValue];
195+
}
196+
173197
@end

0 commit comments

Comments
 (0)