|
| 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 'dart:async'; |
| 6 | +import 'dart:ui'; |
| 7 | + |
| 8 | +import 'package:flutter/scheduler.dart'; |
| 9 | +import 'package:flutter_test/flutter_test.dart'; |
| 10 | +import 'package:flutter/widgets.dart'; |
| 11 | + |
| 12 | +import 'package:e2e/e2e.dart'; |
| 13 | + |
| 14 | +/// The maximum amount of time considered safe to spend for a frame's build |
| 15 | +/// phase. Anything past that is in the danger of missing the frame as 60FPS. |
| 16 | +/// |
| 17 | +/// Changing this doesn't re-evaluate existing summary. |
| 18 | +Duration kBuildBudget = const Duration(milliseconds: 16); |
| 19 | +// TODO(CareF): Automatically calculate the refresh budget (#61958) |
| 20 | + |
| 21 | +/// Usually it's recommended to limit callbacks of the test to [WidgetController] |
| 22 | +/// API so it can be more universally used. |
| 23 | +typedef ControlCallback = Future<void> Function(WidgetController controller); |
| 24 | + |
| 25 | +bool _firstRun = true; |
| 26 | + |
| 27 | +/// watches the [FrameTiming] of `action` and report it to the e2e binding. |
| 28 | +Future<void> watchPerformance( |
| 29 | + E2EWidgetsFlutterBinding binding, |
| 30 | + Future<void> action(), |
| 31 | +) async { |
| 32 | + assert(() { |
| 33 | + if (_firstRun) { |
| 34 | + debugPrint(kDebugWarning); |
| 35 | + _firstRun = false; |
| 36 | + } |
| 37 | + return true; |
| 38 | + }()); |
| 39 | + final List<FrameTiming> frameTimings = <FrameTiming>[]; |
| 40 | + final TimingsCallback watcher = frameTimings.addAll; |
| 41 | + binding.addTimingsCallback(watcher); |
| 42 | + await action(); |
| 43 | + binding.removeTimingsCallback(watcher); |
| 44 | + // TODO(CareF): determine if it's running on firebase and report metric online |
| 45 | + final FrameTimingSummarizer frameTimes = FrameTimingSummarizer(frameTimings); |
| 46 | + binding.reportData = <String, dynamic>{'performance': frameTimes.summary}; |
| 47 | +} |
| 48 | + |
| 49 | +/// This class and summarizes a list of [FrameTiming] for the performance |
| 50 | +/// metrics. |
| 51 | +class FrameTimingSummarizer { |
| 52 | + /// Summarize `data` to frame build time and frame rasterizer time statistics. |
| 53 | + /// |
| 54 | + /// See [TimelineSummary.summaryJson] for detail. |
| 55 | + factory FrameTimingSummarizer(List<FrameTiming> data) { |
| 56 | + assert(data != null); |
| 57 | + assert(data.isNotEmpty); |
| 58 | + final List<Duration> frameBuildTime = List<Duration>.unmodifiable( |
| 59 | + data.map<Duration>((FrameTiming datum) => datum.buildDuration), |
| 60 | + ); |
| 61 | + final List<Duration> frameBuildTimeSorted = List<Duration>.from(frameBuildTime)..sort(); |
| 62 | + final List<Duration> frameRasterizerTime = List<Duration>.unmodifiable( |
| 63 | + data.map<Duration>((FrameTiming datum) => datum.rasterDuration), |
| 64 | + ); |
| 65 | + final List<Duration> frameRasterizerTimeSorted = List<Duration>.from(frameRasterizerTime)..sort(); |
| 66 | + final Duration Function(Duration, Duration) add = (Duration a, Duration b) => a + b; |
| 67 | + return FrameTimingSummarizer._( |
| 68 | + frameBuildTime: frameBuildTime, |
| 69 | + frameRasterizerTime: frameRasterizerTime, |
| 70 | + // This avarage calculation is microsecond precision, which is fine |
| 71 | + // because typical values of these times are milliseconds. |
| 72 | + averageFrameBuildTime: frameBuildTime.reduce(add) ~/ data.length, |
| 73 | + p90FrameBuildTime: _findPercentile(frameBuildTimeSorted, 0.90), |
| 74 | + p99FrameBuildTime: _findPercentile(frameBuildTimeSorted, 0.99), |
| 75 | + worstFrameBuildTime: frameBuildTimeSorted.last, |
| 76 | + missedFrameBuildBudget: _countExceed(frameBuildTimeSorted, kBuildBudget), |
| 77 | + averageFrameRasterizerTime: frameRasterizerTime.reduce(add) ~/ data.length, |
| 78 | + p90FrameRasterizerTime: _findPercentile(frameRasterizerTimeSorted, 0.90), |
| 79 | + p99FrameRasterizerTime: _findPercentile(frameRasterizerTimeSorted, 0.99), |
| 80 | + worstFrameRasterizerTime: frameRasterizerTimeSorted.last, |
| 81 | + missedFrameRasterizerBudget: _countExceed(frameRasterizerTimeSorted, kBuildBudget), |
| 82 | + ); |
| 83 | + } |
| 84 | + |
| 85 | + const FrameTimingSummarizer._({ |
| 86 | + @required this.frameBuildTime, |
| 87 | + @required this.frameRasterizerTime, |
| 88 | + @required this.averageFrameBuildTime, |
| 89 | + @required this.p90FrameBuildTime, |
| 90 | + @required this.p99FrameBuildTime, |
| 91 | + @required this.worstFrameBuildTime, |
| 92 | + @required this.missedFrameBuildBudget, |
| 93 | + @required this.averageFrameRasterizerTime, |
| 94 | + @required this.p90FrameRasterizerTime, |
| 95 | + @required this.p99FrameRasterizerTime, |
| 96 | + @required this.worstFrameRasterizerTime, |
| 97 | + @required this.missedFrameRasterizerBudget |
| 98 | + }); |
| 99 | + |
| 100 | + /// List of frame build time in microseconds |
| 101 | + final List<Duration> frameBuildTime; |
| 102 | + |
| 103 | + /// List of frame rasterizer time in microseconds |
| 104 | + final List<Duration> frameRasterizerTime; |
| 105 | + |
| 106 | + /// The average value of [frameBuildTime] in milliseconds. |
| 107 | + final Duration averageFrameBuildTime; |
| 108 | + |
| 109 | + /// The 90-th percentile value of [frameBuildTime] in milliseconds |
| 110 | + final Duration p90FrameBuildTime; |
| 111 | + |
| 112 | + /// The 99-th percentile value of [frameBuildTime] in milliseconds |
| 113 | + final Duration p99FrameBuildTime; |
| 114 | + |
| 115 | + /// The largest value of [frameBuildTime] in milliseconds |
| 116 | + final Duration worstFrameBuildTime; |
| 117 | + |
| 118 | + /// Number of items in [frameBuildTime] that's greater than [kBuildBudget] |
| 119 | + final int missedFrameBuildBudget; |
| 120 | + |
| 121 | + /// The average value of [frameRasterizerTime] in milliseconds. |
| 122 | + final Duration averageFrameRasterizerTime; |
| 123 | + |
| 124 | + /// The 90-th percentile value of [frameRasterizerTime] in milliseconds. |
| 125 | + final Duration p90FrameRasterizerTime; |
| 126 | + |
| 127 | + /// The 99-th percentile value of [frameRasterizerTime] in milliseconds. |
| 128 | + final Duration p99FrameRasterizerTime; |
| 129 | + |
| 130 | + /// The largest value of [frameRasterizerTime] in milliseconds. |
| 131 | + final Duration worstFrameRasterizerTime; |
| 132 | + |
| 133 | + /// Number of items in [frameRasterizerTime] that's greater than [kBuildBudget] |
| 134 | + final int missedFrameRasterizerBudget; |
| 135 | + |
| 136 | + /// Convert the summary result to a json object. |
| 137 | + /// |
| 138 | + /// See [TimelineSummary.summaryJson] for detail. |
| 139 | + Map<String, dynamic> get summary => <String, dynamic>{ |
| 140 | + 'average_frame_build_time_millis': |
| 141 | + averageFrameBuildTime.inMicroseconds / 1E3, |
| 142 | + '90th_percentile_frame_build_time_millis': |
| 143 | + p90FrameBuildTime.inMicroseconds / 1E3, |
| 144 | + '99th_percentile_frame_build_time_millis': |
| 145 | + p99FrameBuildTime.inMicroseconds / 1E3, |
| 146 | + 'worst_frame_build_time_millis': |
| 147 | + worstFrameBuildTime.inMicroseconds / 1E3, |
| 148 | + 'missed_frame_build_budget_count': missedFrameBuildBudget, |
| 149 | + 'average_frame_rasterizer_time_millis': |
| 150 | + averageFrameRasterizerTime.inMicroseconds / 1E3, |
| 151 | + '90th_percentile_frame_rasterizer_time_millis': |
| 152 | + p90FrameRasterizerTime.inMicroseconds / 1E3, |
| 153 | + '99th_percentile_frame_rasterizer_time_millis': |
| 154 | + p99FrameRasterizerTime.inMicroseconds / 1E3, |
| 155 | + 'worst_frame_rasterizer_time_millis': |
| 156 | + worstFrameRasterizerTime.inMicroseconds / 1E3, |
| 157 | + 'missed_frame_rasterizer_budget_count': missedFrameRasterizerBudget, |
| 158 | + 'frame_count': frameBuildTime.length, |
| 159 | + 'frame_build_times': frameBuildTime |
| 160 | + .map<int>((Duration datum) => datum.inMicroseconds).toList(), |
| 161 | + 'frame_rasterizer_times': frameRasterizerTime |
| 162 | + .map<int>((Duration datum) => datum.inMicroseconds).toList(), |
| 163 | + }; |
| 164 | +} |
| 165 | + |
| 166 | +// The following helper functions require data sorted |
| 167 | + |
| 168 | +// return the 100*p-th percentile of the data |
| 169 | +T _findPercentile<T>(List<T> data, double p) { |
| 170 | + assert(p >= 0 && p <= 1); |
| 171 | + return data[((data.length - 1) * p).round()]; |
| 172 | +} |
| 173 | + |
| 174 | +// return the number of items in data that > threshold |
| 175 | +int _countExceed<T extends Comparable<T>>(List<T> data, T threshold) { |
| 176 | + return data.length - data.indexWhere((T datum) => datum.compareTo(threshold) > 0); |
| 177 | +} |
0 commit comments