blob: 17e2fb11d79f078434ba2d4c53cde84cbb4965fa [file] [log] [blame]
// 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(nullable: false)
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(nullable: false)
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 _now = now();
final prevSlice = TimeSlice(_startTime, _now);
_startTime = _now;
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 that was spend in 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;
}