timeline_summary.dart 5.29 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 21 22 23

const String _kDefaultDirectory = 'build';
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);

/// Extracts statistics from the event loop timeline.
class TimelineSummary {
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 33 34 35 36 37 38 39 40 41 42 43 44 45
  double computeAverageFrameBuildTimeMillis() {
    int totalBuildTimeMicros = 0;
    int frameCount = 0;

    for (TimedEvent event in _extractBeginFrameEvents()) {
      frameCount++;
      totalBuildTimeMicros += event.duration.inMicroseconds;
    }

    return frameCount > 0
      ? (totalBuildTimeMicros / frameCount) / 1000
      : null;
  }

46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63
  /// Find amount of time spent in the framework building widgets,
  /// updating layout, painting and compositing on worst frame.
  ///
  /// Returns `null` if no frames were recorded.
  double computeWorstFrameBuildTimeMillis() {
    int maxBuildTimeMicros = 0;
    int frameCount = 0;

    for (TimedEvent event in _extractBeginFrameEvents()) {
      frameCount++;
      maxBuildTimeMicros = math.max(maxBuildTimeMicros, event.duration.inMicroseconds);
    }

    return frameCount > 0
      ? maxBuildTimeMicros / 1000
      : null;
  }

64 65 66 67 68 69 70 71 72 73 74 75 76 77 78
  /// The total number of frames recorded in the timeline.
  int countFrames() => _extractBeginFrameEvents().length;

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

  /// Encodes this summary as JSON.
  Map<String, dynamic> get summaryJson {
    return <String, dynamic> {
      'average_frame_build_time_millis': computeAverageFrameBuildTimeMillis(),
79
      'worst_frame_build_time_millis': computeWorstFrameBuildTimeMillis(),
80 81
      'missed_frame_build_budget_count': computeMissedFrameBuildBudgetCount(),
      'frame_count': countFrames(),
82 83 84
      'frame_build_times': _extractBeginFrameEvents()
        .map((TimedEvent event) => event.duration.inMicroseconds)
        .toList()
85 86 87 88 89 90 91 92
    };
  }

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

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

104
  String _encodeJson(Map<String, dynamic> json, bool pretty) {
105 106 107 108 109
    return pretty
      ? _prettyEncoder.convert(json)
      : JSON.encode(json);
  }

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

  /// Extracts timed events that are reported as a pair of begin/end events.
  List<TimedEvent> _extractTimedBeginEndEvents(String name) {
    List<TimedEvent> result = <TimedEvent>[];

    // Timeline does not guarantee that the first event is the "begin" event.
121 122
    Iterator<TimelineEvent> events = _extractNamedEvents(name)
        .skipWhile((TimelineEvent evt) => evt.phase != 'B').iterator;
123
    while(events.moveNext()) {
124
      TimelineEvent beginEvent = events.current;
125
      if (events.moveNext()) {
126 127 128 129 130
        TimelineEvent endEvent = events.current;
        result.add(new TimedEvent(
          beginEvent.timestampMicros,
          endEvent.timestampMicros
        ));
131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155
      }
    }

    return result;
  }

  List<TimedEvent> _extractBeginFrameEvents() => _extractTimedBeginEndEvents('Engine::BeginFrame');
}

/// 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;

  TimedEvent(int beginTimeMicros, int endTimeMicros)
    : this.beginTimeMicros = beginTimeMicros,
      this.endTimeMicros = endTimeMicros,
      this.duration = new Duration(microseconds: endTimeMicros - beginTimeMicros);
}