timeline_summary.dart 7.26 KB
Newer Older
1 2 3 4 5 6
// 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';
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 16 17 18 19 20

final JsonEncoder _prettyEncoder = new JsonEncoder.withIndent('  ');

/// 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.
const Duration kBuildBudget = const Duration(milliseconds: 8);

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 _averageDurationInMillis(_extractFrameEvents());
34 35
  }

36
  /// The longest frame build time in milliseconds.
37 38 39
  ///
  /// Returns `null` if no frames were recorded.
  double computeWorstFrameBuildTimeMillis() {
40
    return _maxDurationInMillis(_extractFrameEvents());
41 42
  }

43
  /// The number of frames that missed the [kBuildBudget] and therefore are
44
  /// in the danger of missing frames.
45
  int computeMissedFrameBuildBudgetCount([Duration frameBuildBudget = kBuildBudget]) => _extractFrameEvents()
46 47 48
    .where((TimedEvent event) => event.duration > kBuildBudget)
    .length;

49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71
  /// Average amount of time spent per frame in the GPU rasterizer.
  ///
  /// Returns `null` if no frames were recorded.
  double computeAverageFrameRasterizerTimeMillis() {
    return _averageDurationInMillis(_extractGpuRasterizerDrawEvents());
  }

  /// The longest frame rasterization time in milliseconds.
  ///
  /// Returns `null` if no frames were recorded.
  double computeWorstFrameRasterizerTimeMillis() {
    return _maxDurationInMillis(_extractGpuRasterizerDrawEvents());
  }

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

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

72 73 74 75
  /// Encodes this summary as JSON.
  Map<String, dynamic> get summaryJson {
    return <String, dynamic> {
      'average_frame_build_time_millis': computeAverageFrameBuildTimeMillis(),
76
      'worst_frame_build_time_millis': computeWorstFrameBuildTimeMillis(),
77
      'missed_frame_build_budget_count': computeMissedFrameBuildBudgetCount(),
78 79 80
      'average_frame_rasterizer_time_millis': computeAverageFrameRasterizerTimeMillis(),
      'worst_frame_rasterizer_time_millis': computeWorstFrameRasterizerTimeMillis(),
      'missed_frame_rasterizer_budget_count': computeMissedFrameRasterizerBudgetCount(),
81
      'frame_count': countFrames(),
82
      'frame_build_times': _extractFrameEvents()
83
        .map((TimedEvent event) => event.duration.inMicroseconds)
84 85 86 87
        .toList(),
      'frame_rasterizer_times': _extractGpuRasterizerDrawEvents()
          .map((TimedEvent event) => event.duration.inMicroseconds)
          .toList(),
88 89 90 91 92
    };
  }

  /// Writes all of the recorded timeline data to a file.
  Future<Null> writeTimelineToFile(String traceName,
93 94
      {String destinationDirectory, bool pretty: false}) async {
    destinationDirectory ??= testOutputsDirectory;
95 96
    await fs.directory(destinationDirectory).create(recursive: true);
    File file = fs.file(path.join(destinationDirectory, '$traceName.timeline.json'));
97
    await file.writeAsString(_encodeJson(_timeline.json, pretty));
98 99 100 101
  }

  /// Writes [summaryJson] to a file.
  Future<Null> writeSummaryToFile(String traceName,
102 103
      {String destinationDirectory, bool pretty: false}) async {
    destinationDirectory ??= testOutputsDirectory;
104 105 106 107 108
    await fs.directory(destinationDirectory).create(recursive: true);
    File file = fs.file(path.join(destinationDirectory, '$traceName.timeline_summary.json'));
    await file.writeAsString(_encodeJson(summaryJson, pretty));
  }

109
  String _encodeJson(Map<String, dynamic> json, bool pretty) {
110 111 112 113 114
    return pretty
      ? _prettyEncoder.convert(json)
      : JSON.encode(json);
  }

115 116 117
  List<TimelineEvent> _extractNamedEvents(String name) {
    return _timeline.events
      .where((TimelineEvent event) => event.name == name)
118 119 120
      .toList();
  }

121 122 123 124 125 126 127 128 129 130 131 132 133
  /// Extracts timed events that are reported as complete ("X") timeline events.
  ///
  /// See: https://docs.google.com/document/d/1CvAClvFfyA5R-PhYUmn5OOQtYMH4h6I0nSsKchNAySU
  List<TimedEvent> _extractCompleteEvents(String name) {
    return _extractNamedEvents(name)
        .where((TimelineEvent event) => event.phase == 'X')
        .map((TimelineEvent event) {
          return new TimedEvent(
            event.timestampMicros,
            event.timestampMicros + event.duration.inMicroseconds,
          );
        })
        .toList();
134 135
  }

136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175
  /// 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) {
    List<TimedEvent> result = <TimedEvent>[];

    // Timeline does not guarantee that the first event is the "begin" event.
    Iterator<TimelineEvent> events = _extractNamedEvents(name)
        .skipWhile((TimelineEvent evt) => evt.phase != 'B').iterator;
    while(events.moveNext()) {
      TimelineEvent beginEvent = events.current;
      if (events.moveNext()) {
        TimelineEvent endEvent = events.current;
        result.add(new TimedEvent(
            beginEvent.timestampMicros,
            endEvent.timestampMicros
        ));
      }
    }

    return result;
  }

  double _averageDurationInMillis(List<TimedEvent> events) {
    if (events.length == 0)
      return null;

    int total = events.fold/*<int>*/(0, (int t, TimedEvent e) => t + e.duration.inMilliseconds);
    return total / events.length;
  }

  double _maxDurationInMillis(List<TimedEvent> events) {
    if (events.length == 0)
      return null;

    return events
        .map/*<double>*/((TimedEvent e) => e.duration.inMilliseconds.toDouble())
        .reduce((double a, double b) => math.max(a, b));
  }

176
  List<TimedEvent> _extractFrameEvents() => _extractCompleteEvents('Frame');
177
  List<TimedEvent> _extractGpuRasterizerDrawEvents() => _extractBeginEndEvents('GPURasterizer::Draw');
178 179 180 181 182 183 184 185 186 187 188 189 190
}

/// Timing information about an event that happened in the event loop.
class TimedEvent {
  /// The timestamp when the event began.
  final int beginTimeMicros;

  /// The timestamp when the event ended.
  final int endTimeMicros;

  /// The duration of the event.
  final Duration duration;

191
  /// Creates a timed event given begin and end timestamps in microseconds.
192 193 194 195 196
  TimedEvent(int beginTimeMicros, int endTimeMicros)
    : this.beginTimeMicros = beginTimeMicros,
      this.endTimeMicros = endTimeMicros,
      this.duration = new Duration(microseconds: endTimeMicros - beginTimeMicros);
}