[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); + }); + }); }); }