mirror of
https://github.com/flutter/flutter.git
synced 2025-06-03 00:51:18 +00:00

* Fix TODO syntax. * Clarify messages for some timeouts, to aid debugging. * Increase some other timeouts that were a needlessly short, to reduce sources of flakes. * Remove some more timeouts that were mostly redundant, to remove complexity. * Minor style cleanup. * Remove some dangerous traps (specifically, hide the explicit start/end times in TimedEvent since they shouldn't matter).
206 lines
7.8 KiB
Dart
206 lines
7.8 KiB
Dart
// 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;
|
|
import 'dart:math' as math;
|
|
|
|
import 'package:file/file.dart';
|
|
import 'package:path/path.dart' as path;
|
|
|
|
import 'common.dart';
|
|
import 'timeline.dart';
|
|
|
|
const JsonEncoder _prettyEncoder = 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 = Duration(milliseconds: 8);
|
|
|
|
/// Extracts statistics from a [Timeline].
|
|
class TimelineSummary {
|
|
/// Creates a timeline summary given a full timeline object.
|
|
TimelineSummary.summarize(this._timeline);
|
|
|
|
final Timeline _timeline;
|
|
|
|
/// Average amount of time spent per frame in the framework building widgets,
|
|
/// updating layout, painting and compositing.
|
|
///
|
|
/// Returns null if no frames were recorded.
|
|
double computeAverageFrameBuildTimeMillis() {
|
|
return _averageInMillis(_extractFrameDurations());
|
|
}
|
|
|
|
/// The longest frame build time in milliseconds.
|
|
///
|
|
/// Returns null if no frames were recorded.
|
|
double computeWorstFrameBuildTimeMillis() {
|
|
return _maxInMillis(_extractFrameDurations());
|
|
}
|
|
|
|
/// The number of frames that missed the [kBuildBudget] and therefore are
|
|
/// in the danger of missing frames.
|
|
int computeMissedFrameBuildBudgetCount([Duration frameBuildBudget = kBuildBudget]) => _extractFrameDurations()
|
|
.where((Duration duration) => duration > kBuildBudget)
|
|
.length;
|
|
|
|
/// Average amount of time spent per frame in the GPU rasterizer.
|
|
///
|
|
/// Returns null if no frames were recorded.
|
|
double computeAverageFrameRasterizerTimeMillis() {
|
|
return _averageInMillis(_extractDuration(_extractGpuRasterizerDrawEvents()));
|
|
}
|
|
|
|
/// The longest frame rasterization time in milliseconds.
|
|
///
|
|
/// Returns null if no frames were recorded.
|
|
double computeWorstFrameRasterizerTimeMillis() {
|
|
return _maxInMillis(_extractDuration(_extractGpuRasterizerDrawEvents()));
|
|
}
|
|
|
|
/// 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);
|
|
}
|
|
|
|
/// 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() => _extractFrameDurations().length;
|
|
|
|
/// Encodes this summary as JSON.
|
|
Map<String, dynamic> get summaryJson {
|
|
return <String, dynamic> {
|
|
'average_frame_build_time_millis': computeAverageFrameBuildTimeMillis(),
|
|
'worst_frame_build_time_millis': computeWorstFrameBuildTimeMillis(),
|
|
'missed_frame_build_budget_count': computeMissedFrameBuildBudgetCount(),
|
|
'average_frame_rasterizer_time_millis': computeAverageFrameRasterizerTimeMillis(),
|
|
'90th_percentile_frame_rasterizer_time_millis': computePercentileFrameRasterizerTimeMillis(90.0),
|
|
'99th_percentile_frame_rasterizer_time_millis': computePercentileFrameRasterizerTimeMillis(99.0),
|
|
'worst_frame_rasterizer_time_millis': computeWorstFrameRasterizerTimeMillis(),
|
|
'missed_frame_rasterizer_budget_count': computeMissedFrameRasterizerBudgetCount(),
|
|
'frame_count': countFrames(),
|
|
'frame_build_times': _extractFrameDurations()
|
|
.map((Duration duration) => duration.inMicroseconds)
|
|
.toList(),
|
|
'frame_rasterizer_times': _extractGpuRasterizerDrawEvents()
|
|
.map((TimedEvent event) => event.duration.inMicroseconds)
|
|
.toList(),
|
|
};
|
|
}
|
|
|
|
/// Writes all of the recorded timeline data to a file.
|
|
Future<Null> writeTimelineToFile(
|
|
String traceName, {
|
|
String destinationDirectory,
|
|
bool pretty = false,
|
|
}) async {
|
|
destinationDirectory ??= testOutputsDirectory;
|
|
await fs.directory(destinationDirectory).create(recursive: true);
|
|
final File file = fs.file(path.join(destinationDirectory, '$traceName.timeline.json'));
|
|
await file.writeAsString(_encodeJson(_timeline.json, pretty));
|
|
}
|
|
|
|
/// Writes [summaryJson] to a file.
|
|
Future<Null> writeSummaryToFile(
|
|
String traceName, {
|
|
String destinationDirectory,
|
|
bool pretty = false,
|
|
}) async {
|
|
destinationDirectory ??= testOutputsDirectory;
|
|
await fs.directory(destinationDirectory).create(recursive: true);
|
|
final File file = fs.file(path.join(destinationDirectory, '$traceName.timeline_summary.json'));
|
|
await file.writeAsString(_encodeJson(summaryJson, pretty));
|
|
}
|
|
|
|
String _encodeJson(Map<String, dynamic> jsonObject, bool pretty) {
|
|
return pretty
|
|
? _prettyEncoder.convert(jsonObject)
|
|
: json.encode(jsonObject);
|
|
}
|
|
|
|
List<TimelineEvent> _extractNamedEvents(String name) {
|
|
return _timeline.events
|
|
.where((TimelineEvent event) => event.name == name)
|
|
.toList();
|
|
}
|
|
|
|
List<Duration> _extractDurations(String name) {
|
|
return _extractNamedEvents(name).map((TimelineEvent event) => event.duration).toList();
|
|
}
|
|
|
|
/// 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) {
|
|
final List<TimedEvent> result = <TimedEvent>[];
|
|
|
|
// Timeline does not guarantee that the first event is the "begin" event.
|
|
final Iterator<TimelineEvent> events = _extractNamedEvents(name)
|
|
.skipWhile((TimelineEvent evt) => evt.phase != 'B').iterator;
|
|
while (events.moveNext()) {
|
|
final TimelineEvent beginEvent = events.current;
|
|
if (events.moveNext()) {
|
|
final TimelineEvent endEvent = events.current;
|
|
result.add(new TimedEvent(
|
|
beginEvent.timestampMicros,
|
|
endEvent.timestampMicros,
|
|
));
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
double _averageInMillis(Iterable<Duration> durations) {
|
|
if (durations.isEmpty)
|
|
throw new ArgumentError('durations is empty!');
|
|
final double total = durations.fold<double>(0.0, (double t, Duration duration) => t + duration.inMicroseconds.toDouble() / 1000.0);
|
|
return total / durations.length;
|
|
}
|
|
|
|
double _percentileInMillis(Iterable<Duration> durations, double percentile) {
|
|
if (durations.isEmpty)
|
|
throw new ArgumentError('durations is empty!');
|
|
assert(percentile >= 0.0 && percentile <= 100.0);
|
|
final List<double> doubles = durations.map<double>((Duration duration) => duration.inMicroseconds.toDouble() / 1000.0).toList();
|
|
doubles.sort();
|
|
return doubles[((doubles.length - 1) * (percentile / 100)).round()];
|
|
|
|
}
|
|
|
|
double _maxInMillis(Iterable<Duration> durations) {
|
|
if (durations.isEmpty)
|
|
throw new ArgumentError('durations is empty!');
|
|
return durations
|
|
.map<double>((Duration duration) => duration.inMicroseconds.toDouble() / 1000.0)
|
|
.reduce(math.max);
|
|
}
|
|
|
|
List<TimedEvent> _extractGpuRasterizerDrawEvents() => _extractBeginEndEvents('GPURasterizer::Draw');
|
|
|
|
List<Duration> _extractFrameDurations() => _extractDurations('Frame');
|
|
|
|
Iterable<Duration> _extractDuration(Iterable<TimedEvent> events) {
|
|
return events.map((TimedEvent e) => e.duration);
|
|
}
|
|
}
|
|
|
|
/// Timing information about an event that happened in the event loop.
|
|
class TimedEvent {
|
|
/// Creates a timed event given begin and end timestamps in microseconds.
|
|
TimedEvent(int beginTimeMicros, int endTimeMicros)
|
|
: this.duration = new Duration(microseconds: endTimeMicros - beginTimeMicros);
|
|
|
|
/// The duration of the event.
|
|
final Duration duration;
|
|
}
|