[flutter_driver] show refresh rate status in timeline summary (#95699)
diff --git a/packages/flutter_driver/lib/src/driver/refresh_rate_summarizer.dart b/packages/flutter_driver/lib/src/driver/refresh_rate_summarizer.dart
new file mode 100644
index 0000000..cef0d7a
--- /dev/null
+++ b/packages/flutter_driver/lib/src/driver/refresh_rate_summarizer.dart
@@ -0,0 +1,130 @@
+// Copyright 2014 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'timeline.dart';
+
+/// Event name for refresh rate related timeline events.
+const String kUIThreadVsyncProcessEvent = 'VsyncProcessCallback';
+
+/// A summary of [TimelineEvents]s corresponding to `kUIThreadVsyncProcessEvent` events.
+///
+/// `RefreshRate` is the time between the start of a vsync pulse and the target time of that vsync.
+class RefreshRateSummary {
+
+ /// Creates a [RefreshRateSummary] given the timeline events.
+ factory RefreshRateSummary({required List<TimelineEvent> vsyncEvents}) {
+ return RefreshRateSummary._(refreshRates: _computeRefreshRates(vsyncEvents));
+ }
+
+ RefreshRateSummary._({required List<double> refreshRates}) {
+ _numberOfTotalFrames = refreshRates.length;
+ for (final double refreshRate in refreshRates) {
+ if ((refreshRate - 30).abs() < _kErrorMargin) {
+ _numberOf30HzFrames++;
+ continue;
+ }
+ if ((refreshRate - 60).abs() < _kErrorMargin) {
+ _numberOf60HzFrames++;
+ continue;
+ }
+ if ((refreshRate - 90).abs() < _kErrorMargin) {
+ _numberOf90HzFrames++;
+ continue;
+ }
+ if ((refreshRate - 120).abs() < _kErrorMargin) {
+ _numberOf120HzFrames++;
+ continue;
+ }
+ _framesWithIllegalRefreshRate.add(refreshRate);
+ }
+ assert(_numberOfTotalFrames ==
+ _numberOf30HzFrames +
+ _numberOf60HzFrames +
+ _numberOf90HzFrames +
+ _numberOf120HzFrames +
+ _framesWithIllegalRefreshRate.length);
+ }
+
+ static const double _kErrorMargin = 6.0;
+
+ /// Number of frames with 30hz refresh rate
+ int get numberOf30HzFrames => _numberOf30HzFrames;
+
+ /// Number of frames with 60hz refresh rate
+ int get numberOf60HzFrames => _numberOf60HzFrames;
+
+ /// Number of frames with 90hz refresh rate
+ int get numberOf90HzFrames => _numberOf90HzFrames;
+
+ /// Number of frames with 120hz refresh rate
+ int get numberOf120HzFrames => _numberOf120HzFrames;
+
+ /// The percentage of 30hz frames.
+ ///
+ /// For example, if this value is 20, it means there are 20 percent of total
+ /// frames are 30hz. 0 means no frames are 30hz, 100 means all frames are 30hz.
+ double get percentageOf30HzFrames => _numberOfTotalFrames > 0
+ ? _numberOf30HzFrames / _numberOfTotalFrames * 100
+ : 0;
+
+ /// The percentage of 60hz frames.
+ ///
+ /// For example, if this value is 20, it means there are 20 percent of total
+ /// frames are 60hz. 0 means no frames are 60hz, 100 means all frames are 60hz.
+ double get percentageOf60HzFrames => _numberOfTotalFrames > 0
+ ? _numberOf60HzFrames / _numberOfTotalFrames * 100
+ : 0;
+
+ /// The percentage of 90hz frames.
+ ///
+ /// For example, if this value is 20, it means there are 20 percent of total
+ /// frames are 90hz. 0 means no frames are 90hz, 100 means all frames are 90hz.
+ double get percentageOf90HzFrames => _numberOfTotalFrames > 0
+ ? _numberOf90HzFrames / _numberOfTotalFrames * 100
+ : 0;
+
+ /// The percentage of 90hz frames.
+ ///
+ /// For example, if this value is 20, it means there are 20 percent of total
+ /// frames are 120hz. 0 means no frames are 120hz, 100 means all frames are 120hz.
+ double get percentageOf120HzFrames => _numberOfTotalFrames > 0
+ ? _numberOf120HzFrames / _numberOfTotalFrames * 100
+ : 0;
+
+ /// A list of all the frames with Illegal refresh rate.
+ ///
+ /// A refresh rate is consider illegal if it does not belong to anyone below:
+ /// 30hz, 60hz, 90hz or 120hz.
+ List<double> get framesWithIllegalRefreshRate =>
+ _framesWithIllegalRefreshRate;
+
+ int _numberOf30HzFrames = 0;
+ int _numberOf60HzFrames = 0;
+ int _numberOf90HzFrames = 0;
+ int _numberOf120HzFrames = 0;
+ int _numberOfTotalFrames = 0;
+
+ final List<double> _framesWithIllegalRefreshRate = <double>[];
+
+ static List<double> _computeRefreshRates(List<TimelineEvent> vsyncEvents) {
+ final List<double> result = <double>[];
+ for (int i = 0; i < vsyncEvents.length; i++) {
+ final TimelineEvent event = vsyncEvents[i];
+ if (event.phase != 'B') {
+ continue;
+ }
+ assert(event.name == kUIThreadVsyncProcessEvent);
+ assert(event.arguments != null);
+ final Map<String, dynamic> arguments = event.arguments!;
+ const double nanosecondsPerSecond = 1e+9;
+ final int startTimeInNanoseconds = int.parse(arguments['StartTime'] as String);
+ final int targetTimeInNanoseconds = int.parse(arguments['TargetTime'] as String);
+ final int frameDurationInNanoseconds = targetTimeInNanoseconds - startTimeInNanoseconds;
+ final double refreshRate = nanosecondsPerSecond /
+ frameDurationInNanoseconds;
+ result.add(refreshRate);
+ }
+ return result;
+ }
+}
diff --git a/packages/flutter_driver/lib/src/driver/timeline_summary.dart b/packages/flutter_driver/lib/src/driver/timeline_summary.dart
index 73af4ae..c1cf5c9 100644
--- a/packages/flutter_driver/lib/src/driver/timeline_summary.dart
+++ b/packages/flutter_driver/lib/src/driver/timeline_summary.dart
@@ -13,6 +13,7 @@
import 'percentile_utils.dart';
import 'profiling_summarizer.dart';
import 'raster_cache_summarizer.dart';
+import 'refresh_rate_summarizer.dart';
import 'scene_display_lag_summarizer.dart';
import 'timeline.dart';
import 'vsync_frame_lag_summarizer.dart';
@@ -220,6 +221,7 @@
final Map<String, dynamic> profilingSummary = _profilingSummarizer().summarize();
final RasterCacheSummarizer rasterCacheSummarizer = _rasterCacheSummarizer();
final GCSummarizer gcSummarizer = _gcSummarizer();
+ final RefreshRateSummary refreshRateSummary = RefreshRateSummary(vsyncEvents: _extractNamedEvents(kUIThreadVsyncProcessEvent));
final Map<String, dynamic> timelineSummary = <String, dynamic>{
'average_frame_build_time_millis': computeAverageFrameBuildTimeMillis(),
@@ -271,6 +273,11 @@
'99th_percentile_picture_cache_memory': rasterCacheSummarizer.computePercentilePictureMemory(99.0),
'worst_picture_cache_memory': rasterCacheSummarizer.computeWorstPictureMemory(),
'total_ui_gc_time': gcSummarizer.totalGCTimeMillis,
+ '30hz_frame_percentage': refreshRateSummary.percentageOf30HzFrames,
+ '60hz_frame_percentage': refreshRateSummary.percentageOf60HzFrames,
+ '90hz_frame_percentage': refreshRateSummary.percentageOf90HzFrames,
+ '120hz_frame_percentage': refreshRateSummary.percentageOf120HzFrames,
+ 'illegal_refresh_rate_frame_count': refreshRateSummary.framesWithIllegalRefreshRate.length,
};
timelineSummary.addAll(profilingSummary);
diff --git a/packages/flutter_driver/test/src/real_tests/timeline_summary_test.dart b/packages/flutter_driver/test/src/real_tests/timeline_summary_test.dart
index 38131e7..d93bd3e 100644
--- a/packages/flutter_driver/test/src/real_tests/timeline_summary_test.dart
+++ b/packages/flutter_driver/test/src/real_tests/timeline_summary_test.dart
@@ -3,10 +3,12 @@
// found in the LICENSE file.
import 'dart:convert' show json;
+import 'dart:math';
import 'package:file/file.dart';
import 'package:flutter_driver/flutter_driver.dart';
import 'package:flutter_driver/src/driver/profiling_summarizer.dart';
+import 'package:flutter_driver/src/driver/refresh_rate_summarizer.dart';
import 'package:flutter_driver/src/driver/scene_display_lag_summarizer.dart';
import 'package:flutter_driver/src/driver/vsync_frame_lag_summarizer.dart';
import 'package:path/path.dart' as path;
@@ -89,10 +91,14 @@
'ts': timeStamp,
};
- Map<String, dynamic> vsyncCallback(int timeStamp) => <String, dynamic>{
+ Map<String, dynamic> vsyncCallback(int timeStamp, {String phase = 'B', String startTime = '2750850055428', String endTime = '2750866722095'}) => <String, dynamic>{
'name': 'VsyncProcessCallback',
- 'ph': 'B',
+ 'ph': phase,
'ts': timeStamp,
+ 'args': <String, dynamic>{
+ 'StartTime': startTime,
+ 'TargetTime': endTime,
+ }
};
List<Map<String, dynamic>> _genGC(String name, int count, int startTime, int timeDiff) {
@@ -467,6 +473,11 @@
'99th_percentile_picture_cache_memory': 0.0,
'worst_picture_cache_memory': 0.0,
'total_ui_gc_time': 0.4,
+ '30hz_frame_percentage': 0,
+ '60hz_frame_percentage': 0,
+ '90hz_frame_percentage': 0,
+ '120hz_frame_percentage': 0,
+ 'illegal_refresh_rate_frame_count': 0,
},
);
});
@@ -582,6 +593,11 @@
'99th_percentile_picture_cache_memory': 0.0,
'worst_picture_cache_memory': 0.0,
'total_ui_gc_time': 0.4,
+ '30hz_frame_percentage': 0,
+ '60hz_frame_percentage': 100,
+ '90hz_frame_percentage': 0,
+ '120hz_frame_percentage': 0,
+ 'illegal_refresh_rate_frame_count': 0,
});
});
});
@@ -734,5 +750,173 @@
expect(summarizer.computePercentileVsyncFrameLag(99), 990);
});
});
+
+ group('RefreshRateSummarizer tests', () {
+
+ const double kCompareDelta = 0.01;
+ RefreshRateSummary _summarize(List<Map<String, dynamic>> traceEvents) {
+ final Timeline timeline = Timeline.fromJson(<String, dynamic>{
+ 'traceEvents': traceEvents,
+ });
+ return RefreshRateSummary(vsyncEvents: timeline.events!);
+ }
+
+ List<Map<String, dynamic>> _populateEvents({required int numberOfEvents, required int startTime, required int interval, required int margin}) {
+ final List<Map<String, dynamic>> events = <Map<String, dynamic>>[];
+ int startTimeInNanoseconds = startTime;
+ for (int i = 0; i < numberOfEvents; i ++) {
+ final int randomMargin = margin >= 1 ? (-margin + Random().nextInt(margin*2)) : 0;
+ final int endTime = startTimeInNanoseconds + interval + randomMargin;
+ events.add(vsyncCallback(0, startTime: startTimeInNanoseconds.toString(), endTime: endTime.toString()));
+ startTimeInNanoseconds = endTime;
+ }
+ return events;
+ }
+
+ test('Recognize 30 hz frames.', () async {
+ const int startTimeInNanoseconds = 2750850055430;
+ const int intervalInNanoseconds = 33333333;
+ // allow some margins
+ const int margin = 3000000;
+ final List<Map<String, dynamic>> events = _populateEvents(numberOfEvents: 100,
+ startTime: startTimeInNanoseconds,
+ interval: intervalInNanoseconds,
+ margin: margin,
+ );
+ final RefreshRateSummary summary = _summarize(events);
+ expect(summary.percentageOf30HzFrames, closeTo(100, kCompareDelta));
+ expect(summary.percentageOf60HzFrames, 0);
+ expect(summary.percentageOf90HzFrames, 0);
+ expect(summary.percentageOf120HzFrames, 0);
+ expect(summary.framesWithIllegalRefreshRate, isEmpty);
+ });
+
+ test('Recognize 60 hz frames.', () async {
+ const int startTimeInNanoseconds = 2750850055430;
+ const int intervalInNanoseconds = 16666666;
+ // allow some margins
+ const int margin = 1200000;
+ final List<Map<String, dynamic>> events = _populateEvents(numberOfEvents: 100,
+ startTime: startTimeInNanoseconds,
+ interval: intervalInNanoseconds,
+ margin: margin,
+ );
+
+ final RefreshRateSummary summary = _summarize(events);
+ expect(summary.percentageOf30HzFrames, 0);
+ expect(summary.percentageOf60HzFrames, closeTo(100, kCompareDelta));
+ expect(summary.percentageOf90HzFrames, 0);
+ expect(summary.percentageOf120HzFrames, 0);
+ expect(summary.framesWithIllegalRefreshRate, isEmpty);
+ });
+
+ test('Recognize 90 hz frames.', () async {
+ const int startTimeInNanoseconds = 2750850055430;
+ const int intervalInNanoseconds = 11111111;
+ // allow some margins
+ const int margin = 500000;
+ final List<Map<String, dynamic>> events = _populateEvents(numberOfEvents: 100,
+ startTime: startTimeInNanoseconds,
+ interval: intervalInNanoseconds,
+ margin: margin,
+ );
+
+ final RefreshRateSummary summary = _summarize(events);
+ expect(summary.percentageOf30HzFrames, 0);
+ expect(summary.percentageOf60HzFrames, 0);
+ expect(summary.percentageOf90HzFrames, closeTo(100, kCompareDelta));
+ expect(summary.percentageOf120HzFrames, 0);
+ expect(summary.framesWithIllegalRefreshRate, isEmpty);
+ });
+
+ test('Recognize 120 hz frames.', () async {
+ const int startTimeInNanoseconds = 2750850055430;
+ const int intervalInNanoseconds = 8333333;
+ // allow some margins
+ const int margin = 300000;
+ final List<Map<String, dynamic>> events = _populateEvents(numberOfEvents: 100,
+ startTime: startTimeInNanoseconds,
+ interval: intervalInNanoseconds,
+ margin: margin,
+ );
+ final RefreshRateSummary summary = _summarize(events);
+ expect(summary.percentageOf30HzFrames, 0);
+ expect(summary.percentageOf60HzFrames, 0);
+ expect(summary.percentageOf90HzFrames, 0);
+ expect(summary.percentageOf120HzFrames, closeTo(100, kCompareDelta));
+ expect(summary.framesWithIllegalRefreshRate, isEmpty);
+ });
+
+ test('Identify illegal refresh rates.', () async {
+ const int startTimeInNanoseconds = 2750850055430;
+ const int intervalInNanoseconds = 10000000;
+ final List<Map<String, dynamic>> events = _populateEvents(numberOfEvents: 1,
+ startTime: startTimeInNanoseconds,
+ interval: intervalInNanoseconds,
+ margin: 0,
+ );
+ final RefreshRateSummary summary = _summarize(events);
+ expect(summary.percentageOf30HzFrames, 0);
+ expect(summary.percentageOf60HzFrames, 0);
+ expect(summary.percentageOf90HzFrames, 0);
+ expect(summary.percentageOf120HzFrames, 0);
+ expect(summary.framesWithIllegalRefreshRate, isNotEmpty);
+ expect(summary.framesWithIllegalRefreshRate.first, closeTo(100, kCompareDelta));
+ });
+
+ test('Mixed refresh rates.', () async {
+
+ final List<Map<String, dynamic>> events = <Map<String, dynamic>>[];
+ const int num30Hz = 10;
+ const int num60Hz = 20;
+ const int num90Hz = 20;
+ const int num120Hz = 40;
+ const int numIllegal = 10;
+
+ // Add 30hz frames
+ events.addAll(_populateEvents(numberOfEvents: num30Hz,
+ startTime: 0,
+ interval: 32000000,
+ margin: 0,
+ ));
+
+ // Add 60hz frames
+ events.addAll(_populateEvents(numberOfEvents: num60Hz,
+ startTime: 0,
+ interval: 16000000,
+ margin: 0,
+ ));
+
+
+ // Add 90hz frames
+ events.addAll(_populateEvents(numberOfEvents: num90Hz,
+ startTime: 0,
+ interval: 11000000,
+ margin: 0,
+ ));
+
+ // Add 120hz frames
+ events.addAll(_populateEvents(numberOfEvents: num120Hz,
+ startTime: 0,
+ interval: 8000000,
+ margin: 0,
+ ));
+
+ // Add illegal refresh rate frames
+ events.addAll(_populateEvents(numberOfEvents: numIllegal,
+ startTime: 0,
+ interval: 60000,
+ margin: 0,
+ ));
+
+ final RefreshRateSummary summary = _summarize(events);
+ expect(summary.percentageOf30HzFrames, closeTo(num30Hz, kCompareDelta));
+ expect(summary.percentageOf60HzFrames, closeTo(num60Hz, kCompareDelta));
+ expect(summary.percentageOf90HzFrames, closeTo(num90Hz, kCompareDelta));
+ expect(summary.percentageOf120HzFrames, closeTo(num120Hz, kCompareDelta));
+ expect(summary.framesWithIllegalRefreshRate, isNotEmpty);
+ expect(summary.framesWithIllegalRefreshRate.length, 10);
+ });
+ });
});
}