Skip to content

Commit 0ce527e

Browse files
author
Chris Yang
authored
[flutter_driver] show refresh rate status in timeline summary (#95699)
1 parent 5f3bee5 commit 0ce527e

File tree

3 files changed

+323
-2
lines changed

3 files changed

+323
-2
lines changed
Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
// Copyright 2014 The Flutter Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
5+
import 'timeline.dart';
6+
7+
/// Event name for refresh rate related timeline events.
8+
const String kUIThreadVsyncProcessEvent = 'VsyncProcessCallback';
9+
10+
/// A summary of [TimelineEvents]s corresponding to `kUIThreadVsyncProcessEvent` events.
11+
///
12+
/// `RefreshRate` is the time between the start of a vsync pulse and the target time of that vsync.
13+
class RefreshRateSummary {
14+
15+
/// Creates a [RefreshRateSummary] given the timeline events.
16+
factory RefreshRateSummary({required List<TimelineEvent> vsyncEvents}) {
17+
return RefreshRateSummary._(refreshRates: _computeRefreshRates(vsyncEvents));
18+
}
19+
20+
RefreshRateSummary._({required List<double> refreshRates}) {
21+
_numberOfTotalFrames = refreshRates.length;
22+
for (final double refreshRate in refreshRates) {
23+
if ((refreshRate - 30).abs() < _kErrorMargin) {
24+
_numberOf30HzFrames++;
25+
continue;
26+
}
27+
if ((refreshRate - 60).abs() < _kErrorMargin) {
28+
_numberOf60HzFrames++;
29+
continue;
30+
}
31+
if ((refreshRate - 90).abs() < _kErrorMargin) {
32+
_numberOf90HzFrames++;
33+
continue;
34+
}
35+
if ((refreshRate - 120).abs() < _kErrorMargin) {
36+
_numberOf120HzFrames++;
37+
continue;
38+
}
39+
_framesWithIllegalRefreshRate.add(refreshRate);
40+
}
41+
assert(_numberOfTotalFrames ==
42+
_numberOf30HzFrames +
43+
_numberOf60HzFrames +
44+
_numberOf90HzFrames +
45+
_numberOf120HzFrames +
46+
_framesWithIllegalRefreshRate.length);
47+
}
48+
49+
static const double _kErrorMargin = 6.0;
50+
51+
/// Number of frames with 30hz refresh rate
52+
int get numberOf30HzFrames => _numberOf30HzFrames;
53+
54+
/// Number of frames with 60hz refresh rate
55+
int get numberOf60HzFrames => _numberOf60HzFrames;
56+
57+
/// Number of frames with 90hz refresh rate
58+
int get numberOf90HzFrames => _numberOf90HzFrames;
59+
60+
/// Number of frames with 120hz refresh rate
61+
int get numberOf120HzFrames => _numberOf120HzFrames;
62+
63+
/// The percentage of 30hz frames.
64+
///
65+
/// For example, if this value is 20, it means there are 20 percent of total
66+
/// frames are 30hz. 0 means no frames are 30hz, 100 means all frames are 30hz.
67+
double get percentageOf30HzFrames => _numberOfTotalFrames > 0
68+
? _numberOf30HzFrames / _numberOfTotalFrames * 100
69+
: 0;
70+
71+
/// The percentage of 60hz frames.
72+
///
73+
/// For example, if this value is 20, it means there are 20 percent of total
74+
/// frames are 60hz. 0 means no frames are 60hz, 100 means all frames are 60hz.
75+
double get percentageOf60HzFrames => _numberOfTotalFrames > 0
76+
? _numberOf60HzFrames / _numberOfTotalFrames * 100
77+
: 0;
78+
79+
/// The percentage of 90hz frames.
80+
///
81+
/// For example, if this value is 20, it means there are 20 percent of total
82+
/// frames are 90hz. 0 means no frames are 90hz, 100 means all frames are 90hz.
83+
double get percentageOf90HzFrames => _numberOfTotalFrames > 0
84+
? _numberOf90HzFrames / _numberOfTotalFrames * 100
85+
: 0;
86+
87+
/// The percentage of 90hz frames.
88+
///
89+
/// For example, if this value is 20, it means there are 20 percent of total
90+
/// frames are 120hz. 0 means no frames are 120hz, 100 means all frames are 120hz.
91+
double get percentageOf120HzFrames => _numberOfTotalFrames > 0
92+
? _numberOf120HzFrames / _numberOfTotalFrames * 100
93+
: 0;
94+
95+
/// A list of all the frames with Illegal refresh rate.
96+
///
97+
/// A refresh rate is consider illegal if it does not belong to anyone below:
98+
/// 30hz, 60hz, 90hz or 120hz.
99+
List<double> get framesWithIllegalRefreshRate =>
100+
_framesWithIllegalRefreshRate;
101+
102+
int _numberOf30HzFrames = 0;
103+
int _numberOf60HzFrames = 0;
104+
int _numberOf90HzFrames = 0;
105+
int _numberOf120HzFrames = 0;
106+
int _numberOfTotalFrames = 0;
107+
108+
final List<double> _framesWithIllegalRefreshRate = <double>[];
109+
110+
static List<double> _computeRefreshRates(List<TimelineEvent> vsyncEvents) {
111+
final List<double> result = <double>[];
112+
for (int i = 0; i < vsyncEvents.length; i++) {
113+
final TimelineEvent event = vsyncEvents[i];
114+
if (event.phase != 'B') {
115+
continue;
116+
}
117+
assert(event.name == kUIThreadVsyncProcessEvent);
118+
assert(event.arguments != null);
119+
final Map<String, dynamic> arguments = event.arguments!;
120+
const double nanosecondsPerSecond = 1e+9;
121+
final int startTimeInNanoseconds = int.parse(arguments['StartTime'] as String);
122+
final int targetTimeInNanoseconds = int.parse(arguments['TargetTime'] as String);
123+
final int frameDurationInNanoseconds = targetTimeInNanoseconds - startTimeInNanoseconds;
124+
final double refreshRate = nanosecondsPerSecond /
125+
frameDurationInNanoseconds;
126+
result.add(refreshRate);
127+
}
128+
return result;
129+
}
130+
}

packages/flutter_driver/lib/src/driver/timeline_summary.dart

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import 'gc_summarizer.dart';
1313
import 'percentile_utils.dart';
1414
import 'profiling_summarizer.dart';
1515
import 'raster_cache_summarizer.dart';
16+
import 'refresh_rate_summarizer.dart';
1617
import 'scene_display_lag_summarizer.dart';
1718
import 'timeline.dart';
1819
import 'vsync_frame_lag_summarizer.dart';
@@ -220,6 +221,7 @@ class TimelineSummary {
220221
final Map<String, dynamic> profilingSummary = _profilingSummarizer().summarize();
221222
final RasterCacheSummarizer rasterCacheSummarizer = _rasterCacheSummarizer();
222223
final GCSummarizer gcSummarizer = _gcSummarizer();
224+
final RefreshRateSummary refreshRateSummary = RefreshRateSummary(vsyncEvents: _extractNamedEvents(kUIThreadVsyncProcessEvent));
223225

224226
final Map<String, dynamic> timelineSummary = <String, dynamic>{
225227
'average_frame_build_time_millis': computeAverageFrameBuildTimeMillis(),
@@ -271,6 +273,11 @@ class TimelineSummary {
271273
'99th_percentile_picture_cache_memory': rasterCacheSummarizer.computePercentilePictureMemory(99.0),
272274
'worst_picture_cache_memory': rasterCacheSummarizer.computeWorstPictureMemory(),
273275
'total_ui_gc_time': gcSummarizer.totalGCTimeMillis,
276+
'30hz_frame_percentage': refreshRateSummary.percentageOf30HzFrames,
277+
'60hz_frame_percentage': refreshRateSummary.percentageOf60HzFrames,
278+
'90hz_frame_percentage': refreshRateSummary.percentageOf90HzFrames,
279+
'120hz_frame_percentage': refreshRateSummary.percentageOf120HzFrames,
280+
'illegal_refresh_rate_frame_count': refreshRateSummary.framesWithIllegalRefreshRate.length,
274281
};
275282

276283
timelineSummary.addAll(profilingSummary);

packages/flutter_driver/test/src/real_tests/timeline_summary_test.dart

Lines changed: 186 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,12 @@
33
// found in the LICENSE file.
44

55
import 'dart:convert' show json;
6+
import 'dart:math';
67

78
import 'package:file/file.dart';
89
import 'package:flutter_driver/flutter_driver.dart';
910
import 'package:flutter_driver/src/driver/profiling_summarizer.dart';
11+
import 'package:flutter_driver/src/driver/refresh_rate_summarizer.dart';
1012
import 'package:flutter_driver/src/driver/scene_display_lag_summarizer.dart';
1113
import 'package:flutter_driver/src/driver/vsync_frame_lag_summarizer.dart';
1214
import 'package:path/path.dart' as path;
@@ -89,10 +91,14 @@ void main() {
8991
'ts': timeStamp,
9092
};
9193

92-
Map<String, dynamic> vsyncCallback(int timeStamp) => <String, dynamic>{
94+
Map<String, dynamic> vsyncCallback(int timeStamp, {String phase = 'B', String startTime = '2750850055428', String endTime = '2750866722095'}) => <String, dynamic>{
9395
'name': 'VsyncProcessCallback',
94-
'ph': 'B',
96+
'ph': phase,
9597
'ts': timeStamp,
98+
'args': <String, dynamic>{
99+
'StartTime': startTime,
100+
'TargetTime': endTime,
101+
}
96102
};
97103

98104
List<Map<String, dynamic>> _genGC(String name, int count, int startTime, int timeDiff) {
@@ -467,6 +473,11 @@ void main() {
467473
'99th_percentile_picture_cache_memory': 0.0,
468474
'worst_picture_cache_memory': 0.0,
469475
'total_ui_gc_time': 0.4,
476+
'30hz_frame_percentage': 0,
477+
'60hz_frame_percentage': 0,
478+
'90hz_frame_percentage': 0,
479+
'120hz_frame_percentage': 0,
480+
'illegal_refresh_rate_frame_count': 0,
470481
},
471482
);
472483
});
@@ -582,6 +593,11 @@ void main() {
582593
'99th_percentile_picture_cache_memory': 0.0,
583594
'worst_picture_cache_memory': 0.0,
584595
'total_ui_gc_time': 0.4,
596+
'30hz_frame_percentage': 0,
597+
'60hz_frame_percentage': 100,
598+
'90hz_frame_percentage': 0,
599+
'120hz_frame_percentage': 0,
600+
'illegal_refresh_rate_frame_count': 0,
585601
});
586602
});
587603
});
@@ -734,5 +750,173 @@ void main() {
734750
expect(summarizer.computePercentileVsyncFrameLag(99), 990);
735751
});
736752
});
753+
754+
group('RefreshRateSummarizer tests', () {
755+
756+
const double kCompareDelta = 0.01;
757+
RefreshRateSummary _summarize(List<Map<String, dynamic>> traceEvents) {
758+
final Timeline timeline = Timeline.fromJson(<String, dynamic>{
759+
'traceEvents': traceEvents,
760+
});
761+
return RefreshRateSummary(vsyncEvents: timeline.events!);
762+
}
763+
764+
List<Map<String, dynamic>> _populateEvents({required int numberOfEvents, required int startTime, required int interval, required int margin}) {
765+
final List<Map<String, dynamic>> events = <Map<String, dynamic>>[];
766+
int startTimeInNanoseconds = startTime;
767+
for (int i = 0; i < numberOfEvents; i ++) {
768+
final int randomMargin = margin >= 1 ? (-margin + Random().nextInt(margin*2)) : 0;
769+
final int endTime = startTimeInNanoseconds + interval + randomMargin;
770+
events.add(vsyncCallback(0, startTime: startTimeInNanoseconds.toString(), endTime: endTime.toString()));
771+
startTimeInNanoseconds = endTime;
772+
}
773+
return events;
774+
}
775+
776+
test('Recognize 30 hz frames.', () async {
777+
const int startTimeInNanoseconds = 2750850055430;
778+
const int intervalInNanoseconds = 33333333;
779+
// allow some margins
780+
const int margin = 3000000;
781+
final List<Map<String, dynamic>> events = _populateEvents(numberOfEvents: 100,
782+
startTime: startTimeInNanoseconds,
783+
interval: intervalInNanoseconds,
784+
margin: margin,
785+
);
786+
final RefreshRateSummary summary = _summarize(events);
787+
expect(summary.percentageOf30HzFrames, closeTo(100, kCompareDelta));
788+
expect(summary.percentageOf60HzFrames, 0);
789+
expect(summary.percentageOf90HzFrames, 0);
790+
expect(summary.percentageOf120HzFrames, 0);
791+
expect(summary.framesWithIllegalRefreshRate, isEmpty);
792+
});
793+
794+
test('Recognize 60 hz frames.', () async {
795+
const int startTimeInNanoseconds = 2750850055430;
796+
const int intervalInNanoseconds = 16666666;
797+
// allow some margins
798+
const int margin = 1200000;
799+
final List<Map<String, dynamic>> events = _populateEvents(numberOfEvents: 100,
800+
startTime: startTimeInNanoseconds,
801+
interval: intervalInNanoseconds,
802+
margin: margin,
803+
);
804+
805+
final RefreshRateSummary summary = _summarize(events);
806+
expect(summary.percentageOf30HzFrames, 0);
807+
expect(summary.percentageOf60HzFrames, closeTo(100, kCompareDelta));
808+
expect(summary.percentageOf90HzFrames, 0);
809+
expect(summary.percentageOf120HzFrames, 0);
810+
expect(summary.framesWithIllegalRefreshRate, isEmpty);
811+
});
812+
813+
test('Recognize 90 hz frames.', () async {
814+
const int startTimeInNanoseconds = 2750850055430;
815+
const int intervalInNanoseconds = 11111111;
816+
// allow some margins
817+
const int margin = 500000;
818+
final List<Map<String, dynamic>> events = _populateEvents(numberOfEvents: 100,
819+
startTime: startTimeInNanoseconds,
820+
interval: intervalInNanoseconds,
821+
margin: margin,
822+
);
823+
824+
final RefreshRateSummary summary = _summarize(events);
825+
expect(summary.percentageOf30HzFrames, 0);
826+
expect(summary.percentageOf60HzFrames, 0);
827+
expect(summary.percentageOf90HzFrames, closeTo(100, kCompareDelta));
828+
expect(summary.percentageOf120HzFrames, 0);
829+
expect(summary.framesWithIllegalRefreshRate, isEmpty);
830+
});
831+
832+
test('Recognize 120 hz frames.', () async {
833+
const int startTimeInNanoseconds = 2750850055430;
834+
const int intervalInNanoseconds = 8333333;
835+
// allow some margins
836+
const int margin = 300000;
837+
final List<Map<String, dynamic>> events = _populateEvents(numberOfEvents: 100,
838+
startTime: startTimeInNanoseconds,
839+
interval: intervalInNanoseconds,
840+
margin: margin,
841+
);
842+
final RefreshRateSummary summary = _summarize(events);
843+
expect(summary.percentageOf30HzFrames, 0);
844+
expect(summary.percentageOf60HzFrames, 0);
845+
expect(summary.percentageOf90HzFrames, 0);
846+
expect(summary.percentageOf120HzFrames, closeTo(100, kCompareDelta));
847+
expect(summary.framesWithIllegalRefreshRate, isEmpty);
848+
});
849+
850+
test('Identify illegal refresh rates.', () async {
851+
const int startTimeInNanoseconds = 2750850055430;
852+
const int intervalInNanoseconds = 10000000;
853+
final List<Map<String, dynamic>> events = _populateEvents(numberOfEvents: 1,
854+
startTime: startTimeInNanoseconds,
855+
interval: intervalInNanoseconds,
856+
margin: 0,
857+
);
858+
final RefreshRateSummary summary = _summarize(events);
859+
expect(summary.percentageOf30HzFrames, 0);
860+
expect(summary.percentageOf60HzFrames, 0);
861+
expect(summary.percentageOf90HzFrames, 0);
862+
expect(summary.percentageOf120HzFrames, 0);
863+
expect(summary.framesWithIllegalRefreshRate, isNotEmpty);
864+
expect(summary.framesWithIllegalRefreshRate.first, closeTo(100, kCompareDelta));
865+
});
866+
867+
test('Mixed refresh rates.', () async {
868+
869+
final List<Map<String, dynamic>> events = <Map<String, dynamic>>[];
870+
const int num30Hz = 10;
871+
const int num60Hz = 20;
872+
const int num90Hz = 20;
873+
const int num120Hz = 40;
874+
const int numIllegal = 10;
875+
876+
// Add 30hz frames
877+
events.addAll(_populateEvents(numberOfEvents: num30Hz,
878+
startTime: 0,
879+
interval: 32000000,
880+
margin: 0,
881+
));
882+
883+
// Add 60hz frames
884+
events.addAll(_populateEvents(numberOfEvents: num60Hz,
885+
startTime: 0,
886+
interval: 16000000,
887+
margin: 0,
888+
));
889+
890+
891+
// Add 90hz frames
892+
events.addAll(_populateEvents(numberOfEvents: num90Hz,
893+
startTime: 0,
894+
interval: 11000000,
895+
margin: 0,
896+
));
897+
898+
// Add 120hz frames
899+
events.addAll(_populateEvents(numberOfEvents: num120Hz,
900+
startTime: 0,
901+
interval: 8000000,
902+
margin: 0,
903+
));
904+
905+
// Add illegal refresh rate frames
906+
events.addAll(_populateEvents(numberOfEvents: numIllegal,
907+
startTime: 0,
908+
interval: 60000,
909+
margin: 0,
910+
));
911+
912+
final RefreshRateSummary summary = _summarize(events);
913+
expect(summary.percentageOf30HzFrames, closeTo(num30Hz, kCompareDelta));
914+
expect(summary.percentageOf60HzFrames, closeTo(num60Hz, kCompareDelta));
915+
expect(summary.percentageOf90HzFrames, closeTo(num90Hz, kCompareDelta));
916+
expect(summary.percentageOf120HzFrames, closeTo(num120Hz, kCompareDelta));
917+
expect(summary.framesWithIllegalRefreshRate, isNotEmpty);
918+
expect(summary.framesWithIllegalRefreshRate.length, 10);
919+
});
920+
});
737921
});
738922
}

0 commit comments

Comments
 (0)