|
| 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 | +bool _firstRun = true; |
| 22 | + |
| 23 | +/// The warning message to show when a benchmark is performed with assert on. |
| 24 | +/// TODO(CareF) remove this and update pubspect when flutter/flutter#61509 is |
| 25 | +/// in released version. |
| 26 | +const String kDebugWarning = ''' |
| 27 | +┏╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍┓ |
| 28 | +┇ ⚠ THIS BENCHMARK IS BEING RUN IN DEBUG MODE ⚠ ┇ |
| 29 | +┡╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍┦ |
| 30 | +│ │ |
| 31 | +│ Numbers obtained from a benchmark while asserts are │ |
| 32 | +│ enabled will not accurately reflect the performance │ |
| 33 | +│ that will be experienced by end users using release ╎ |
| 34 | +│ builds. Benchmarks should be run using this command ╎ |
| 35 | +│ line: "flutter run --profile test.dart" or ┊ |
| 36 | +│ or "flutter drive --profile -t test.dart". ┊ |
| 37 | +│ ┊ |
| 38 | +└─────────────────────────────────────────────────╌┄┈ 🐢 |
| 39 | +'''; |
| 40 | + |
| 41 | +/// watches the [FrameTiming] of `action` and report it to the e2e binding. |
| 42 | +Future<void> watchPerformance( |
| 43 | + E2EWidgetsFlutterBinding binding, |
| 44 | + Future<void> action(), { |
| 45 | + String reportKey = 'performance', |
| 46 | +}) async { |
| 47 | + assert(() { |
| 48 | + if (_firstRun) { |
| 49 | + debugPrint(kDebugWarning); |
| 50 | + _firstRun = false; |
| 51 | + } |
| 52 | + return true; |
| 53 | + }()); |
| 54 | + final List<FrameTiming> frameTimings = <FrameTiming>[]; |
| 55 | + final TimingsCallback watcher = frameTimings.addAll; |
| 56 | + binding.addTimingsCallback(watcher); |
| 57 | + await action(); |
| 58 | + binding.removeTimingsCallback(watcher); |
| 59 | + final FrameTimingSummarizer frameTimes = FrameTimingSummarizer(frameTimings); |
| 60 | + binding.reportData = <String, dynamic>{reportKey: frameTimes.summary}; |
| 61 | +} |
| 62 | + |
| 63 | +/// This class and summarizes a list of [FrameTiming] for the performance |
| 64 | +/// metrics. |
| 65 | +class FrameTimingSummarizer { |
| 66 | + /// Summarize `data` to frame build time and frame rasterizer time statistics. |
| 67 | + /// |
| 68 | + /// See [TimelineSummary.summaryJson] for detail. |
| 69 | + factory FrameTimingSummarizer(List<FrameTiming> data) { |
| 70 | + assert(data != null); |
| 71 | + assert(data.isNotEmpty); |
| 72 | + final List<Duration> frameBuildTime = List<Duration>.unmodifiable( |
| 73 | + data.map<Duration>((FrameTiming datum) => datum.buildDuration), |
| 74 | + ); |
| 75 | + final List<Duration> frameBuildTimeSorted = |
| 76 | + List<Duration>.from(frameBuildTime)..sort(); |
| 77 | + final List<Duration> frameRasterizerTime = List<Duration>.unmodifiable( |
| 78 | + data.map<Duration>((FrameTiming datum) => datum.rasterDuration), |
| 79 | + ); |
| 80 | + final List<Duration> frameRasterizerTimeSorted = |
| 81 | + List<Duration>.from(frameRasterizerTime)..sort(); |
| 82 | + final Duration Function(Duration, Duration) add = |
| 83 | + (Duration a, Duration b) => a + b; |
| 84 | + return FrameTimingSummarizer._( |
| 85 | + frameBuildTime: frameBuildTime, |
| 86 | + frameRasterizerTime: frameRasterizerTime, |
| 87 | + // This avarage calculation is microsecond precision, which is fine |
| 88 | + // because typical values of these times are milliseconds. |
| 89 | + averageFrameBuildTime: frameBuildTime.reduce(add) ~/ data.length, |
| 90 | + p90FrameBuildTime: _findPercentile(frameBuildTimeSorted, 0.90), |
| 91 | + p99FrameBuildTime: _findPercentile(frameBuildTimeSorted, 0.99), |
| 92 | + worstFrameBuildTime: frameBuildTimeSorted.last, |
| 93 | + missedFrameBuildBudget: _countExceed(frameBuildTimeSorted, kBuildBudget), |
| 94 | + averageFrameRasterizerTime: |
| 95 | + frameRasterizerTime.reduce(add) ~/ data.length, |
| 96 | + p90FrameRasterizerTime: _findPercentile(frameRasterizerTimeSorted, 0.90), |
| 97 | + p99FrameRasterizerTime: _findPercentile(frameRasterizerTimeSorted, 0.99), |
| 98 | + worstFrameRasterizerTime: frameRasterizerTimeSorted.last, |
| 99 | + missedFrameRasterizerBudget: |
| 100 | + _countExceed(frameRasterizerTimeSorted, kBuildBudget), |
| 101 | + ); |
| 102 | + } |
| 103 | + |
| 104 | + const FrameTimingSummarizer._({ |
| 105 | + @required this.frameBuildTime, |
| 106 | + @required this.frameRasterizerTime, |
| 107 | + @required this.averageFrameBuildTime, |
| 108 | + @required this.p90FrameBuildTime, |
| 109 | + @required this.p99FrameBuildTime, |
| 110 | + @required this.worstFrameBuildTime, |
| 111 | + @required this.missedFrameBuildBudget, |
| 112 | + @required this.averageFrameRasterizerTime, |
| 113 | + @required this.p90FrameRasterizerTime, |
| 114 | + @required this.p99FrameRasterizerTime, |
| 115 | + @required this.worstFrameRasterizerTime, |
| 116 | + @required this.missedFrameRasterizerBudget, |
| 117 | + }); |
| 118 | + |
| 119 | + /// List of frame build time in microseconds |
| 120 | + final List<Duration> frameBuildTime; |
| 121 | + |
| 122 | + /// List of frame rasterizer time in microseconds |
| 123 | + final List<Duration> frameRasterizerTime; |
| 124 | + |
| 125 | + /// The average value of [frameBuildTime] in milliseconds. |
| 126 | + final Duration averageFrameBuildTime; |
| 127 | + |
| 128 | + /// The 90-th percentile value of [frameBuildTime] in milliseconds |
| 129 | + final Duration p90FrameBuildTime; |
| 130 | + |
| 131 | + /// The 99-th percentile value of [frameBuildTime] in milliseconds |
| 132 | + final Duration p99FrameBuildTime; |
| 133 | + |
| 134 | + /// The largest value of [frameBuildTime] in milliseconds |
| 135 | + final Duration worstFrameBuildTime; |
| 136 | + |
| 137 | + /// Number of items in [frameBuildTime] that's greater than [kBuildBudget] |
| 138 | + final int missedFrameBuildBudget; |
| 139 | + |
| 140 | + /// The average value of [frameRasterizerTime] in milliseconds. |
| 141 | + final Duration averageFrameRasterizerTime; |
| 142 | + |
| 143 | + /// The 90-th percentile value of [frameRasterizerTime] in milliseconds. |
| 144 | + final Duration p90FrameRasterizerTime; |
| 145 | + |
| 146 | + /// The 99-th percentile value of [frameRasterizerTime] in milliseconds. |
| 147 | + final Duration p99FrameRasterizerTime; |
| 148 | + |
| 149 | + /// The largest value of [frameRasterizerTime] in milliseconds. |
| 150 | + final Duration worstFrameRasterizerTime; |
| 151 | + |
| 152 | + /// Number of items in [frameRasterizerTime] that's greater than [kBuildBudget] |
| 153 | + final int missedFrameRasterizerBudget; |
| 154 | + |
| 155 | + /// Convert the summary result to a json object. |
| 156 | + /// |
| 157 | + /// See [TimelineSummary.summaryJson] for detail. |
| 158 | + Map<String, dynamic> get summary => <String, dynamic>{ |
| 159 | + 'average_frame_build_time_millis': |
| 160 | + averageFrameBuildTime.inMicroseconds / 1E3, |
| 161 | + '90th_percentile_frame_build_time_millis': |
| 162 | + p90FrameBuildTime.inMicroseconds / 1E3, |
| 163 | + '99th_percentile_frame_build_time_millis': |
| 164 | + p99FrameBuildTime.inMicroseconds / 1E3, |
| 165 | + 'worst_frame_build_time_millis': |
| 166 | + worstFrameBuildTime.inMicroseconds / 1E3, |
| 167 | + 'missed_frame_build_budget_count': missedFrameBuildBudget, |
| 168 | + 'average_frame_rasterizer_time_millis': |
| 169 | + averageFrameRasterizerTime.inMicroseconds / 1E3, |
| 170 | + '90th_percentile_frame_rasterizer_time_millis': |
| 171 | + p90FrameRasterizerTime.inMicroseconds / 1E3, |
| 172 | + '99th_percentile_frame_rasterizer_time_millis': |
| 173 | + p99FrameRasterizerTime.inMicroseconds / 1E3, |
| 174 | + 'worst_frame_rasterizer_time_millis': |
| 175 | + worstFrameRasterizerTime.inMicroseconds / 1E3, |
| 176 | + 'missed_frame_rasterizer_budget_count': missedFrameRasterizerBudget, |
| 177 | + 'frame_count': frameBuildTime.length, |
| 178 | + 'frame_build_times': frameBuildTime |
| 179 | + .map<int>((Duration datum) => datum.inMicroseconds) |
| 180 | + .toList(), |
| 181 | + 'frame_rasterizer_times': frameRasterizerTime |
| 182 | + .map<int>((Duration datum) => datum.inMicroseconds) |
| 183 | + .toList(), |
| 184 | + }; |
| 185 | +} |
| 186 | + |
| 187 | +// The following helper functions require data sorted |
| 188 | + |
| 189 | +// return the 100*p-th percentile of the data |
| 190 | +T _findPercentile<T>(List<T> data, double p) { |
| 191 | + assert(p >= 0 && p <= 1); |
| 192 | + return data[((data.length - 1) * p).round()]; |
| 193 | +} |
| 194 | + |
| 195 | +// return the number of items in data that > threshold |
| 196 | +int _countExceed<T extends Comparable<T>>(List<T> data, T threshold) { |
| 197 | + return data.length - |
| 198 | + data.indexWhere((T datum) => datum.compareTo(threshold) > 0); |
| 199 | +} |
0 commit comments