| // Copyright 2014 The Flutter 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:developer'; |
| import 'dart:typed_data'; |
| |
| import 'package:meta/meta.dart'; |
| |
| import '_timeline_io.dart' |
| if (dart.library.js_util) '_timeline_web.dart' as impl; |
| import 'constants.dart'; |
| |
| /// Measures how long blocks of code take to run. |
| /// |
| /// This class can be used as a drop-in replacement for [Timeline] as it |
| /// provides methods compatible with [Timeline] signature-wise, and it has |
| /// minimal overhead. |
| /// |
| /// Provides [debugReset] and [debugCollect] methods that make it convenient to use in |
| /// frame-oriented environment where collected metrics can be attributed to a |
| /// frame, then aggregated into frame statistics, e.g. frame averages. |
| /// |
| /// Forwards measurements to [Timeline] so they appear in Flutter DevTools. |
| abstract final class FlutterTimeline { |
| static _BlockBuffer _buffer = _BlockBuffer(); |
| |
| /// Whether block timings are collected and can be retrieved using the |
| /// [debugCollect] method. |
| /// |
| /// This is always false in release mode. |
| static bool get debugCollectionEnabled => _collectionEnabled; |
| |
| /// Enables metric collection. |
| /// |
| /// Metric collection can only be enabled in non-release modes. It is most |
| /// useful in profile mode where application performance is representative |
| /// of a deployed application. |
| /// |
| /// When disabled, resets collected data by calling [debugReset]. |
| /// |
| /// Throws a [StateError] if invoked in release mode. |
| static set debugCollectionEnabled(bool value) { |
| if (kReleaseMode) { |
| throw _createReleaseModeNotSupportedError(); |
| } |
| if (value == _collectionEnabled) { |
| return; |
| } |
| _collectionEnabled = value; |
| debugReset(); |
| } |
| |
| static StateError _createReleaseModeNotSupportedError() { |
| return StateError('FlutterTimeline metric collection not supported in release mode.'); |
| } |
| |
| static bool _collectionEnabled = false; |
| |
| /// Start a synchronous operation labeled `name`. |
| /// |
| /// Optionally takes a map of `arguments`. This slice may also optionally be |
| /// associated with a [Flow] event. This operation must be finished by calling |
| /// [finishSync] before returning to the event queue. |
| /// |
| /// This is a drop-in replacement for [Timeline.startSync]. |
| static void startSync(String name, { Map<String, Object?>? arguments, Flow? flow }) { |
| Timeline.startSync(name, arguments: arguments, flow: flow); |
| if (!kReleaseMode && _collectionEnabled) { |
| _buffer.startSync(name, arguments: arguments, flow: flow); |
| } |
| } |
| |
| /// Finish the last synchronous operation that was started. |
| /// |
| /// This is a drop-in replacement for [Timeline.finishSync]. |
| static void finishSync() { |
| Timeline.finishSync(); |
| if (!kReleaseMode && _collectionEnabled) { |
| _buffer.finishSync(); |
| } |
| } |
| |
| /// Emit an instant event. |
| /// |
| /// This is a drop-in replacement for [Timeline.instantSync]. |
| static void instantSync(String name, { Map<String, Object?>? arguments }) { |
| Timeline.instantSync(name, arguments: arguments); |
| } |
| |
| /// A utility method to time a synchronous `function`. Internally calls |
| /// `function` bracketed by calls to [startSync] and [finishSync]. |
| /// |
| /// This is a drop-in replacement for [Timeline.timeSync]. |
| static T timeSync<T>(String name, TimelineSyncFunction<T> function, |
| { Map<String, Object?>? arguments, Flow? flow }) { |
| startSync(name, arguments: arguments, flow: flow); |
| try { |
| return function(); |
| } finally { |
| finishSync(); |
| } |
| } |
| |
| /// The current time stamp from the clock used by the timeline in |
| /// microseconds. |
| /// |
| /// When run on the Dart VM, uses the same monotonic clock as the embedding |
| /// API's `Dart_TimelineGetMicros`. |
| /// |
| /// When run on the web, uses `window.performance.now`. |
| /// |
| /// This is a drop-in replacement for [Timeline.now]. |
| static int get now => impl.performanceTimestamp.toInt(); |
| |
| /// Returns timings collected since [debugCollectionEnabled] was set to true, |
| /// since the previous [debugCollect], or since the previous [debugReset], |
| /// whichever was last. |
| /// |
| /// Resets the collected timings. |
| /// |
| /// This is only meant to be used in non-release modes, typically in profile |
| /// mode that provides timings close to release mode timings. |
| static AggregatedTimings debugCollect() { |
| if (kReleaseMode) { |
| throw _createReleaseModeNotSupportedError(); |
| } |
| if (!_collectionEnabled) { |
| throw StateError('Timeline metric collection not enabled.'); |
| } |
| final AggregatedTimings result = AggregatedTimings(_buffer.computeTimings()); |
| debugReset(); |
| return result; |
| } |
| |
| /// Forgets all previously collected timing data. |
| /// |
| /// Use this method to scope metrics to a frame, a pointer event, or any |
| /// other event. To do that, call [debugReset] at the start of the event, then |
| /// call [debugCollect] at the end of the event. |
| /// |
| /// This is only meant to be used in non-release modes. |
| static void debugReset() { |
| if (kReleaseMode) { |
| throw _createReleaseModeNotSupportedError(); |
| } |
| _buffer = _BlockBuffer(); |
| } |
| } |
| |
| /// Provides [start], [end], and [duration] of a named block of code, timed by |
| /// [FlutterTimeline]. |
| @immutable |
| final class TimedBlock { |
| /// Creates a timed block of code from a [name], [start], and [end]. |
| /// |
| /// The [name] should be sufficiently unique and descriptive for someone to |
| /// easily tell which part of code was measured. |
| const TimedBlock({ |
| required this.name, |
| required this.start, |
| required this.end, |
| }) : assert(end >= start, 'The start timestamp must not be greater than the end timestamp.'); |
| |
| /// A readable label for a block of code that was measured. |
| /// |
| /// This field should be sufficiently unique and descriptive for someone to |
| /// easily tell which part of code was measured. |
| final String name; |
| |
| /// The timestamp in microseconds that marks the beginning of the measured |
| /// block of code. |
| final double start; |
| |
| /// The timestamp in microseconds that marks the end of the measured block of |
| /// code. |
| final double end; |
| |
| /// How long the measured block of code took to execute in microseconds. |
| double get duration => end - start; |
| |
| @override |
| String toString() { |
| return 'TimedBlock($name, $start, $end, $duration)'; |
| } |
| } |
| |
| /// Provides aggregated results for timings collected by [FlutterTimeline]. |
| @immutable |
| final class AggregatedTimings { |
| /// Creates aggregated timings for the provided timed blocks. |
| AggregatedTimings(this.timedBlocks); |
| |
| /// All timed blocks collected between the last reset and [FlutterTimeline.debugCollect]. |
| final List<TimedBlock> timedBlocks; |
| |
| /// Aggregated timed blocks collected between the last reset and [FlutterTimeline.debugCollect]. |
| /// |
| /// Does not guarantee that all code blocks will be reported. Only those that |
| /// executed since the last reset are listed here. Use [getAggregated] for |
| /// graceful handling of missing code blocks. |
| late final List<AggregatedTimedBlock> aggregatedBlocks = _computeAggregatedBlocks(); |
| |
| List<AggregatedTimedBlock> _computeAggregatedBlocks() { |
| final Map<String, (double, int)> aggregate = <String, (double, int)>{}; |
| for (final TimedBlock block in timedBlocks) { |
| final (double, int) previousValue = aggregate.putIfAbsent(block.name, () => (0, 0)); |
| aggregate[block.name] = (previousValue.$1 + block.duration, previousValue.$2 + 1); |
| } |
| return aggregate.entries.map<AggregatedTimedBlock>( |
| (MapEntry<String, (double, int)> entry) { |
| return AggregatedTimedBlock(name: entry.key, duration: entry.value.$1, count: entry.value.$2); |
| } |
| ).toList(); |
| } |
| |
| /// Returns aggregated numbers for a named block of code. |
| /// |
| /// If the block in question never executed since the last reset, returns an |
| /// aggregation with zero duration and count. |
| AggregatedTimedBlock getAggregated(String name) { |
| return aggregatedBlocks.singleWhere( |
| (AggregatedTimedBlock block) => block.name == name, |
| // Handle the case where there are no recorded blocks of the specified |
| // type. In this case, the aggregated duration is simply zero, and so is |
| // the number of occurrences (i.e. count). |
| orElse: () => AggregatedTimedBlock(name: name, duration: 0, count: 0), |
| ); |
| } |
| } |
| |
| /// Aggregates multiple [TimedBlock] objects that share a [name]. |
| /// |
| /// It is common for the same block of code to be executed multiple times within |
| /// a frame. It is useful to combine multiple executions and report the total |
| /// amount of time attributed to that block of code. |
| @immutable |
| final class AggregatedTimedBlock { |
| /// Creates a timed block of code from a [name] and [duration]. |
| /// |
| /// The [name] should be sufficiently unique and descriptive for someone to |
| /// easily tell which part of code was measured. |
| const AggregatedTimedBlock({ |
| required this.name, |
| required this.duration, |
| required this.count, |
| }) : assert(duration >= 0); |
| |
| /// A readable label for a block of code that was measured. |
| /// |
| /// This field should be sufficiently unique and descriptive for someone to |
| /// easily tell which part of code was measured. |
| final String name; |
| |
| /// The sum of [TimedBlock.duration] values of aggretaged blocks. |
| final double duration; |
| |
| /// The number of [TimedBlock] objects aggregated. |
| final int count; |
| |
| @override |
| String toString() { |
| return 'AggregatedTimedBlock($name, $duration, $count)'; |
| } |
| } |
| |
| const int _kSliceSize = 500; |
| |
| /// A growable list of float64 values with predictable [add] performance. |
| /// |
| /// The list is organized into a "chain" of [Float64List]s. The object starts |
| /// with a `Float64List` "slice". When [add] is called, the value is added to |
| /// the slice. Once the slice is full, it is moved into the chain, and a new |
| /// slice is allocated. Slice size is static and therefore its allocation has |
| /// predictable cost. This is unlike the default [List] implementation, which, |
| /// when full, doubles its buffer size and copies all old elements into the new |
| /// buffer, leading to unpredictable performance. This makes it a poor choice |
| /// for recording performance because buffer reallocation would affect the |
| /// runtime. |
| /// |
| /// The trade-off is that reading values back from the chain is more expensive |
| /// compared to [List] because it requires iterating over multiple slices. This |
| /// is a reasonable trade-off for performance metrics, because it is more |
| /// important to minimize the overhead while recording metrics, than it is when |
| /// reading them. |
| final class _Float64ListChain { |
| _Float64ListChain(); |
| |
| final List<Float64List> _chain = <Float64List>[]; |
| Float64List _slice = Float64List(_kSliceSize); |
| int _pointer = 0; |
| |
| int get length => _length; |
| int _length = 0; |
| |
| /// Adds and [element] to this chain. |
| void add(double element) { |
| _slice[_pointer] = element; |
| _pointer += 1; |
| _length += 1; |
| if (_pointer >= _kSliceSize) { |
| _chain.add(_slice); |
| _slice = Float64List(_kSliceSize); |
| _pointer = 0; |
| } |
| } |
| |
| /// Returns all elements added to this chain. |
| /// |
| /// This getter is not optimized to be fast. It is assumed that when metrics |
| /// are read back, they do not affect the timings of the work being |
| /// benchmarked. |
| List<double> extractElements() { |
| final List<double> result = <double>[]; |
| _chain.forEach(result.addAll); |
| for (int i = 0; i < _pointer; i++) { |
| result.add(_slice[i]); |
| } |
| return result; |
| } |
| } |
| |
| /// Same as [_Float64ListChain] but for recording string values. |
| final class _StringListChain { |
| _StringListChain(); |
| |
| final List<List<String?>> _chain = <List<String?>>[]; |
| List<String?> _slice = List<String?>.filled(_kSliceSize, null); |
| int _pointer = 0; |
| |
| int get length => _length; |
| int _length = 0; |
| |
| /// Adds and [element] to this chain. |
| void add(String element) { |
| _slice[_pointer] = element; |
| _pointer += 1; |
| _length += 1; |
| if (_pointer >= _kSliceSize) { |
| _chain.add(_slice); |
| _slice = List<String?>.filled(_kSliceSize, null); |
| _pointer = 0; |
| } |
| } |
| |
| /// Returns all elements added to this chain. |
| /// |
| /// This getter is not optimized to be fast. It is assumed that when metrics |
| /// are read back, they do not affect the timings of the work being |
| /// benchmarked. |
| List<String> extractElements() { |
| final List<String> result = <String>[]; |
| for (final List<String?> slice in _chain) { |
| for (final String? element in slice) { |
| result.add(element!); |
| } |
| } |
| for (int i = 0; i < _pointer; i++) { |
| result.add(_slice[i]!); |
| } |
| return result; |
| } |
| } |
| |
| /// A buffer that records starts and ends of code blocks, and their names. |
| final class _BlockBuffer { |
| // Start-finish blocks can be nested. Track this nestedness by stacking the |
| // start timestamps. Finish timestamps will pop timings from the stack and |
| // add the (start, finish) tuple to the _block. |
| static const int _stackDepth = 1000; |
| static final Float64List _startStack = Float64List(_stackDepth); |
| static final List<String?> _nameStack = List<String?>.filled(_stackDepth, null); |
| static int _stackPointer = 0; |
| |
| final _Float64ListChain _starts = _Float64ListChain(); |
| final _Float64ListChain _finishes = _Float64ListChain(); |
| final _StringListChain _names = _StringListChain(); |
| |
| List<TimedBlock> computeTimings() { |
| assert( |
| _stackPointer == 0, |
| 'Invalid sequence of `startSync` and `finishSync`.\n' |
| 'The operation stack was not empty. The following operations are still ' |
| 'waiting to be finished via the `finishSync` method:\n' |
| '${List<String>.generate(_stackPointer, (int i) => _nameStack[i]!).join(', ')}' |
| ); |
| |
| final List<TimedBlock> result = <TimedBlock>[]; |
| final int length = _finishes.length; |
| final List<double> starts = _starts.extractElements(); |
| final List<double> finishes = _finishes.extractElements(); |
| final List<String> names = _names.extractElements(); |
| |
| assert(starts.length == length); |
| assert(finishes.length == length); |
| assert(names.length == length); |
| |
| for (int i = 0; i < length; i++) { |
| result.add(TimedBlock( |
| start: starts[i], |
| end: finishes[i], |
| name: names[i], |
| )); |
| } |
| |
| return result; |
| } |
| |
| void startSync(String name, { Map<String, Object?>? arguments, Flow? flow }) { |
| _startStack[_stackPointer] = impl.performanceTimestamp; |
| _nameStack[_stackPointer] = name; |
| _stackPointer += 1; |
| } |
| |
| void finishSync() { |
| assert( |
| _stackPointer > 0, |
| 'Invalid sequence of `startSync` and `finishSync`.\n' |
| 'Attempted to finish timing a block of code, but there are no pending ' |
| '`startSync` calls.' |
| ); |
| |
| final double finishTime = impl.performanceTimestamp; |
| final double startTime = _startStack[_stackPointer - 1]; |
| final String name = _nameStack[_stackPointer - 1]!; |
| _stackPointer -= 1; |
| |
| _starts.add(startTime); |
| _finishes.add(finishTime); |
| _names.add(name); |
| } |
| } |