| // 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); |
| }); |
| }); |
| }); |
| } |