| // Copyright (c) 2020, the Dart project authors. Please see the AUTHORS file |
| // for details. 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' as developer; |
| |
| import 'package:collection/collection.dart'; |
| |
| /// Callback type for [OperationPerformanceImpl.runAsyncHook]. |
| typedef RunAsyncHook = |
| Future<R> Function<R, T>( |
| String name, |
| Future<R> Function(T) operation, |
| T arg, { |
| Map<String, Object?>? arguments, |
| }); |
| |
| /// The performance of an operation. |
| abstract class OperationPerformance { |
| /// The child operations, might be empty. |
| List<OperationPerformance> get children; |
| |
| /// The number of times this child has been started/run. |
| int get count; |
| |
| /// The data attachments, for non-timing data, e.g. how many files were read, |
| /// or how many bytes were processed. |
| List<OperationPerformanceData> get data; |
| |
| /// The duration of this operation, including its children. |
| Duration get elapsed; |
| |
| /// The duration of this operation, excluding its children. |
| Duration get elapsedSelf; |
| |
| /// The name of the operation. |
| String get name; |
| |
| OperationPerformance? getChild(String name); |
| |
| Map<String, Object?> toJson(); |
| |
| /// Write this operation and its children into the [buffer]. |
| void write({required StringBuffer buffer, String indent = ''}); |
| } |
| |
| /// The data attachment for an [OperationPerformance]. |
| abstract class OperationPerformanceData<T> { |
| String get name; |
| |
| T get value; |
| } |
| |
| abstract class OperationPerformanceDataImpl<T> |
| implements OperationPerformanceData<T> { |
| @override |
| final String name; |
| |
| OperationPerformanceDataImpl(this.name); |
| |
| @override |
| String toString() { |
| return '$name: $value'; |
| } |
| } |
| |
| // Pre-existing name. |
| // ignore: camel_case_types |
| class OperationPerformanceDataImpl_int |
| extends OperationPerformanceDataImpl<int> { |
| @override |
| int value = 0; |
| |
| OperationPerformanceDataImpl_int(super.name); |
| |
| void add(int item) { |
| value += item; |
| } |
| |
| void increment() { |
| value++; |
| } |
| } |
| |
| // Pre-existing name. |
| // ignore: camel_case_types |
| class OperationPerformanceDataImpl_Set<T> |
| extends OperationPerformanceDataImpl<Set<T>> { |
| @override |
| final Set<T> value = {}; |
| |
| OperationPerformanceDataImpl_Set(super.name); |
| |
| void add(T item) { |
| value.add(item); |
| } |
| |
| @override |
| String toString() { |
| return '$name: ${value.length}'; |
| } |
| } |
| |
| class OperationPerformanceImpl implements OperationPerformance { |
| /// This closure can be overriden to intercept [runAsync] invocations. |
| static RunAsyncHook runAsyncHook = |
| <R, T>(name, operation, arg, {arguments}) => operation(arg); |
| |
| @override |
| final String name; |
| final Stopwatch _timer = Stopwatch(); |
| int _count = 0; |
| |
| final List<OperationPerformanceImpl> _children = []; |
| |
| final Map<String, OperationPerformanceData<Object>> _data = {}; |
| |
| OperationPerformanceImpl(this.name); |
| |
| @override |
| List<OperationPerformance> get children { |
| return _children; |
| } |
| |
| @override |
| int get count => _count; |
| |
| @override |
| List<OperationPerformanceData<Object>> get data { |
| return _data.values.toList(); |
| } |
| |
| @override |
| Duration get elapsed { |
| return _timer.elapsed; |
| } |
| |
| @override |
| Duration get elapsedSelf { |
| return elapsed - _elapsedChildren; |
| } |
| |
| Duration get _elapsedChildren { |
| return children.fold<Duration>( |
| Duration.zero, |
| (sum, child) => sum + child.elapsed, |
| ); |
| } |
| |
| /// Collapse any children into this operation, making [elapsedSelf] equal |
| /// to [elapsed]. |
| void collapse() { |
| _children.clear(); |
| } |
| |
| @override |
| OperationPerformanceImpl? getChild(String name) { |
| return _children.firstWhereOrNull((child) => child.name == name); |
| } |
| |
| OperationPerformanceDataImpl_int getDataInt(String name) { |
| var data = _data[name]; |
| if (data is OperationPerformanceDataImpl_int) { |
| return data; |
| } else if (data != null) { |
| throw StateError('Not int: ${data.runtimeType}'); |
| } else { |
| return _data[name] = OperationPerformanceDataImpl_int(name); |
| } |
| } |
| |
| OperationPerformanceDataImpl_Set<T> getDataSet<T>(String name) { |
| var data = _data[name]; |
| if (data is OperationPerformanceDataImpl_Set<T>) { |
| return data; |
| } else if (data != null) { |
| throw StateError('Not Set: ${data.runtimeType}'); |
| } else { |
| return _data[name] = OperationPerformanceDataImpl_Set<T>(name); |
| } |
| } |
| |
| /// Run the [operation] as a child with the given [name]. |
| /// |
| /// If there is no such child, a new one is created, with a new timer. |
| /// |
| /// If there is already a child with that name, its timer will resume and |
| /// then stop. So, it will accumulate time across all runs. |
| /// |
| /// This also creates a synchronous [developer.Timeline] span with the |
| /// given [name] and specified [arguments] (serialized as JSON). |
| T run<T>( |
| String name, |
| T Function(OperationPerformanceImpl performance) operation, { |
| Map<String, Object?>? arguments, |
| }) { |
| OperationPerformanceImpl child = _existingOrNewChild(name); |
| child._start(); |
| |
| try { |
| developer.Timeline.startSync(name, arguments: arguments); |
| return operation(child); |
| } finally { |
| developer.Timeline.finishSync(); |
| child._stop(); |
| } |
| } |
| |
| /// Run the [operation] as a child with the given [name]. |
| /// |
| /// If there is no such child, a new one is created, with a new timer. |
| /// |
| /// If there is already a child with that name, its timer will resume and |
| /// then stop. So, it will accumulate time across all runs. |
| /// |
| /// Execution of the [operation] will be perfomed by the [runAsyncHook], |
| /// which by default simply executes the [operation], but this hook can be |
| /// overriden externally and emit one or more [developer.Timeline] spans |
| /// capturing synchronous execution time spent inside the [operation]. In |
| /// this case [arguments] will be recorded (serialized as JSON) in the |
| /// first emitted span. |
| Future<T> runAsync<T>( |
| String name, |
| Future<T> Function(OperationPerformanceImpl performance) operation, { |
| Map<String, Object?>? arguments, |
| }) async { |
| var child = _existingOrNewChild(name); |
| child._start(); |
| try { |
| return await runAsyncHook(name, operation, child, arguments: arguments); |
| } finally { |
| child._stop(); |
| } |
| } |
| |
| @override |
| Map<String, Object?> toJson() { |
| return <String, Object?>{ |
| 'name': name, |
| 'count': count, |
| 'elapsed': elapsed.inMicroseconds, |
| 'elapsedSelf': elapsedSelf.inMicroseconds, |
| 'children': children, |
| }; |
| } |
| |
| @override |
| String toString() { |
| return '(name: $name, count: $_count, ' |
| 'elapsed: $elapsed, elapsedSelf: $elapsedSelf)'; |
| } |
| |
| @override |
| void write({required StringBuffer buffer, String indent = ''}) { |
| buffer.write('$indent${toString()}'); |
| |
| if (_data.isNotEmpty) { |
| var sortedNames = _data.keys.toList()..sort(); |
| var sortedData = sortedNames.map((name) => _data[name]); |
| var dataStr = sortedData.map((e) => '$e').join(', '); |
| buffer.write('($dataStr)'); |
| } |
| |
| buffer.writeln(); |
| |
| var childIndent = '$indent '; |
| var sortedChildren = children.toList() |
| ..sort((a, b) => b.elapsed.compareTo(a.elapsed)); |
| for (var child in sortedChildren) { |
| child.write(buffer: buffer, indent: childIndent); |
| } |
| } |
| |
| OperationPerformanceImpl _existingOrNewChild(String name) { |
| var child = getChild(name); |
| if (child == null) { |
| child = OperationPerformanceImpl(name); |
| _children.add(child); |
| } |
| return child; |
| } |
| |
| void _start() { |
| _timer.start(); |
| _count++; |
| } |
| |
| void _stop() { |
| _timer.stop(); |
| } |
| } |