// 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 '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; import '../../common.dart'; void main() { group('TimelineSummary', () { TimelineSummary summarize(List<Map<String, dynamic>> testEvents) { return TimelineSummary.summarize(Timeline.fromJson(<String, dynamic>{ 'traceEvents': testEvents, })); } Map<String, dynamic> frameBegin(int timeStamp) => <String, dynamic>{ 'name': 'Frame', 'ph': 'B', 'ts': timeStamp, }; Map<String, dynamic> frameEnd(int timeStamp) => <String, dynamic>{ 'name': 'Frame', 'ph': 'E', 'ts': timeStamp, }; Map<String, dynamic> begin(int timeStamp) => <String, dynamic>{ 'name': 'GPURasterizer::Draw', 'ph': 'B', 'ts': timeStamp, }; Map<String, dynamic> end(int timeStamp) => <String, dynamic>{ 'name': 'GPURasterizer::Draw', 'ph': 'E', 'ts': timeStamp, }; Map<String, dynamic> lagBegin(int timeStamp, int vsyncsMissed) => <String, dynamic>{ 'name': 'SceneDisplayLag', 'ph': 'b', 'ts': timeStamp, 'args': <String, String>{ 'vsync_transitions_missed': vsyncsMissed.toString(), }, }; Map<String, dynamic> lagEnd(int timeStamp, int vsyncsMissed) => <String, dynamic>{ 'name': 'SceneDisplayLag', 'ph': 'e', 'ts': timeStamp, 'args': <String, String>{ 'vsync_transitions_missed': vsyncsMissed.toString(), }, }; Map<String, dynamic> cpuUsage(int timeStamp, double cpuUsage) => <String, dynamic>{ 'cat': 'embedder', 'name': 'CpuUsage', 'ts': timeStamp, 'args': <String, String>{ 'total_cpu_usage': cpuUsage.toString(), }, }; Map<String, dynamic> memoryUsage(int timeStamp, double dirty, double shared) => <String, dynamic>{ 'cat': 'embedder', 'name': 'MemoryUsage', 'ts': timeStamp, 'args': <String, String>{ 'owned_shared_memory_usage': shared.toString(), 'dirty_memory_usage': dirty.toString(), }, }; Map<String, dynamic> platformVsync(int timeStamp) => <String, dynamic>{ 'name': 'VSYNC', 'ph': 'B', 'ts': timeStamp, }; Map<String, dynamic> vsyncCallback(int timeStamp, {String phase = 'B', String startTime = '2750850055428', String endTime = '2750866722095'}) => <String, dynamic>{ 'name': 'VsyncProcessCallback', 'ph': phase, 'ts': timeStamp, 'args': <String, dynamic>{ 'StartTime': startTime, 'TargetTime': endTime, }, }; List<Map<String, dynamic>> genGC(String name, int count, int startTime, int timeDiff) { int ts = startTime; bool begin = true; final List<Map<String, dynamic>> ret = <Map<String, dynamic>>[]; for (int i = 0; i < count; i++) { ret.add(<String, dynamic>{ 'name': name, 'cat': 'GC', 'tid': 19695, 'pid': 19650, 'ts': ts, 'tts': ts, 'ph': begin ? 'B' : 'E', 'args': <String, dynamic>{ 'isolateGroupId': 'isolateGroups/10824099774666259225', }, }); ts = ts + timeDiff; begin = !begin; } return ret; } List<Map<String, dynamic>> newGenGC(int count, int startTime, int timeDiff) { return genGC('CollectNewGeneration', count, startTime, timeDiff); } List<Map<String, dynamic>> oldGenGC(int count, int startTime, int timeDiff) { return genGC('CollectOldGeneration', count, startTime, timeDiff); } List<Map<String, dynamic>> rasterizeTimeSequenceInMillis(List<int> sequence) { final List<Map<String, dynamic>> result = <Map<String, dynamic>>[]; int t = 0; for (final int duration in sequence) { result.add(begin(t)); t += duration * 1000; result.add(end(t)); } return result; } group('frame_count', () { test('counts frames', () { expect( summarize(<Map<String, dynamic>>[ frameBegin(1000), frameEnd(2000), frameBegin(3000), frameEnd(5000), ]).countFrames(), 2, ); }); }); group('average_frame_build_time_millis', () { test('throws when there is no data', () { expect( () => summarize(<Map<String, dynamic>>[]).computeAverageFrameBuildTimeMillis(), throwsA( isA<StateError>() .having((StateError e) => e.message, 'message', contains('The TimelineSummary had no events to summarize.'), )), ); }); test('computes average frame build time in milliseconds', () { expect( summarize(<Map<String, dynamic>>[ frameBegin(1000), frameEnd(2000), frameBegin(3000), frameEnd(5000), ]).computeAverageFrameBuildTimeMillis(), 1.5, ); }); test('skips leading "end" events', () { expect( summarize(<Map<String, dynamic>>[ frameEnd(1000), frameBegin(2000), frameEnd(4000), ]).computeAverageFrameBuildTimeMillis(), 2.0, ); }); test('skips trailing "begin" events', () { expect( summarize(<Map<String, dynamic>>[ frameBegin(2000), frameEnd(4000), frameBegin(5000), ]).computeAverageFrameBuildTimeMillis(), 2.0, ); }); // see https://github.com/flutter/flutter/issues/54095. test('ignore multiple "end" events', () { expect( summarize(<Map<String, dynamic>>[ frameBegin(2000), frameEnd(4000), frameEnd(4300), // rogue frame end. frameBegin(5000), frameEnd(6000), ]).computeAverageFrameBuildTimeMillis(), 1.5, ); }); test('pick latest when there are multiple "begin" events', () { expect( summarize(<Map<String, dynamic>>[ frameBegin(1000), // rogue frame begin. frameBegin(2000), frameEnd(4000), frameEnd(4300), // rogue frame end. frameBegin(4400), // rogue frame begin. frameBegin(5000), frameEnd(6000), ]).computeAverageFrameBuildTimeMillis(), 1.5, ); }); }); group('worst_frame_build_time_millis', () { test('throws when there is no data', () { expect( () => summarize(<Map<String, dynamic>>[]).computeWorstFrameBuildTimeMillis(), throwsA( isA<StateError>() .having((StateError e) => e.message, 'message', contains('The TimelineSummary had no events to summarize.'), )), ); }); test('computes worst frame build time in milliseconds', () { expect( summarize(<Map<String, dynamic>>[ frameBegin(1000), frameEnd(2000), frameBegin(3000), frameEnd(5000), ]).computeWorstFrameBuildTimeMillis(), 2.0, ); expect( summarize(<Map<String, dynamic>>[ frameBegin(3000), frameEnd(5000), frameBegin(1000), frameEnd(2000), ]).computeWorstFrameBuildTimeMillis(), 2.0, ); }); test('skips leading "end" events', () { expect( summarize(<Map<String, dynamic>>[ frameEnd(1000), frameBegin(2000), frameEnd(4000), ]).computeWorstFrameBuildTimeMillis(), 2.0, ); }); test('skips trailing "begin" events', () { expect( summarize(<Map<String, dynamic>>[ frameBegin(2000), frameEnd(4000), frameBegin(5000), ]).computeWorstFrameBuildTimeMillis(), 2.0, ); }); }); group('computeMissedFrameBuildBudgetCount', () { test('computes the number of missed build budgets', () { final TimelineSummary summary = summarize(<Map<String, dynamic>>[ frameBegin(1000), frameEnd(18000), frameBegin(19000), frameEnd(28000), frameBegin(29000), frameEnd(47000), ]); expect(summary.countFrames(), 3); expect(summary.computeMissedFrameBuildBudgetCount(), 2); }); }); group('average_frame_rasterizer_time_millis', () { test('throws when there is no data', () { expect( () => summarize(<Map<String, dynamic>>[]).computeAverageFrameRasterizerTimeMillis(), throwsA( isA<StateError>() .having((StateError e) => e.message, 'message', contains('The TimelineSummary had no events to summarize.'), )), ); }); test('computes average frame rasterizer time in milliseconds', () { expect( summarize(<Map<String, dynamic>>[ begin(1000), end(2000), begin(3000), end(5000), ]).computeAverageFrameRasterizerTimeMillis(), 1.5, ); }); test('skips leading "end" events', () { expect( summarize(<Map<String, dynamic>>[ end(1000), begin(2000), end(4000), ]).computeAverageFrameRasterizerTimeMillis(), 2.0, ); }); test('skips trailing "begin" events', () { expect( summarize(<Map<String, dynamic>>[ begin(2000), end(4000), begin(5000), ]).computeAverageFrameRasterizerTimeMillis(), 2.0, ); }); }); group('worst_frame_rasterizer_time_millis', () { test('throws when there is no data', () { expect( () => summarize(<Map<String, dynamic>>[]).computeWorstFrameRasterizerTimeMillis(), throwsA( isA<StateError>() .having((StateError e) => e.message, 'message', contains('The TimelineSummary had no events to summarize.'), )), ); }); test('computes worst frame rasterizer time in milliseconds', () { expect( summarize(<Map<String, dynamic>>[ begin(1000), end(2000), begin(3000), end(5000), ]).computeWorstFrameRasterizerTimeMillis(), 2.0, ); expect( summarize(<Map<String, dynamic>>[ begin(3000), end(5000), begin(1000), end(2000), ]).computeWorstFrameRasterizerTimeMillis(), 2.0, ); }); test('skips leading "end" events', () { expect( summarize(<Map<String, dynamic>>[ end(1000), begin(2000), end(4000), ]).computeWorstFrameRasterizerTimeMillis(), 2.0, ); }); test('skips trailing "begin" events', () { expect( summarize(<Map<String, dynamic>>[ begin(2000), end(4000), begin(5000), ]).computeWorstFrameRasterizerTimeMillis(), 2.0, ); }); }); group('percentile_frame_rasterizer_time_millis', () { test('throws when there is no data', () { expect( () => summarize(<Map<String, dynamic>>[]).computePercentileFrameRasterizerTimeMillis(90.0), throwsA( isA<StateError>() .having((StateError e) => e.message, 'message', contains('The TimelineSummary had no events to summarize.'), )), ); }); const List<List<int>> sequences = <List<int>>[ <int>[1, 2, 3, 4, 5, 6, 7, 8, 9, 10], <int>[1, 2, 3, 4, 5], <int>[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20], ]; const List<int> p90s = <int>[ 9, 5, 18, ]; test('computes 90th frame rasterizer time in milliseconds', () { for (int i = 0; i < sequences.length; ++i) { expect( summarize(rasterizeTimeSequenceInMillis(sequences[i])).computePercentileFrameRasterizerTimeMillis(90.0), p90s[i], ); } }); test('compute 99th frame rasterizer time in milliseconds', () { final List<int> sequence = <int>[]; for (int i = 1; i <= 100; ++i) { sequence.add(i); } expect( summarize(rasterizeTimeSequenceInMillis(sequence)).computePercentileFrameRasterizerTimeMillis(99.0), 99, ); }); }); group('computeMissedFrameRasterizerBudgetCount', () { test('computes the number of missed rasterizer budgets', () { final TimelineSummary summary = summarize(<Map<String, dynamic>>[ begin(1000), end(18000), begin(19000), end(28000), begin(29000), end(47000), ]); expect(summary.computeMissedFrameRasterizerBudgetCount(), 2); }); }); group('summaryJson', () { test('computes and returns summary as JSON', () { expect( summarize(<Map<String, dynamic>>[ begin(1000), end(19000), begin(19000), end(29000), begin(29000), end(49000), ...newGenGC(4, 10, 100), ...oldGenGC(5, 10000, 100), frameBegin(1000), frameEnd(18000), frameBegin(19000), frameEnd(28000), frameBegin(29000), frameEnd(48000), ]).summaryJson, <String, dynamic>{ 'average_frame_build_time_millis': 15.0, '90th_percentile_frame_build_time_millis': 19.0, '99th_percentile_frame_build_time_millis': 19.0, 'worst_frame_build_time_millis': 19.0, 'missed_frame_build_budget_count': 2, 'average_frame_rasterizer_time_millis': 16.0, '90th_percentile_frame_rasterizer_time_millis': 20.0, '99th_percentile_frame_rasterizer_time_millis': 20.0, 'worst_frame_rasterizer_time_millis': 20.0, 'missed_frame_rasterizer_budget_count': 2, 'frame_count': 3, 'frame_rasterizer_count': 3, 'new_gen_gc_count': 4, 'old_gen_gc_count': 5, 'frame_build_times': <int>[17000, 9000, 19000], 'frame_rasterizer_times': <int>[18000, 10000, 20000], 'frame_begin_times': <int>[0, 18000, 28000], 'frame_rasterizer_begin_times': <int>[0, 18000, 28000], 'average_vsync_transitions_missed': 0.0, '90th_percentile_vsync_transitions_missed': 0.0, '99th_percentile_vsync_transitions_missed': 0.0, 'average_vsync_frame_lag': 0.0, '90th_percentile_vsync_frame_lag': 0.0, '99th_percentile_vsync_frame_lag': 0.0, 'average_layer_cache_count': 0.0, '90th_percentile_layer_cache_count': 0.0, '99th_percentile_layer_cache_count': 0.0, 'worst_layer_cache_count': 0.0, 'average_layer_cache_memory': 0.0, '90th_percentile_layer_cache_memory': 0.0, '99th_percentile_layer_cache_memory': 0.0, 'worst_layer_cache_memory': 0.0, 'average_picture_cache_count': 0.0, '90th_percentile_picture_cache_count': 0.0, '99th_percentile_picture_cache_count': 0.0, 'worst_picture_cache_count': 0.0, 'average_picture_cache_memory': 0.0, '90th_percentile_picture_cache_memory': 0.0, '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, '80hz_frame_percentage': 0, '90hz_frame_percentage': 0, '120hz_frame_percentage': 0, 'illegal_refresh_rate_frame_count': 0, }, ); }); }); group('writeTimelineToFile', () { late Directory tempDir; setUp(() { useMemoryFileSystemForTesting(); tempDir = fs.systemTempDirectory.createTempSync('flutter_driver_test.'); }); tearDown(() { tryToDelete(tempDir); restoreFileSystem(); }); test('writes timeline to JSON file without summary', () async { await summarize(<Map<String, String>>[<String, String>{'foo': 'bar'}]) .writeTimelineToFile('test', destinationDirectory: tempDir.path, includeSummary: false); final String written = await fs.file(path.join(tempDir.path, 'test.timeline.json')).readAsString(); expect(written, '{"traceEvents":[{"foo":"bar"}]}'); }); test('writes timeline to JSON file with summary', () async { await summarize(<Map<String, dynamic>>[ <String, String>{'foo': 'bar'}, begin(1000), end(19000), frameBegin(1000), frameEnd(18000), ]).writeTimelineToFile( 'test', destinationDirectory: tempDir.path, ); final String written = await fs.file(path.join(tempDir.path, 'test.timeline.json')).readAsString(); expect( written, '{"traceEvents":[{"foo":"bar"},' '{"name":"GPURasterizer::Draw","ph":"B","ts":1000},' '{"name":"GPURasterizer::Draw","ph":"E","ts":19000},' '{"name":"Frame","ph":"B","ts":1000},' '{"name":"Frame","ph":"E","ts":18000}]}', ); }); test('writes summary to JSON file', () async { await summarize(<Map<String, dynamic>>[ begin(1000), end(19000), begin(19000), end(29000), begin(29000), end(49000), frameBegin(1000), frameEnd(18000), frameBegin(19000), frameEnd(28000), frameBegin(29000), frameEnd(48000), lagBegin(1000, 4), lagEnd(2000, 4), lagBegin(1200, 12), lagEnd(2400, 12), lagBegin(4200, 8), lagEnd(9400, 8), ...newGenGC(4, 10, 100), ...oldGenGC(5, 10000, 100), cpuUsage(5000, 20), cpuUsage(5010, 60), memoryUsage(6000, 20, 40), memoryUsage(6100, 30, 45), platformVsync(7000), vsyncCallback(7500), ]).writeTimelineToFile('test', destinationDirectory: tempDir.path); final String written = await fs.file(path.join(tempDir.path, 'test.timeline_summary.json')).readAsString(); expect(json.decode(written), <String, dynamic>{ 'average_frame_build_time_millis': 15.0, 'worst_frame_build_time_millis': 19.0, '90th_percentile_frame_build_time_millis': 19.0, '99th_percentile_frame_build_time_millis': 19.0, 'missed_frame_build_budget_count': 2, 'average_frame_rasterizer_time_millis': 16.0, '90th_percentile_frame_rasterizer_time_millis': 20.0, '99th_percentile_frame_rasterizer_time_millis': 20.0, 'worst_frame_rasterizer_time_millis': 20.0, 'missed_frame_rasterizer_budget_count': 2, 'frame_count': 3, 'frame_rasterizer_count': 3, 'new_gen_gc_count': 4, 'old_gen_gc_count': 5, 'frame_build_times': <int>[17000, 9000, 19000], 'frame_rasterizer_times': <int>[18000, 10000, 20000], 'frame_begin_times': <int>[0, 18000, 28000], 'frame_rasterizer_begin_times': <int>[0, 18000, 28000], 'average_vsync_transitions_missed': 8.0, '90th_percentile_vsync_transitions_missed': 12.0, '99th_percentile_vsync_transitions_missed': 12.0, 'average_vsync_frame_lag': 500.0, '90th_percentile_vsync_frame_lag': 500.0, '99th_percentile_vsync_frame_lag': 500.0, 'average_cpu_usage': 40.0, '90th_percentile_cpu_usage': 60.0, '99th_percentile_cpu_usage': 60.0, 'average_memory_usage': 67.5, '90th_percentile_memory_usage': 75.0, '99th_percentile_memory_usage': 75.0, 'average_layer_cache_count': 0.0, '90th_percentile_layer_cache_count': 0.0, '99th_percentile_layer_cache_count': 0.0, 'worst_layer_cache_count': 0.0, 'average_layer_cache_memory': 0.0, '90th_percentile_layer_cache_memory': 0.0, '99th_percentile_layer_cache_memory': 0.0, 'worst_layer_cache_memory': 0.0, 'average_picture_cache_count': 0.0, '90th_percentile_picture_cache_count': 0.0, '99th_percentile_picture_cache_count': 0.0, 'worst_picture_cache_count': 0.0, 'average_picture_cache_memory': 0.0, '90th_percentile_picture_cache_memory': 0.0, '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, '80hz_frame_percentage': 0, '90hz_frame_percentage': 0, '120hz_frame_percentage': 0, 'illegal_refresh_rate_frame_count': 0, }); }); }); group('SceneDisplayLagSummarizer tests', () { SceneDisplayLagSummarizer summarize(List<Map<String, dynamic>> traceEvents) { final Timeline timeline = Timeline.fromJson(<String, dynamic>{ 'traceEvents': traceEvents, }); return SceneDisplayLagSummarizer(timeline.events!); } test('average_vsyncs_missed', () async { final SceneDisplayLagSummarizer summarizer = summarize(<Map<String, dynamic>>[ lagBegin(1000, 4), lagEnd(2000, 4), lagBegin(1200, 12), lagEnd(2400, 12), lagBegin(4200, 8), lagEnd(9400, 8), ]); expect(summarizer.computeAverageVsyncTransitionsMissed(), 8.0); }); test('all stats are 0 for 0 missed transitions', () async { final SceneDisplayLagSummarizer summarizer = summarize(<Map<String, dynamic>>[]); expect(summarizer.computeAverageVsyncTransitionsMissed(), 0.0); expect(summarizer.computePercentileVsyncTransitionsMissed(90.0), 0.0); expect(summarizer.computePercentileVsyncTransitionsMissed(99.0), 0.0); }); test('90th_percentile_vsyncs_missed', () async { final SceneDisplayLagSummarizer summarizer = summarize(<Map<String, dynamic>>[ lagBegin(1000, 4), lagEnd(2000, 4), lagBegin(1200, 12), lagEnd(2400, 12), lagBegin(4200, 8), lagEnd(9400, 8), lagBegin(6100, 14), lagEnd(11000, 14), lagBegin(7100, 16), lagEnd(11500, 16), lagBegin(7400, 11), lagEnd(13000, 11), lagBegin(8200, 27), lagEnd(14100, 27), lagBegin(8700, 7), lagEnd(14300, 7), lagBegin(24200, 4187), lagEnd(39400, 4187), ]); expect(summarizer.computePercentileVsyncTransitionsMissed(90), 27.0); }); test('99th_percentile_vsyncs_missed', () async { final SceneDisplayLagSummarizer summarizer = summarize(<Map<String, dynamic>>[ lagBegin(1000, 4), lagEnd(2000, 4), lagBegin(1200, 12), lagEnd(2400, 12), lagBegin(4200, 8), lagEnd(9400, 8), lagBegin(6100, 14), lagEnd(11000, 14), lagBegin(24200, 4187), lagEnd(39400, 4187), ]); expect(summarizer.computePercentileVsyncTransitionsMissed(99), 4187.0); }); }); group('ProfilingSummarizer tests', () { ProfilingSummarizer summarize(List<Map<String, dynamic>> traceEvents) { final Timeline timeline = Timeline.fromJson(<String, dynamic>{ 'traceEvents': traceEvents, }); return ProfilingSummarizer.fromEvents(timeline.events!); } test('has_both_cpu_and_memory_usage', () async { final ProfilingSummarizer summarizer = summarize(<Map<String, dynamic>>[ cpuUsage(0, 10), memoryUsage(0, 6, 10), cpuUsage(0, 12), memoryUsage(0, 8, 40), ]); expect(summarizer.computeAverage(ProfileType.CPU), 11.0); expect(summarizer.computeAverage(ProfileType.Memory), 32.0); }); test('has_only_memory_usage', () async { final ProfilingSummarizer summarizer = summarize(<Map<String, dynamic>>[ memoryUsage(0, 6, 10), memoryUsage(0, 8, 40), ]); expect(summarizer.computeAverage(ProfileType.Memory), 32.0); expect(summarizer.summarize().containsKey('average_cpu_usage'), false); }); test('90th_percentile_cpu_usage', () async { final ProfilingSummarizer summarizer = summarize(<Map<String, dynamic>>[ cpuUsage(0, 10), cpuUsage(1, 20), cpuUsage(2, 20), cpuUsage(3, 80), cpuUsage(4, 70), cpuUsage(4, 72), cpuUsage(4, 85), cpuUsage(4, 100), ]); expect(summarizer.computePercentile(ProfileType.CPU, 90), 85.0); }); }); group('VsyncFrameLagSummarizer tests', () { VsyncFrameLagSummarizer summarize(List<Map<String, dynamic>> traceEvents) { final Timeline timeline = Timeline.fromJson(<String, dynamic>{ 'traceEvents': traceEvents, }); return VsyncFrameLagSummarizer(timeline.events!); } test('average_vsync_frame_lag', () async { final VsyncFrameLagSummarizer summarizer = summarize(<Map<String, dynamic>>[ platformVsync(10), vsyncCallback(12), platformVsync(16), vsyncCallback(29), ]); expect(summarizer.computeAverageVsyncFrameLag(), 7.5); }); test('malformed_event_ordering', () async { final VsyncFrameLagSummarizer summarizer = summarize(<Map<String, dynamic>>[ vsyncCallback(10), platformVsync(10), ]); expect(summarizer.computeAverageVsyncFrameLag(), 0); expect(summarizer.computePercentileVsyncFrameLag(80), 0); }); test('penalize_consecutive_vsyncs', () async { final VsyncFrameLagSummarizer summarizer = summarize(<Map<String, dynamic>>[ platformVsync(10), platformVsync(12), ]); expect(summarizer.computeAverageVsyncFrameLag(), 2); }); test('pick_nearest_platform_vsync', () async { final VsyncFrameLagSummarizer summarizer = summarize(<Map<String, dynamic>>[ platformVsync(10), platformVsync(12), vsyncCallback(18), ]); expect(summarizer.computeAverageVsyncFrameLag(), 4); }); test('percentile_vsync_frame_lag', () async { final List<Map<String, dynamic>> events = <Map<String, dynamic>>[]; int ts = 100; for (int i = 0; i < 100; i++) { events.add(platformVsync(ts)); ts = ts + 10 * (i + 1); events.add(vsyncCallback(ts)); } final VsyncFrameLagSummarizer summarizer = summarize(events); expect(summarizer.computePercentileVsyncFrameLag(90), 890); expect(summarizer.computePercentileVsyncFrameLag(99), 990); }); }); group('RefreshRateSummarizer tests', () { const double kCompareDelta = 0.01; RefreshRateSummary summarizeRefresh(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 = summarizeRefresh(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 = summarizeRefresh(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 = summarizeRefresh(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 = summarizeRefresh(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 = summarizeRefresh(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 num80Hz = 20; const int num90Hz = 20; const int num120Hz = 40; const int numIllegal = 10; const int totalFrames = num30Hz + num60Hz + num80Hz + num90Hz + num120Hz + numIllegal; // 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 80hz frames events.addAll(populateEvents(numberOfEvents: num80Hz, startTime: 0, interval: 12000000, 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 = summarizeRefresh(events); expect(summary.percentageOf30HzFrames, closeTo(num30Hz/totalFrames*100, kCompareDelta)); expect(summary.percentageOf60HzFrames, closeTo(num60Hz/totalFrames*100, kCompareDelta)); expect(summary.percentageOf80HzFrames, closeTo(num80Hz/totalFrames*100, kCompareDelta)); expect(summary.percentageOf90HzFrames, closeTo(num90Hz/totalFrames*100, kCompareDelta)); expect(summary.percentageOf120HzFrames, closeTo(num120Hz/totalFrames*100, kCompareDelta)); expect(summary.framesWithIllegalRefreshRate, isNotEmpty); expect(summary.framesWithIllegalRefreshRate.length, 10); }); }); }); }