• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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