Merge package:timing into the tools monorepo
diff --git a/pkgs/timing/.github/dependabot.yml b/pkgs/timing/.github/dependabot.yml
new file mode 100644
index 0000000..cde02ad
--- /dev/null
+++ b/pkgs/timing/.github/dependabot.yml
@@ -0,0 +1,15 @@
+# Dependabot configuration file.
+# See https://docs.github.com/en/code-security/dependabot/dependabot-version-updates
+version: 2
+
+updates:
+ - package-ecosystem: github-actions
+ directory: /
+ schedule:
+ interval: monthly
+ labels:
+ - autosubmit
+ groups:
+ github-actions:
+ patterns:
+ - "*"
diff --git a/pkgs/timing/.github/workflows/publish.yaml b/pkgs/timing/.github/workflows/publish.yaml
new file mode 100644
index 0000000..fcb7ccb
--- /dev/null
+++ b/pkgs/timing/.github/workflows/publish.yaml
@@ -0,0 +1,14 @@
+# A CI configuration to auto-publish pub packages.
+
+name: Publish
+
+on:
+ pull_request:
+ branches: [ master ]
+ push:
+ tags: [ 'v[0-9]+.[0-9]+.[0-9]+*' ]
+
+jobs:
+ publish:
+ if: ${{ github.repository_owner == 'dart-lang' }}
+ uses: dart-lang/ecosystem/.github/workflows/publish.yaml@main
diff --git a/pkgs/timing/.github/workflows/test-package.yml b/pkgs/timing/.github/workflows/test-package.yml
new file mode 100644
index 0000000..416123a
--- /dev/null
+++ b/pkgs/timing/.github/workflows/test-package.yml
@@ -0,0 +1,56 @@
+name: Dart CI
+
+on:
+ # Run on PRs and pushes to the default branch.
+ push:
+ branches: [ master ]
+ pull_request:
+ branches: [ master ]
+ schedule:
+ - cron: "0 0 * * 0"
+
+env:
+ PUB_ENVIRONMENT: bot.github
+
+jobs:
+ # Check code formatting and static analysis on a single OS (linux)
+ # against Dart dev.
+ analyze:
+ runs-on: ubuntu-latest
+ strategy:
+ fail-fast: false
+ matrix:
+ sdk: [3.4, dev]
+ steps:
+ - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
+ - uses: dart-lang/setup-dart@e630b99d28a3b71860378cafdc2a067c71107f94
+ with:
+ sdk: ${{ matrix.sdk }}
+ - id: install
+ run: dart pub get
+ - run: dart format --output=none --set-exit-if-changed .
+ if: always() && steps.install.outcome == 'success'
+ - run: dart analyze --fatal-infos
+ if: always() && steps.install.outcome == 'success'
+
+ # Run tests on a matrix consisting of two dimensions:
+ # 1. OS: ubuntu-latest, (macos-latest, windows-latest)
+ # 2. release channel: dev, 2.2.0
+ test:
+ needs: analyze
+ runs-on: ${{ matrix.os }}
+ strategy:
+ fail-fast: false
+ matrix:
+ # Add macos-latest and/or windows-latest if relevant for this package.
+ os: [ubuntu-latest]
+ sdk: [3.4, dev]
+ steps:
+ - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
+ - uses: dart-lang/setup-dart@e630b99d28a3b71860378cafdc2a067c71107f94
+ with:
+ sdk: ${{ matrix.sdk }}
+ - id: install
+ run: dart pub get
+ - run: dart test --platform vm
+ if: always() && steps.install.outcome == 'success'
diff --git a/pkgs/timing/.gitignore b/pkgs/timing/.gitignore
new file mode 100644
index 0000000..1ddf798
--- /dev/null
+++ b/pkgs/timing/.gitignore
@@ -0,0 +1,7 @@
+.packages
+/build/
+pubspec.lock
+
+# Files generated by dart tools
+.dart_tool
+doc/
diff --git a/pkgs/timing/CHANGELOG.md b/pkgs/timing/CHANGELOG.md
new file mode 100644
index 0000000..f173a3f
--- /dev/null
+++ b/pkgs/timing/CHANGELOG.md
@@ -0,0 +1,33 @@
+## 1.0.2-wip
+
+- Require Dart `3.4`.
+
+## 1.0.1
+
+- Require Dart `2.14`.
+
+## 1.0.0
+
+- Enable null safety.
+- Require Dart `2.12`.
+
+## 0.1.1+3
+
+- Allow `package:json_annotation` `'>=1.0.0 <5.0.0'`.
+
+## 0.1.1+2
+
+- Support the latest version of `package:json_annotation`.
+- Require Dart 2.2 or later.
+
+## 0.1.1+1
+
+- Support the latest version of `package:json_annotation`.
+
+## 0.1.1
+
+- Add JSON serialization
+
+## 0.1.0
+
+- Initial release
diff --git a/pkgs/timing/LICENSE b/pkgs/timing/LICENSE
new file mode 100644
index 0000000..9972f6e
--- /dev/null
+++ b/pkgs/timing/LICENSE
@@ -0,0 +1,27 @@
+Copyright 2018, the Dart project authors.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+ * Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above
+ copyright notice, this list of conditions and the following
+ disclaimer in the documentation and/or other materials provided
+ with the distribution.
+ * Neither the name of Google LLC nor the names of its
+ contributors may be used to endorse or promote products derived
+ from this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/pkgs/timing/README.md b/pkgs/timing/README.md
new file mode 100644
index 0000000..ad7481a
--- /dev/null
+++ b/pkgs/timing/README.md
@@ -0,0 +1,30 @@
+[](https://github.com/dart-lang/timing/actions/workflows/test-package.yml)
+[](https://pub.dev/packages/timing)
+[](https://pub.dev/packages/timing/publisher)
+
+Timing is a simple package for tracking performance of both async and sync actions
+
+## Usage
+
+```dart
+var tracker = AsyncTimeTracker();
+await tracker.track(() async {
+ // some async code here
+});
+
+// Use results
+print('${tracker.duration} ${tracker.innerDuration} ${tracker.slices}');
+```
+
+## Building
+
+Use the following command to re-generate `lib/src/timing.g.dart` file:
+
+```bash
+dart pub run build_runner build
+```
+
+## Publishing automation
+
+For information about our publishing automation and release process, see
+https://github.com/dart-lang/ecosystem/wiki/Publishing-automation.
diff --git a/pkgs/timing/analysis_options.yaml b/pkgs/timing/analysis_options.yaml
new file mode 100644
index 0000000..396236d
--- /dev/null
+++ b/pkgs/timing/analysis_options.yaml
@@ -0,0 +1,2 @@
+# https://dart.dev/tools/analysis#the-analysis-options-file
+include: package:dart_flutter_team_lints/analysis_options.yaml
diff --git a/pkgs/timing/lib/src/clock.dart b/pkgs/timing/lib/src/clock.dart
new file mode 100644
index 0000000..6a9d295
--- /dev/null
+++ b/pkgs/timing/lib/src/clock.dart
@@ -0,0 +1,20 @@
+// Copyright (c) 2017, 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:async';
+
+/// A function that returns the current [DateTime].
+typedef _Clock = DateTime Function();
+DateTime _defaultClock() => DateTime.now();
+
+const _zoneKey = #timing_Clock;
+
+/// Returns the current [DateTime].
+///
+/// May be overridden for tests using [scopeClock].
+DateTime now() => (Zone.current[_zoneKey] as _Clock? ?? _defaultClock)();
+
+/// Runs [f], with [clock] scoped whenever [now] is called.
+T scopeClock<T>(DateTime Function() clock, T Function() f) =>
+ runZoned(f, zoneValues: {_zoneKey: clock});
diff --git a/pkgs/timing/lib/src/timing.dart b/pkgs/timing/lib/src/timing.dart
new file mode 100644
index 0000000..049ba81
--- /dev/null
+++ b/pkgs/timing/lib/src/timing.dart
@@ -0,0 +1,338 @@
+// Copyright (c) 2018, 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:async';
+
+import 'package:json_annotation/json_annotation.dart';
+
+import 'clock.dart';
+
+part 'timing.g.dart';
+
+/// The timings of an operation, including its [startTime], [stopTime], and
+/// [duration].
+@JsonSerializable()
+class TimeSlice {
+ /// The total duration of this operation, equivalent to taking the difference
+ /// between [stopTime] and [startTime].
+ Duration get duration => stopTime.difference(startTime);
+
+ final DateTime startTime;
+
+ final DateTime stopTime;
+
+ TimeSlice(this.startTime, this.stopTime);
+
+ factory TimeSlice.fromJson(Map<String, dynamic> json) =>
+ _$TimeSliceFromJson(json);
+
+ Map<String, dynamic> toJson() => _$TimeSliceToJson(this);
+
+ @override
+ String toString() => '($startTime + $duration)';
+}
+
+/// The timings of an async operation, consist of several sync [slices] and
+/// includes total [startTime], [stopTime], and [duration].
+@JsonSerializable()
+class TimeSliceGroup implements TimeSlice {
+ final List<TimeSlice> slices;
+
+ @override
+ DateTime get startTime => slices.first.startTime;
+
+ @override
+ DateTime get stopTime => slices.last.stopTime;
+
+ /// The total duration of this operation, equivalent to taking the difference
+ /// between [stopTime] and [startTime].
+ @override
+ Duration get duration => stopTime.difference(startTime);
+
+ /// Sum of [duration]s of all [slices].
+ ///
+ /// If some of slices implements [TimeSliceGroup] [innerDuration] will be used
+ /// to compute sum.
+ Duration get innerDuration => slices.fold(
+ Duration.zero,
+ (duration, slice) =>
+ duration +
+ (slice is TimeSliceGroup ? slice.innerDuration : slice.duration));
+
+ TimeSliceGroup(this.slices);
+
+ /// Constructs TimeSliceGroup from JSON representation
+ factory TimeSliceGroup.fromJson(Map<String, dynamic> json) =>
+ _$TimeSliceGroupFromJson(json);
+
+ @override
+ Map<String, dynamic> toJson() => _$TimeSliceGroupToJson(this);
+
+ @override
+ String toString() => slices.toString();
+}
+
+abstract class TimeTracker implements TimeSlice {
+ /// Whether tracking is active.
+ ///
+ /// Tracking is only active after `isStarted` and before `isFinished`.
+ bool get isTracking;
+
+ /// Whether tracking is finished.
+ ///
+ /// Tracker can't be used as [TimeSlice] before it is finished
+ bool get isFinished;
+
+ /// Whether tracking was started.
+ ///
+ /// Equivalent of `isTracking || isFinished`
+ bool get isStarted;
+
+ T track<T>(T Function() action);
+}
+
+/// Tracks only sync actions
+class SyncTimeTracker implements TimeTracker {
+ /// When this operation started, call [_start] to set this.
+ @override
+ DateTime get startTime => _startTime!;
+ DateTime? _startTime;
+
+ /// When this operation stopped, call [_stop] to set this.
+ @override
+ DateTime get stopTime => _stopTime!;
+ DateTime? _stopTime;
+
+ /// Start tracking this operation, must only be called once, before [_stop].
+ void _start() {
+ assert(_startTime == null && _stopTime == null);
+ _startTime = now();
+ }
+
+ /// Stop tracking this operation, must only be called once, after [_start].
+ void _stop() {
+ assert(_startTime != null && _stopTime == null);
+ _stopTime = now();
+ }
+
+ /// Splits tracker into two slices.
+ ///
+ /// Returns new [TimeSlice] started on [startTime] and ended now. Modifies
+ /// [startTime] of tracker to current time point
+ ///
+ /// Don't change state of tracker. Can be called only while [isTracking], and
+ /// tracker will sill be tracking after call.
+ TimeSlice _split() {
+ if (!isTracking) {
+ throw StateError('Can be only called while tracking');
+ }
+ final splitPoint = now();
+ final prevSlice = TimeSlice(_startTime!, splitPoint);
+ _startTime = splitPoint;
+ return prevSlice;
+ }
+
+ @override
+ T track<T>(T Function() action) {
+ if (isStarted) {
+ throw StateError('Can not be tracked twice');
+ }
+ _start();
+ try {
+ return action();
+ } finally {
+ _stop();
+ }
+ }
+
+ @override
+ bool get isStarted => _startTime != null;
+
+ @override
+ bool get isTracking => _startTime != null && _stopTime == null;
+
+ @override
+ bool get isFinished => _startTime != null && _stopTime != null;
+
+ @override
+ Duration get duration => _stopTime!.difference(_startTime!);
+
+ /// Converts to JSON representation
+ ///
+ /// Can't be used before [isFinished]
+ @override
+ Map<String, dynamic> toJson() => _$TimeSliceToJson(this);
+}
+
+/// Async actions returning [Future] will be tracked as single sync time span
+/// from the beginning of execution till completion of future
+class SimpleAsyncTimeTracker extends SyncTimeTracker {
+ @override
+ T track<T>(T Function() action) {
+ if (isStarted) {
+ throw StateError('Can not be tracked twice');
+ }
+ T result;
+ _start();
+ try {
+ result = action();
+ } catch (_) {
+ _stop();
+ rethrow;
+ }
+ if (result is Future) {
+ return result.whenComplete(_stop) as T;
+ } else {
+ _stop();
+ return result;
+ }
+ }
+}
+
+/// No-op implementation of [SyncTimeTracker] that does nothing.
+class NoOpTimeTracker implements TimeTracker {
+ static final sharedInstance = NoOpTimeTracker();
+
+ @override
+ Duration get duration =>
+ throw UnsupportedError('Unsupported in no-op implementation');
+
+ @override
+ DateTime get startTime =>
+ throw UnsupportedError('Unsupported in no-op implementation');
+
+ @override
+ DateTime get stopTime =>
+ throw UnsupportedError('Unsupported in no-op implementation');
+
+ @override
+ bool get isStarted =>
+ throw UnsupportedError('Unsupported in no-op implementation');
+
+ @override
+ bool get isTracking =>
+ throw UnsupportedError('Unsupported in no-op implementation');
+
+ @override
+ bool get isFinished =>
+ throw UnsupportedError('Unsupported in no-op implementation');
+
+ @override
+ T track<T>(T Function() action) => action();
+
+ @override
+ Map<String, dynamic> toJson() =>
+ throw UnsupportedError('Unsupported in no-op implementation');
+}
+
+/// Track all async execution as disjoint time [slices] in ascending order.
+///
+/// Can [track] both async and sync actions.
+/// Can exclude time of tested trackers.
+///
+/// If tracked action spawns some dangled async executions behavior is't
+/// defined. Tracked might or might not track time of such executions
+class AsyncTimeTracker extends TimeSliceGroup implements TimeTracker {
+ final bool trackNested;
+
+ static const _zoneKey = #timing_AsyncTimeTracker;
+
+ AsyncTimeTracker({this.trackNested = true}) : super([]);
+
+ T _trackSyncSlice<T>(ZoneDelegate parent, Zone zone, T Function() action) {
+ // Ignore dangling runs after tracker completes
+ if (isFinished) {
+ return action();
+ }
+
+ final isNestedRun = slices.isNotEmpty &&
+ slices.last is SyncTimeTracker &&
+ (slices.last as SyncTimeTracker).isTracking;
+ final isExcludedNestedTrack = !trackNested && zone[_zoneKey] != this;
+
+ // Exclude nested sync tracks
+ if (isNestedRun && isExcludedNestedTrack) {
+ final timer = slices.last as SyncTimeTracker;
+ // Split already tracked time into new slice.
+ // Replace tracker in slices.last with splitted slice, to indicate for
+ // recursive calls that we not tracking.
+ slices.last = parent.run(zone, timer._split);
+ try {
+ return action();
+ } finally {
+ // Split tracker again and discard slice from nested tracker
+ parent.run(zone, timer._split);
+ // Add tracker back to list of slices and continue tracking
+ slices.add(timer);
+ }
+ }
+
+ // Exclude nested async tracks
+ if (isExcludedNestedTrack) {
+ return action();
+ }
+
+ // Split time slices in nested sync runs
+ if (isNestedRun) {
+ return action();
+ }
+
+ final timer = SyncTimeTracker();
+ slices.add(timer);
+
+ // Pass to parent zone, in case of overwritten clock
+ return parent.runUnary(zone, timer.track, action);
+ }
+
+ static final asyncTimeTrackerZoneSpecification = ZoneSpecification(
+ run: <R>(Zone self, ZoneDelegate parent, Zone zone, R Function() f) {
+ final tracker = self[_zoneKey] as AsyncTimeTracker;
+ return tracker._trackSyncSlice(parent, zone, () => parent.run(zone, f));
+ },
+ runUnary: <R, T>(Zone self, ZoneDelegate parent, Zone zone, R Function(T) f,
+ T arg) {
+ final tracker = self[_zoneKey] as AsyncTimeTracker;
+ return tracker._trackSyncSlice(
+ parent, zone, () => parent.runUnary(zone, f, arg));
+ },
+ runBinary: <R, T1, T2>(Zone self, ZoneDelegate parent, Zone zone,
+ R Function(T1, T2) f, T1 arg1, T2 arg2) {
+ final tracker = self[_zoneKey] as AsyncTimeTracker;
+ return tracker._trackSyncSlice(
+ parent, zone, () => parent.runBinary(zone, f, arg1, arg2));
+ },
+ );
+
+ @override
+ T track<T>(T Function() action) {
+ if (isStarted) {
+ throw StateError('Can not be tracked twice');
+ }
+ _tracking = true;
+ final result = runZoned(action,
+ zoneSpecification: asyncTimeTrackerZoneSpecification,
+ zoneValues: {_zoneKey: this});
+ if (result is Future) {
+ return result
+ // Break possible sync processing of future completion, so slice
+ // trackers can be finished
+ .whenComplete(Future.value)
+ .whenComplete(() => _tracking = false) as T;
+ } else {
+ _tracking = false;
+ return result;
+ }
+ }
+
+ bool? _tracking;
+
+ @override
+ bool get isStarted => _tracking != null;
+
+ @override
+ bool get isFinished => _tracking == false;
+
+ @override
+ bool get isTracking => _tracking == true;
+}
diff --git a/pkgs/timing/lib/src/timing.g.dart b/pkgs/timing/lib/src/timing.g.dart
new file mode 100644
index 0000000..679c082
--- /dev/null
+++ b/pkgs/timing/lib/src/timing.g.dart
@@ -0,0 +1,29 @@
+// GENERATED CODE - DO NOT MODIFY BY HAND
+
+part of 'timing.dart';
+
+// **************************************************************************
+// JsonSerializableGenerator
+// **************************************************************************
+
+TimeSlice _$TimeSliceFromJson(Map<String, dynamic> json) => TimeSlice(
+ DateTime.parse(json['startTime'] as String),
+ DateTime.parse(json['stopTime'] as String),
+ );
+
+Map<String, dynamic> _$TimeSliceToJson(TimeSlice instance) => <String, dynamic>{
+ 'startTime': instance.startTime.toIso8601String(),
+ 'stopTime': instance.stopTime.toIso8601String(),
+ };
+
+TimeSliceGroup _$TimeSliceGroupFromJson(Map<String, dynamic> json) =>
+ TimeSliceGroup(
+ (json['slices'] as List<dynamic>)
+ .map((e) => TimeSlice.fromJson(e as Map<String, dynamic>))
+ .toList(),
+ );
+
+Map<String, dynamic> _$TimeSliceGroupToJson(TimeSliceGroup instance) =>
+ <String, dynamic>{
+ 'slices': instance.slices,
+ };
diff --git a/pkgs/timing/lib/timing.dart b/pkgs/timing/lib/timing.dart
new file mode 100644
index 0000000..5cb16d4
--- /dev/null
+++ b/pkgs/timing/lib/timing.dart
@@ -0,0 +1,13 @@
+// Copyright (c) 2018, 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.
+
+export 'src/timing.dart'
+ show
+ AsyncTimeTracker,
+ NoOpTimeTracker,
+ SimpleAsyncTimeTracker,
+ SyncTimeTracker,
+ TimeSlice,
+ TimeSliceGroup,
+ TimeTracker;
diff --git a/pkgs/timing/pubspec.yaml b/pkgs/timing/pubspec.yaml
new file mode 100644
index 0000000..0be8e50
--- /dev/null
+++ b/pkgs/timing/pubspec.yaml
@@ -0,0 +1,18 @@
+name: timing
+version: 1.0.2-wip
+description: >-
+ A simple package for tracking the performance of synchronous and asynchronous
+ actions.
+repository: https://github.com/dart-lang/timing
+
+environment:
+ sdk: ^3.4.0
+
+dependencies:
+ json_annotation: ^4.9.0
+
+dev_dependencies:
+ build_runner: ^2.0.6
+ dart_flutter_team_lints: ^3.0.0
+ json_serializable: ^6.0.0
+ test: ^1.17.10
diff --git a/pkgs/timing/test/timing_test.dart b/pkgs/timing/test/timing_test.dart
new file mode 100644
index 0000000..b5836d9
--- /dev/null
+++ b/pkgs/timing/test/timing_test.dart
@@ -0,0 +1,416 @@
+// Copyright (c) 2018, 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.
+
+// ignore_for_file: only_throw_errors, inference_failure_on_instance_creation
+
+import 'dart:async';
+
+import 'package:test/test.dart';
+import 'package:timing/src/clock.dart';
+import 'package:timing/src/timing.dart';
+
+void _noop() {}
+
+void main() {
+ late DateTime time;
+ final startTime = DateTime(2017);
+ DateTime fakeClock() => time;
+
+ late TimeTracker tracker;
+ late TimeTracker nestedTracker;
+
+ T scopedTrack<T>(T Function() f) =>
+ scopeClock(fakeClock, () => tracker.track(f));
+
+ setUp(() {
+ time = startTime;
+ });
+
+ void canHandleSync([void Function() additionalExpects = _noop]) {
+ test('Can track sync code', () {
+ expect(tracker.isStarted, false);
+ expect(tracker.isTracking, false);
+ expect(tracker.isFinished, false);
+ scopedTrack(() {
+ expect(tracker.isStarted, true);
+ expect(tracker.isTracking, true);
+ expect(tracker.isFinished, false);
+ time = time.add(const Duration(seconds: 5));
+ });
+ expect(tracker.isStarted, true);
+ expect(tracker.isTracking, false);
+ expect(tracker.isFinished, true);
+ expect(tracker.startTime, startTime);
+ expect(tracker.stopTime, time);
+ expect(tracker.duration, const Duration(seconds: 5));
+ additionalExpects();
+ });
+
+ test('Can track handled sync exceptions', () async {
+ scopedTrack(() {
+ try {
+ time = time.add(const Duration(seconds: 4));
+ throw 'error';
+ } on String {
+ time = time.add(const Duration(seconds: 1));
+ }
+ });
+ expect(tracker.isFinished, true);
+ expect(tracker.startTime, startTime);
+ expect(tracker.stopTime, time);
+ expect(tracker.duration, const Duration(seconds: 5));
+ additionalExpects();
+ });
+
+ test('Can track in case of unhandled sync exceptions', () async {
+ expect(
+ () => scopedTrack(() {
+ time = time.add(const Duration(seconds: 5));
+ throw 'error';
+ }),
+ throwsA(const TypeMatcher<String>()));
+ expect(tracker.startTime, startTime);
+ expect(tracker.stopTime, time);
+ expect(tracker.duration, const Duration(seconds: 5));
+ additionalExpects();
+ });
+
+ test('Can be nested sync', () {
+ scopedTrack(() {
+ time = time.add(const Duration(seconds: 1));
+ nestedTracker.track(() {
+ time = time.add(const Duration(seconds: 2));
+ });
+ time = time.add(const Duration(seconds: 4));
+ });
+ expect(tracker.isFinished, true);
+ expect(tracker.startTime, startTime);
+ expect(tracker.stopTime, time);
+ expect(tracker.duration, const Duration(seconds: 7));
+ expect(nestedTracker.startTime.isAfter(startTime), true);
+ expect(nestedTracker.stopTime.isBefore(time), true);
+ expect(nestedTracker.duration, const Duration(seconds: 2));
+ additionalExpects();
+ });
+ }
+
+ void canHandleAsync([void Function() additionalExpects = _noop]) {
+ test('Can track async code', () async {
+ expect(tracker.isStarted, false);
+ expect(tracker.isTracking, false);
+ expect(tracker.isFinished, false);
+ await scopedTrack(() => Future(() {
+ expect(tracker.isStarted, true);
+ expect(tracker.isTracking, true);
+ expect(tracker.isFinished, false);
+ time = time.add(const Duration(seconds: 5));
+ }));
+ expect(tracker.isStarted, true);
+ expect(tracker.isTracking, false);
+ expect(tracker.isFinished, true);
+ expect(tracker.startTime, startTime);
+ expect(tracker.stopTime, time);
+ expect(tracker.duration, const Duration(seconds: 5));
+ additionalExpects();
+ });
+
+ test('Can track handled async exceptions', () async {
+ await scopedTrack(() {
+ time = time.add(const Duration(seconds: 1));
+ return Future(() {
+ time = time.add(const Duration(seconds: 2));
+ throw 'error';
+ }).then((_) {
+ time = time.add(const Duration(seconds: 4));
+ }).catchError((error, stack) {
+ time = time.add(const Duration(seconds: 8));
+ });
+ });
+ expect(tracker.isFinished, true);
+ expect(tracker.startTime, startTime);
+ expect(tracker.stopTime, time);
+ expect(tracker.duration, const Duration(seconds: 11));
+ additionalExpects();
+ });
+
+ test('Can track in case of unhandled async exceptions', () async {
+ final future = scopedTrack(() {
+ time = time.add(const Duration(seconds: 1));
+ return Future(() {
+ time = time.add(const Duration(seconds: 2));
+ throw 'error';
+ }).then((_) {
+ time = time.add(const Duration(seconds: 4));
+ });
+ });
+ await expectLater(future, throwsA(const TypeMatcher<String>()));
+ expect(tracker.isFinished, true);
+ expect(tracker.startTime, startTime);
+ expect(tracker.stopTime, time);
+ expect(tracker.duration, const Duration(seconds: 3));
+ additionalExpects();
+ });
+
+ test('Can be nested async', () async {
+ await scopedTrack(() async {
+ time = time.add(const Duration(milliseconds: 1));
+ await Future.value();
+ time = time.add(const Duration(milliseconds: 2));
+ await nestedTracker.track(() async {
+ time = time.add(const Duration(milliseconds: 4));
+ await Future.value();
+ time = time.add(const Duration(milliseconds: 8));
+ await Future.value();
+ time = time.add(const Duration(milliseconds: 16));
+ });
+ time = time.add(const Duration(milliseconds: 32));
+ await Future.value();
+ time = time.add(const Duration(milliseconds: 64));
+ });
+ expect(tracker.isFinished, true);
+ expect(tracker.startTime, startTime);
+ expect(tracker.stopTime, time);
+ expect(tracker.duration, const Duration(milliseconds: 127));
+ expect(nestedTracker.startTime.isAfter(startTime), true);
+ expect(nestedTracker.stopTime.isBefore(time), true);
+ expect(nestedTracker.duration, const Duration(milliseconds: 28));
+ additionalExpects();
+ });
+ }
+
+ group('SyncTimeTracker', () {
+ setUp(() {
+ tracker = SyncTimeTracker();
+ nestedTracker = SyncTimeTracker();
+ });
+
+ canHandleSync();
+
+ test('Can not track async code', () async {
+ await scopedTrack(() => Future(() {
+ time = time.add(const Duration(seconds: 5));
+ }));
+ expect(tracker.isFinished, true);
+ expect(tracker.startTime, startTime);
+ expect(tracker.stopTime, startTime);
+ expect(tracker.duration, const Duration(seconds: 0));
+ });
+ });
+
+ group('AsyncTimeTracker.simple', () {
+ setUp(() {
+ tracker = SimpleAsyncTimeTracker();
+ nestedTracker = SimpleAsyncTimeTracker();
+ });
+
+ canHandleSync();
+
+ canHandleAsync();
+
+ test('Can not distinguish own async code', () async {
+ final future = scopedTrack(() => Future(() {
+ time = time.add(const Duration(seconds: 5));
+ }));
+ time = time.add(const Duration(seconds: 10));
+ await future;
+ expect(tracker.isFinished, true);
+ expect(tracker.startTime, startTime);
+ expect(tracker.stopTime, time);
+ expect(tracker.duration, const Duration(seconds: 15));
+ });
+ });
+
+ group('AsyncTimeTracker', () {
+ late AsyncTimeTracker asyncTracker;
+ late AsyncTimeTracker nestedAsyncTracker;
+ setUp(() {
+ tracker = asyncTracker = AsyncTimeTracker();
+ nestedTracker = nestedAsyncTracker = AsyncTimeTracker();
+ });
+
+ canHandleSync(() {
+ expect(asyncTracker.innerDuration, asyncTracker.duration);
+ expect(asyncTracker.slices.length, 1);
+ });
+
+ canHandleAsync(() {
+ expect(asyncTracker.innerDuration, asyncTracker.duration);
+ expect(asyncTracker.slices.length, greaterThan(1));
+ });
+
+ test('Can track complex async innerDuration', () async {
+ final completer = Completer();
+ final future = scopedTrack(() async {
+ time = time.add(const Duration(seconds: 1)); // Tracked sync
+ await Future.value();
+ time = time.add(const Duration(seconds: 2)); // Tracked async
+ await completer.future;
+ time = time.add(const Duration(seconds: 4)); // Tracked async, delayed
+ }).then((_) {
+ time = time.add(const Duration(seconds: 8)); // Async, after tracking
+ });
+ time = time.add(const Duration(seconds: 16)); // Sync, between slices
+
+ await Future(() {
+ // Async, between slices
+ time = time.add(const Duration(seconds: 32));
+ completer.complete();
+ });
+ await future;
+ expect(asyncTracker.isFinished, true);
+ expect(asyncTracker.startTime, startTime);
+ expect(asyncTracker.stopTime.isBefore(time), true);
+ expect(asyncTracker.duration, const Duration(seconds: 55));
+ expect(asyncTracker.innerDuration, const Duration(seconds: 7));
+ expect(asyncTracker.slices.length, greaterThan(1));
+ });
+
+ test('Can exclude nested sync', () {
+ tracker = asyncTracker = AsyncTimeTracker(trackNested: false);
+ scopedTrack(() {
+ time = time.add(const Duration(seconds: 1));
+ nestedAsyncTracker.track(() {
+ time = time.add(const Duration(seconds: 2));
+ });
+ time = time.add(const Duration(seconds: 4));
+ });
+ expect(asyncTracker.isFinished, true);
+ expect(asyncTracker.startTime, startTime);
+ expect(asyncTracker.stopTime, time);
+ expect(asyncTracker.duration, const Duration(seconds: 7));
+ expect(asyncTracker.innerDuration, const Duration(seconds: 5));
+ expect(asyncTracker.slices.length, greaterThan(1));
+ expect(nestedAsyncTracker.startTime.isAfter(startTime), true);
+ expect(nestedAsyncTracker.stopTime.isBefore(time), true);
+ expect(nestedAsyncTracker.duration, const Duration(seconds: 2));
+ expect(nestedAsyncTracker.innerDuration, const Duration(seconds: 2));
+ expect(nestedAsyncTracker.slices.length, 1);
+ });
+
+ test('Can exclude complex nested sync', () {
+ tracker = asyncTracker = AsyncTimeTracker(trackNested: false);
+ nestedAsyncTracker = AsyncTimeTracker(trackNested: false);
+ final nestedAsyncTracker2 = AsyncTimeTracker(trackNested: false);
+ scopedTrack(() {
+ time = time.add(const Duration(seconds: 1));
+ nestedAsyncTracker.track(() {
+ time = time.add(const Duration(seconds: 2));
+ nestedAsyncTracker2.track(() {
+ time = time.add(const Duration(seconds: 4));
+ });
+ time = time.add(const Duration(seconds: 8));
+ });
+ time = time.add(const Duration(seconds: 16));
+ });
+ expect(asyncTracker.isFinished, true);
+ expect(asyncTracker.startTime, startTime);
+ expect(asyncTracker.stopTime, time);
+ expect(asyncTracker.duration, const Duration(seconds: 31));
+ expect(asyncTracker.innerDuration, const Duration(seconds: 17));
+ expect(asyncTracker.slices.length, greaterThan(1));
+ expect(nestedAsyncTracker.startTime.isAfter(startTime), true);
+ expect(nestedAsyncTracker.stopTime.isBefore(time), true);
+ expect(nestedAsyncTracker.duration, const Duration(seconds: 14));
+ expect(nestedAsyncTracker.innerDuration, const Duration(seconds: 10));
+ expect(nestedAsyncTracker.slices.length, greaterThan(1));
+ expect(nestedAsyncTracker2.startTime.isAfter(startTime), true);
+ expect(nestedAsyncTracker2.stopTime.isBefore(time), true);
+ expect(nestedAsyncTracker2.duration, const Duration(seconds: 4));
+ expect(nestedAsyncTracker2.innerDuration, const Duration(seconds: 4));
+ expect(nestedAsyncTracker2.slices.length, 1);
+ });
+
+ test(
+ 'Can track all on grand-parent level and '
+ 'exclude grand-childrens from parent', () {
+ tracker = asyncTracker = AsyncTimeTracker(trackNested: true);
+ nestedAsyncTracker = AsyncTimeTracker(trackNested: false);
+ final nestedAsyncTracker2 = AsyncTimeTracker();
+ scopedTrack(() {
+ time = time.add(const Duration(seconds: 1));
+ nestedAsyncTracker.track(() {
+ time = time.add(const Duration(seconds: 2));
+ nestedAsyncTracker2.track(() {
+ time = time.add(const Duration(seconds: 4));
+ });
+ time = time.add(const Duration(seconds: 8));
+ });
+ time = time.add(const Duration(seconds: 16));
+ });
+ expect(asyncTracker.isFinished, true);
+ expect(asyncTracker.startTime, startTime);
+ expect(asyncTracker.stopTime, time);
+ expect(asyncTracker.duration, const Duration(seconds: 31));
+ expect(asyncTracker.innerDuration, const Duration(seconds: 31));
+ expect(asyncTracker.slices.length, 1);
+ expect(nestedAsyncTracker.startTime.isAfter(startTime), true);
+ expect(nestedAsyncTracker.stopTime.isBefore(time), true);
+ expect(nestedAsyncTracker.duration, const Duration(seconds: 14));
+ expect(nestedAsyncTracker.innerDuration, const Duration(seconds: 10));
+ expect(nestedAsyncTracker.slices.length, greaterThan(1));
+ expect(nestedAsyncTracker2.startTime.isAfter(startTime), true);
+ expect(nestedAsyncTracker2.stopTime.isBefore(time), true);
+ expect(nestedAsyncTracker2.duration, const Duration(seconds: 4));
+ expect(nestedAsyncTracker2.innerDuration, const Duration(seconds: 4));
+ expect(nestedAsyncTracker2.slices.length, 1);
+ });
+
+ test('Can exclude nested async', () async {
+ tracker = asyncTracker = AsyncTimeTracker(trackNested: false);
+ await scopedTrack(() async {
+ time = time.add(const Duration(seconds: 1));
+ await nestedAsyncTracker.track(() async {
+ time = time.add(const Duration(seconds: 2));
+ await Future.value();
+ time = time.add(const Duration(seconds: 4));
+ await Future.value();
+ time = time.add(const Duration(seconds: 8));
+ });
+ time = time.add(const Duration(seconds: 16));
+ });
+ expect(asyncTracker.isFinished, true);
+ expect(asyncTracker.startTime, startTime);
+ expect(asyncTracker.stopTime, time);
+ expect(asyncTracker.duration, const Duration(seconds: 31));
+ expect(asyncTracker.innerDuration, const Duration(seconds: 17));
+ expect(asyncTracker.slices.length, greaterThan(1));
+ expect(nestedAsyncTracker.startTime.isAfter(startTime), true);
+ expect(nestedAsyncTracker.stopTime.isBefore(time), true);
+ expect(nestedAsyncTracker.duration, const Duration(seconds: 14));
+ expect(nestedAsyncTracker.innerDuration, const Duration(seconds: 14));
+ expect(nestedAsyncTracker.slices.length, greaterThan(1));
+ });
+
+ test('Can handle callbacks in excluded nested async', () async {
+ tracker = asyncTracker = AsyncTimeTracker(trackNested: false);
+ await scopedTrack(() async {
+ time = time.add(const Duration(seconds: 1));
+ final completer = Completer();
+ final future = completer.future.then((_) {
+ time = time.add(const Duration(seconds: 2));
+ });
+ await nestedAsyncTracker.track(() async {
+ time = time.add(const Duration(seconds: 4));
+ await Future.value();
+ time = time.add(const Duration(seconds: 8));
+ completer.complete();
+ await future;
+ time = time.add(const Duration(seconds: 16));
+ });
+ time = time.add(const Duration(seconds: 32));
+ });
+ expect(asyncTracker.isFinished, true);
+ expect(asyncTracker.startTime, startTime);
+ expect(asyncTracker.stopTime, time);
+ expect(asyncTracker.duration, const Duration(seconds: 63));
+ expect(asyncTracker.innerDuration, const Duration(seconds: 35));
+ expect(asyncTracker.slices.length, greaterThan(1));
+ expect(nestedAsyncTracker.startTime.isAfter(startTime), true);
+ expect(nestedAsyncTracker.stopTime.isBefore(time), true);
+ expect(nestedAsyncTracker.duration, const Duration(seconds: 30));
+ expect(nestedAsyncTracker.innerDuration, const Duration(seconds: 28));
+ expect(nestedAsyncTracker.slices.length, greaterThan(1));
+ });
+ });
+}