1// Copyright 2016 The Chromium 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 5import 'dart:async'; 6import 'dart:convert' show json, JsonEncoder; 7import 'dart:math' as math; 8 9import 'package:file/file.dart'; 10import 'package:path/path.dart' as path; 11 12import 'common.dart'; 13import 'timeline.dart'; 14 15const JsonEncoder _prettyEncoder = JsonEncoder.withIndent(' '); 16 17/// The maximum amount of time considered safe to spend for a frame's build 18/// phase. Anything past that is in the danger of missing the frame as 60FPS. 19const Duration kBuildBudget = Duration(milliseconds: 16); 20 21/// Extracts statistics from a [Timeline]. 22class TimelineSummary { 23 /// Creates a timeline summary given a full timeline object. 24 TimelineSummary.summarize(this._timeline); 25 26 final Timeline _timeline; 27 28 /// Average amount of time spent per frame in the framework building widgets, 29 /// updating layout, painting and compositing. 30 /// 31 /// Returns null if no frames were recorded. 32 double computeAverageFrameBuildTimeMillis() { 33 return _averageInMillis(_extractFrameDurations()); 34 } 35 36 /// The [p]-th percentile frame rasterization time in milliseconds. 37 /// 38 /// Returns null if no frames were recorded. 39 double computePercentileFrameBuildTimeMillis(double p) { 40 return _percentileInMillis(_extractFrameDurations(), p); 41 } 42 43 /// The longest frame build time in milliseconds. 44 /// 45 /// Returns null if no frames were recorded. 46 double computeWorstFrameBuildTimeMillis() { 47 return _maxInMillis(_extractFrameDurations()); 48 } 49 50 /// The number of frames that missed the [kBuildBudget] and therefore are 51 /// in the danger of missing frames. 52 int computeMissedFrameBuildBudgetCount([ Duration frameBuildBudget = kBuildBudget ]) => _extractFrameDurations() 53 .where((Duration duration) => duration > kBuildBudget) 54 .length; 55 56 /// Average amount of time spent per frame in the GPU rasterizer. 57 /// 58 /// Returns null if no frames were recorded. 59 double computeAverageFrameRasterizerTimeMillis() { 60 return _averageInMillis(_extractDuration(_extractGpuRasterizerDrawEvents())); 61 } 62 63 /// The longest frame rasterization time in milliseconds. 64 /// 65 /// Returns null if no frames were recorded. 66 double computeWorstFrameRasterizerTimeMillis() { 67 return _maxInMillis(_extractDuration(_extractGpuRasterizerDrawEvents())); 68 } 69 70 /// The [p]-th percentile frame rasterization time in milliseconds. 71 /// 72 /// Returns null if no frames were recorded. 73 double computePercentileFrameRasterizerTimeMillis(double p) { 74 return _percentileInMillis(_extractDuration(_extractGpuRasterizerDrawEvents()), p); 75 } 76 77 /// The number of frames that missed the [kBuildBudget] on the GPU and 78 /// therefore are in the danger of missing frames. 79 int computeMissedFrameRasterizerBudgetCount([ Duration frameBuildBudget = kBuildBudget ]) => _extractGpuRasterizerDrawEvents() 80 .where((TimedEvent event) => event.duration > kBuildBudget) 81 .length; 82 83 /// The total number of frames recorded in the timeline. 84 int countFrames() => _extractFrameDurations().length; 85 86 /// Encodes this summary as JSON. 87 Map<String, dynamic> get summaryJson { 88 return <String, dynamic>{ 89 'average_frame_build_time_millis': computeAverageFrameBuildTimeMillis(), 90 '90th_percentile_frame_build_time_millis': computePercentileFrameBuildTimeMillis(90.0), 91 '99th_percentile_frame_build_time_millis': computePercentileFrameBuildTimeMillis(99.0), 92 'worst_frame_build_time_millis': computeWorstFrameBuildTimeMillis(), 93 'missed_frame_build_budget_count': computeMissedFrameBuildBudgetCount(), 94 'average_frame_rasterizer_time_millis': computeAverageFrameRasterizerTimeMillis(), 95 '90th_percentile_frame_rasterizer_time_millis': computePercentileFrameRasterizerTimeMillis(90.0), 96 '99th_percentile_frame_rasterizer_time_millis': computePercentileFrameRasterizerTimeMillis(99.0), 97 'worst_frame_rasterizer_time_millis': computeWorstFrameRasterizerTimeMillis(), 98 'missed_frame_rasterizer_budget_count': computeMissedFrameRasterizerBudgetCount(), 99 'frame_count': countFrames(), 100 'frame_build_times': _extractFrameDurations() 101 .map<int>((Duration duration) => duration.inMicroseconds) 102 .toList(), 103 'frame_rasterizer_times': _extractGpuRasterizerDrawEvents() 104 .map<int>((TimedEvent event) => event.duration.inMicroseconds) 105 .toList(), 106 }; 107 } 108 109 /// Writes all of the recorded timeline data to a file. 110 Future<void> writeTimelineToFile( 111 String traceName, { 112 String destinationDirectory, 113 bool pretty = false, 114 }) async { 115 destinationDirectory ??= testOutputsDirectory; 116 await fs.directory(destinationDirectory).create(recursive: true); 117 final File file = fs.file(path.join(destinationDirectory, '$traceName.timeline.json')); 118 await file.writeAsString(_encodeJson(_timeline.json, pretty)); 119 } 120 121 /// Writes [summaryJson] to a file. 122 Future<void> writeSummaryToFile( 123 String traceName, { 124 String destinationDirectory, 125 bool pretty = false, 126 }) async { 127 destinationDirectory ??= testOutputsDirectory; 128 await fs.directory(destinationDirectory).create(recursive: true); 129 final File file = fs.file(path.join(destinationDirectory, '$traceName.timeline_summary.json')); 130 await file.writeAsString(_encodeJson(summaryJson, pretty)); 131 } 132 133 String _encodeJson(Map<String, dynamic> jsonObject, bool pretty) { 134 return pretty 135 ? _prettyEncoder.convert(jsonObject) 136 : json.encode(jsonObject); 137 } 138 139 List<TimelineEvent> _extractNamedEvents(String name) { 140 return _timeline.events 141 .where((TimelineEvent event) => event.name == name) 142 .toList(); 143 } 144 145 List<Duration> _extractDurations(String name) { 146 return _extractNamedEvents(name).map<Duration>((TimelineEvent event) => event.duration).toList(); 147 } 148 149 /// Extracts timed events that are reported as a pair of begin/end events. 150 /// 151 /// See: https://docs.google.com/document/d/1CvAClvFfyA5R-PhYUmn5OOQtYMH4h6I0nSsKchNAySU 152 List<TimedEvent> _extractBeginEndEvents(String name) { 153 final List<TimedEvent> result = <TimedEvent>[]; 154 155 // Timeline does not guarantee that the first event is the "begin" event. 156 final Iterator<TimelineEvent> events = _extractNamedEvents(name) 157 .skipWhile((TimelineEvent evt) => evt.phase != 'B').iterator; 158 while (events.moveNext()) { 159 final TimelineEvent beginEvent = events.current; 160 if (events.moveNext()) { 161 final TimelineEvent endEvent = events.current; 162 result.add(TimedEvent( 163 beginEvent.timestampMicros, 164 endEvent.timestampMicros, 165 )); 166 } 167 } 168 169 return result; 170 } 171 172 double _averageInMillis(Iterable<Duration> durations) { 173 if (durations.isEmpty) 174 throw ArgumentError('durations is empty!'); 175 final double total = durations.fold<double>(0.0, (double t, Duration duration) => t + duration.inMicroseconds.toDouble() / 1000.0); 176 return total / durations.length; 177 } 178 179 double _percentileInMillis(Iterable<Duration> durations, double percentile) { 180 if (durations.isEmpty) 181 throw ArgumentError('durations is empty!'); 182 assert(percentile >= 0.0 && percentile <= 100.0); 183 final List<double> doubles = durations.map<double>((Duration duration) => duration.inMicroseconds.toDouble() / 1000.0).toList(); 184 doubles.sort(); 185 return doubles[((doubles.length - 1) * (percentile / 100)).round()]; 186 187 } 188 189 double _maxInMillis(Iterable<Duration> durations) { 190 if (durations.isEmpty) 191 throw ArgumentError('durations is empty!'); 192 return durations 193 .map<double>((Duration duration) => duration.inMicroseconds.toDouble() / 1000.0) 194 .reduce(math.max); 195 } 196 197 List<TimedEvent> _extractGpuRasterizerDrawEvents() => _extractBeginEndEvents('GPURasterizer::Draw'); 198 199 List<Duration> _extractFrameDurations() => _extractDurations('Frame'); 200 201 Iterable<Duration> _extractDuration(Iterable<TimedEvent> events) { 202 return events.map<Duration>((TimedEvent e) => e.duration); 203 } 204} 205 206/// Timing information about an event that happened in the event loop. 207class TimedEvent { 208 /// Creates a timed event given begin and end timestamps in microseconds. 209 TimedEvent(int beginTimeMicros, int endTimeMicros) 210 : duration = Duration(microseconds: endTimeMicros - beginTimeMicros); 211 212 /// The duration of the event. 213 final Duration duration; 214} 215