1// Copyright 2014 The Flutter Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5import 'dart:developer';
6import 'dart:typed_data';
7
8import 'package:meta/meta.dart';
9
10import '_timeline_io.dart' if (dart.library.js_interop) '_timeline_web.dart' as impl;
11import 'constants.dart';
12
13/// Measures how long blocks of code take to run.
14///
15/// This class can be used as a drop-in replacement for [Timeline] as it
16/// provides methods compatible with [Timeline] signature-wise, and it has
17/// minimal overhead.
18///
19/// Provides [debugReset] and [debugCollect] methods that make it convenient to use in
20/// frame-oriented environment where collected metrics can be attributed to a
21/// frame, then aggregated into frame statistics, e.g. frame averages.
22///
23/// Forwards measurements to [Timeline] so they appear in Flutter DevTools.
24abstract final class FlutterTimeline {
25 static _BlockBuffer _buffer = _BlockBuffer();
26
27 /// Whether block timings are collected and can be retrieved using the
28 /// [debugCollect] method.
29 ///
30 /// This is always false in release mode.
31 static bool get debugCollectionEnabled => _collectionEnabled;
32
33 /// Enables metric collection.
34 ///
35 /// Metric collection can only be enabled in non-release modes. It is most
36 /// useful in profile mode where application performance is representative
37 /// of a deployed application.
38 ///
39 /// When disabled, resets collected data by calling [debugReset].
40 ///
41 /// Throws a [StateError] if invoked in release mode.
42 static set debugCollectionEnabled(bool value) {
43 if (kReleaseMode) {
44 throw _createReleaseModeNotSupportedError();
45 }
46 if (value == _collectionEnabled) {
47 return;
48 }
49 _collectionEnabled = value;
50 debugReset();
51 }
52
53 static StateError _createReleaseModeNotSupportedError() {
54 return StateError('FlutterTimeline metric collection not supported in release mode.');
55 }
56
57 static bool _collectionEnabled = false;
58
59 /// Start a synchronous operation labeled `name`.
60 ///
61 /// Optionally takes a map of `arguments`. This slice may also optionally be
62 /// associated with a [Flow] event. This operation must be finished by calling
63 /// [finishSync] before returning to the event queue.
64 ///
65 /// This is a drop-in replacement for [Timeline.startSync].
66 static void startSync(String name, {Map<String, Object?>? arguments, Flow? flow}) {
67 Timeline.startSync(name, arguments: arguments, flow: flow);
68 if (!kReleaseMode && _collectionEnabled) {
69 _buffer.startSync(name, arguments: arguments, flow: flow);
70 }
71 }
72
73 /// Finish the last synchronous operation that was started.
74 ///
75 /// This is a drop-in replacement for [Timeline.finishSync].
76 static void finishSync() {
77 Timeline.finishSync();
78 if (!kReleaseMode && _collectionEnabled) {
79 _buffer.finishSync();
80 }
81 }
82
83 /// Emit an instant event.
84 ///
85 /// This is a drop-in replacement for [Timeline.instantSync].
86 static void instantSync(String name, {Map<String, Object?>? arguments}) {
87 Timeline.instantSync(name, arguments: arguments);
88 }
89
90 /// A utility method to time a synchronous `function`. Internally calls
91 /// `function` bracketed by calls to [startSync] and [finishSync].
92 ///
93 /// This is a drop-in replacement for [Timeline.timeSync].
94 static T timeSync<T>(
95 String name,
96 TimelineSyncFunction<T> function, {
97 Map<String, Object?>? arguments,
98 Flow? flow,
99 }) {
100 startSync(name, arguments: arguments, flow: flow);
101 try {
102 return function();
103 } finally {
104 finishSync();
105 }
106 }
107
108 /// The current time stamp from the clock used by the timeline in
109 /// microseconds.
110 ///
111 /// When run on the Dart VM, uses the same monotonic clock as the embedding
112 /// API's `Dart_TimelineGetMicros`.
113 ///
114 /// When run on the web, uses `window.performance.now`.
115 ///
116 /// This is a drop-in replacement for [Timeline.now].
117 static int get now => impl.performanceTimestamp.toInt();
118
119 /// Returns timings collected since [debugCollectionEnabled] was set to true,
120 /// since the previous [debugCollect], or since the previous [debugReset],
121 /// whichever was last.
122 ///
123 /// Resets the collected timings.
124 ///
125 /// This is only meant to be used in non-release modes, typically in profile
126 /// mode that provides timings close to release mode timings.
127 static AggregatedTimings debugCollect() {
128 if (kReleaseMode) {
129 throw _createReleaseModeNotSupportedError();
130 }
131 if (!_collectionEnabled) {
132 throw StateError('Timeline metric collection not enabled.');
133 }
134 final AggregatedTimings result = AggregatedTimings(_buffer.computeTimings());
135 debugReset();
136 return result;
137 }
138
139 /// Forgets all previously collected timing data.
140 ///
141 /// Use this method to scope metrics to a frame, a pointer event, or any
142 /// other event. To do that, call [debugReset] at the start of the event, then
143 /// call [debugCollect] at the end of the event.
144 ///
145 /// This is only meant to be used in non-release modes.
146 static void debugReset() {
147 if (kReleaseMode) {
148 throw _createReleaseModeNotSupportedError();
149 }
150 _buffer = _BlockBuffer();
151 }
152}
153
154/// Provides [start], [end], and [duration] of a named block of code, timed by
155/// [FlutterTimeline].
156@immutable
157final class TimedBlock {
158 /// Creates a timed block of code from a [name], [start], and [end].
159 ///
160 /// The [name] should be sufficiently unique and descriptive for someone to
161 /// easily tell which part of code was measured.
162 const TimedBlock({required this.name, required this.start, required this.end})
163 : assert(end >= start, 'The start timestamp must not be greater than the end timestamp.');
164
165 /// A readable label for a block of code that was measured.
166 ///
167 /// This field should be sufficiently unique and descriptive for someone to
168 /// easily tell which part of code was measured.
169 final String name;
170
171 /// The timestamp in microseconds that marks the beginning of the measured
172 /// block of code.
173 final double start;
174
175 /// The timestamp in microseconds that marks the end of the measured block of
176 /// code.
177 final double end;
178
179 /// How long the measured block of code took to execute in microseconds.
180 double get duration => end - start;
181
182 @override
183 String toString() {
184 return 'TimedBlock($name, $start, $end, $duration)';
185 }
186}
187
188/// Provides aggregated results for timings collected by [FlutterTimeline].
189@immutable
190final class AggregatedTimings {
191 /// Creates aggregated timings for the provided timed blocks.
192 AggregatedTimings(this.timedBlocks);
193
194 /// All timed blocks collected between the last reset and [FlutterTimeline.debugCollect].
195 final List<TimedBlock> timedBlocks;
196
197 /// Aggregated timed blocks collected between the last reset and [FlutterTimeline.debugCollect].
198 ///
199 /// Does not guarantee that all code blocks will be reported. Only those that
200 /// executed since the last reset are listed here. Use [getAggregated] for
201 /// graceful handling of missing code blocks.
202 late final List<AggregatedTimedBlock> aggregatedBlocks = _computeAggregatedBlocks();
203
204 List<AggregatedTimedBlock> _computeAggregatedBlocks() {
205 final Map<String, (double, int)> aggregate = <String, (double, int)>{};
206 for (final TimedBlock block in timedBlocks) {
207 final (double, int) previousValue = aggregate.putIfAbsent(block.name, () => (0, 0));
208 aggregate[block.name] = (previousValue.$1 + block.duration, previousValue.$2 + 1);
209 }
210 return aggregate.entries.map<AggregatedTimedBlock>((MapEntry<String, (double, int)> entry) {
211 return AggregatedTimedBlock(name: entry.key, duration: entry.value.$1, count: entry.value.$2);
212 }).toList();
213 }
214
215 /// Returns aggregated numbers for a named block of code.
216 ///
217 /// If the block in question never executed since the last reset, returns an
218 /// aggregation with zero duration and count.
219 AggregatedTimedBlock getAggregated(String name) {
220 return aggregatedBlocks.singleWhere(
221 (AggregatedTimedBlock block) => block.name == name,
222 // Handle the case where there are no recorded blocks of the specified
223 // type. In this case, the aggregated duration is simply zero, and so is
224 // the number of occurrences (i.e. count).
225 orElse: () => AggregatedTimedBlock(name: name, duration: 0, count: 0),
226 );
227 }
228}
229
230/// Aggregates multiple [TimedBlock] objects that share a [name].
231///
232/// It is common for the same block of code to be executed multiple times within
233/// a frame. It is useful to combine multiple executions and report the total
234/// amount of time attributed to that block of code.
235@immutable
236final class AggregatedTimedBlock {
237 /// Creates a timed block of code from a [name] and [duration].
238 ///
239 /// The [name] should be sufficiently unique and descriptive for someone to
240 /// easily tell which part of code was measured.
241 const AggregatedTimedBlock({required this.name, required this.duration, required this.count})
242 : assert(duration >= 0);
243
244 /// A readable label for a block of code that was measured.
245 ///
246 /// This field should be sufficiently unique and descriptive for someone to
247 /// easily tell which part of code was measured.
248 final String name;
249
250 /// The sum of [TimedBlock.duration] values of aggregated blocks.
251 final double duration;
252
253 /// The number of [TimedBlock] objects aggregated.
254 final int count;
255
256 @override
257 String toString() {
258 return 'AggregatedTimedBlock($name, $duration, $count)';
259 }
260}
261
262const int _kSliceSize = 500;
263
264/// A growable list of float64 values with predictable [add] performance.
265///
266/// The list is organized into a "chain" of [Float64List]s. The object starts
267/// with a `Float64List` "slice". When [add] is called, the value is added to
268/// the slice. Once the slice is full, it is moved into the chain, and a new
269/// slice is allocated. Slice size is static and therefore its allocation has
270/// predictable cost. This is unlike the default [List] implementation, which,
271/// when full, doubles its buffer size and copies all old elements into the new
272/// buffer, leading to unpredictable performance. This makes it a poor choice
273/// for recording performance because buffer reallocation would affect the
274/// runtime.
275///
276/// The trade-off is that reading values back from the chain is more expensive
277/// compared to [List] because it requires iterating over multiple slices. This
278/// is a reasonable trade-off for performance metrics, because it is more
279/// important to minimize the overhead while recording metrics, than it is when
280/// reading them.
281final class _Float64ListChain {
282 _Float64ListChain();
283
284 final List<Float64List> _chain = <Float64List>[];
285 Float64List _slice = Float64List(_kSliceSize);
286 int _pointer = 0;
287
288 int get length => _length;
289 int _length = 0;
290
291 /// Adds and [element] to this chain.
292 void add(double element) {
293 _slice[_pointer] = element;
294 _pointer += 1;
295 _length += 1;
296 if (_pointer >= _kSliceSize) {
297 _chain.add(_slice);
298 _slice = Float64List(_kSliceSize);
299 _pointer = 0;
300 }
301 }
302
303 /// Returns all elements added to this chain.
304 ///
305 /// This getter is not optimized to be fast. It is assumed that when metrics
306 /// are read back, they do not affect the timings of the work being
307 /// benchmarked.
308 List<double> extractElements() {
309 return <double>[
310 for (final Float64List list in _chain) ...list,
311 for (int i = 0; i < _pointer; i++) _slice[i],
312 ];
313 }
314}
315
316/// Same as [_Float64ListChain] but for recording string values.
317final class _StringListChain {
318 _StringListChain();
319
320 final List<List<String?>> _chain = <List<String?>>[];
321 List<String?> _slice = List<String?>.filled(_kSliceSize, null);
322 int _pointer = 0;
323
324 int get length => _length;
325 int _length = 0;
326
327 /// Adds and [element] to this chain.
328 void add(String element) {
329 _slice[_pointer] = element;
330 _pointer += 1;
331 _length += 1;
332 if (_pointer >= _kSliceSize) {
333 _chain.add(_slice);
334 _slice = List<String?>.filled(_kSliceSize, null);
335 _pointer = 0;
336 }
337 }
338
339 /// Returns all elements added to this chain.
340 ///
341 /// This getter is not optimized to be fast. It is assumed that when metrics
342 /// are read back, they do not affect the timings of the work being
343 /// benchmarked.
344 List<String> extractElements() {
345 return <String>[
346 for (final List<String?> slice in _chain)
347 for (final String? value in slice) value!,
348 for (int i = 0; i < _pointer; i++) _slice[i]!,
349 ];
350 }
351}
352
353/// A buffer that records starts and ends of code blocks, and their names.
354final class _BlockBuffer {
355 // Start-finish blocks can be nested. Track this nestedness by stacking the
356 // start timestamps. Finish timestamps will pop timings from the stack and
357 // add the (start, finish) tuple to the _block.
358 static const int _stackDepth = 1000;
359 static final Float64List _startStack = Float64List(_stackDepth);
360 static final List<String?> _nameStack = List<String?>.filled(_stackDepth, null);
361 static int _stackPointer = 0;
362
363 final _Float64ListChain _starts = _Float64ListChain();
364 final _Float64ListChain _finishes = _Float64ListChain();
365 final _StringListChain _names = _StringListChain();
366
367 List<TimedBlock> computeTimings() {
368 assert(
369 _stackPointer == 0,
370 'Invalid sequence of `startSync` and `finishSync`.\n'
371 'The operation stack was not empty. The following operations are still '
372 'waiting to be finished via the `finishSync` method:\n'
373 '${List<String>.generate(_stackPointer, (int i) => _nameStack[i]!).join(', ')}',
374 );
375
376 final List<TimedBlock> result = <TimedBlock>[];
377 final int length = _finishes.length;
378 final List<double> starts = _starts.extractElements();
379 final List<double> finishes = _finishes.extractElements();
380 final List<String> names = _names.extractElements();
381
382 assert(starts.length == length);
383 assert(finishes.length == length);
384 assert(names.length == length);
385
386 for (int i = 0; i < length; i++) {
387 result.add(TimedBlock(start: starts[i], end: finishes[i], name: names[i]));
388 }
389
390 return result;
391 }
392
393 void startSync(String name, {Map<String, Object?>? arguments, Flow? flow}) {
394 _startStack[_stackPointer] = impl.performanceTimestamp;
395 _nameStack[_stackPointer] = name;
396 _stackPointer += 1;
397 }
398
399 void finishSync() {
400 assert(
401 _stackPointer > 0,
402 'Invalid sequence of `startSync` and `finishSync`.\n'
403 'Attempted to finish timing a block of code, but there are no pending '
404 '`startSync` calls.',
405 );
406
407 final double finishTime = impl.performanceTimestamp;
408 final double startTime = _startStack[_stackPointer - 1];
409 final String name = _nameStack[_stackPointer - 1]!;
410 _stackPointer -= 1;
411
412 _starts.add(startTime);
413 _finishes.add(finishTime);
414 _names.add(name);
415 }
416}
417