timeline_summary.dart 8.25 KB
Newer Older
1 2 3 4 5
// Copyright 2016 The Chromium 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:async';
6
import 'dart:convert' show json, JsonEncoder;
7
import 'dart:math' as math;
8 9 10 11 12

import 'package:file/file.dart';
import 'package:path/path.dart' as path;

import 'common.dart';
13
import 'timeline.dart';
14

15
const JsonEncoder _prettyEncoder = JsonEncoder.withIndent('  ');
16 17 18

/// The maximum amount of time considered safe to spend for a frame's build
/// phase. Anything past that is in the danger of missing the frame as 60FPS.
19
const Duration kBuildBudget = Duration(milliseconds: 16);
20

21
/// Extracts statistics from a [Timeline].
22
class TimelineSummary {
23
  /// Creates a timeline summary given a full timeline object.
24
  TimelineSummary.summarize(this._timeline);
25

26
  final Timeline _timeline;
27 28 29

  /// Average amount of time spent per frame in the framework building widgets,
  /// updating layout, painting and compositing.
30
  ///
31
  /// Returns null if no frames were recorded.
32
  double computeAverageFrameBuildTimeMillis() {
33
    return _averageInMillis(_extractFrameDurations());
34 35
  }

36 37 38 39 40 41 42
  /// The [p]-th percentile frame rasterization time in milliseconds.
  ///
  /// Returns null if no frames were recorded.
  double computePercentileFrameBuildTimeMillis(double p) {
    return _percentileInMillis(_extractFrameDurations(), p);
  }

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 55
    .length;

56 57
  /// Average amount of time spent per frame in the GPU rasterizer.
  ///
58
  /// Returns null if no frames were recorded.
59
  double computeAverageFrameRasterizerTimeMillis() {
60
    return _averageInMillis(_extractDuration(_extractGpuRasterizerDrawEvents()));
61 62 63 64
  }

  /// The longest frame rasterization time in milliseconds.
  ///
65
  /// Returns null if no frames were recorded.
66
  double computeWorstFrameRasterizerTimeMillis() {
67
    return _maxInMillis(_extractDuration(_extractGpuRasterizerDrawEvents()));
68 69
  }

70 71 72 73 74 75 76
  /// The [p]-th percentile frame rasterization time in milliseconds.
  ///
  /// Returns null if no frames were recorded.
  double computePercentileFrameRasterizerTimeMillis(double p) {
    return _percentileInMillis(_extractDuration(_extractGpuRasterizerDrawEvents()), p);
  }

77 78
  /// The number of frames that missed the [kBuildBudget] on the GPU and
  /// therefore are in the danger of missing frames.
79
  int computeMissedFrameRasterizerBudgetCount([ Duration frameBuildBudget = kBuildBudget ]) => _extractGpuRasterizerDrawEvents()
80 81 82 83
      .where((TimedEvent event) => event.duration > kBuildBudget)
      .length;

  /// The total number of frames recorded in the timeline.
84
  int countFrames() => _extractFrameDurations().length;
85

86 87
  /// Encodes this summary as JSON.
  Map<String, dynamic> get summaryJson {
88
    return <String, dynamic>{
89
      'average_frame_build_time_millis': computeAverageFrameBuildTimeMillis(),
90 91
      '90th_percentile_frame_build_time_millis': computePercentileFrameBuildTimeMillis(90.0),
      '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 96
      '90th_percentile_frame_rasterizer_time_millis': computePercentileFrameRasterizerTimeMillis(90.0),
      '99th_percentile_frame_rasterizer_time_millis': computePercentileFrameRasterizerTimeMillis(99.0),
97 98
      'worst_frame_rasterizer_time_millis': computeWorstFrameRasterizerTimeMillis(),
      'missed_frame_rasterizer_budget_count': computeMissedFrameRasterizerBudgetCount(),
99
      'frame_count': countFrames(),
100
      'frame_build_times': _extractFrameDurations()
101
        .map<int>((Duration duration) => duration.inMicroseconds)
102 103
        .toList(),
      '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 112
    String traceName, {
    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 124
    String traceName, {
    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 131 132
    await file.writeAsString(_encodeJson(summaryJson, pretty));
  }

133
  String _encodeJson(Map<String, dynamic> jsonObject, bool pretty) {
134
    return pretty
135 136
      ? _prettyEncoder.convert(jsonObject)
      : json.encode(jsonObject);
137 138
  }

139 140 141
  List<TimelineEvent> _extractNamedEvents(String name) {
    return _timeline.events
      .where((TimelineEvent event) => event.name == name)
142 143 144
      .toList();
  }

145
  List<Duration> _extractDurations(String name) {
146
    return _extractNamedEvents(name).map<Duration>((TimelineEvent event) => event.duration).toList();
147 148
  }

149 150 151 152
  /// Extracts timed events that are reported as a pair of begin/end events.
  ///
  /// See: https://docs.google.com/document/d/1CvAClvFfyA5R-PhYUmn5OOQtYMH4h6I0nSsKchNAySU
  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 164
          beginEvent.timestampMicros,
          endEvent.timestampMicros,
165 166 167 168 169 170 171
        ));
      }
    }

    return result;
  }

172 173
  double _averageInMillis(Iterable<Duration> durations) {
    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 180
  double _percentileInMillis(Iterable<Duration> durations, double percentile) {
    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 185 186 187 188
    doubles.sort();
    return doubles[((doubles.length - 1) * (percentile / 100)).round()];

  }

189 190
  double _maxInMillis(Iterable<Duration> durations) {
    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 207
}

/// Timing information about an event that happened in the event loop.
class 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 213 214
  /// The duration of the event.
  final Duration duration;
}