Avoid conflating concept of duration in `TimeRange` util class (#9325)
`TimeRange` represents the range between two `start` and `end` times marked in microseconds (so a duration). However, it stored `start` and `end` as `Duration` as well, resulting in the existence of three durations which was a bit confusing as I worked to understand code using `TimeRange`.
Beyond that, `TimeRange` was not immutable so had a concept of being "well formed" (or having both a start and end), resulting in multiple checks for that state and many not-null assertion operations. To maintain the intermediate status and also reduce the necessity of the not-null assertions, split out the intermediate state to a new `TimeRangeBuilder` class with a `build` method that consolidates the not-null assertions to that singular function.
diff --git a/packages/devtools_app/lib/src/screens/network/network_request_inspector_views.dart b/packages/devtools_app/lib/src/screens/network/network_request_inspector_views.dart
index f894963..00d2da0 100644
--- a/packages/devtools_app/lib/src/screens/network/network_request_inspector_views.dart
+++ b/packages/devtools_app/lib/src/screens/network/network_request_inspector_views.dart
@@ -696,7 +696,7 @@
// flex so that we can set a minimum width for small timing chunks.
final timingWidgets = <Widget>[];
for (final instant in data.instantEvents) {
- final duration = instant.timeRange!.duration;
+ final duration = instant.timeRange.duration;
timingWidgets.add(_buildTimingRow(nextColor(), instant.name, duration));
}
final duration = Duration(
@@ -714,14 +714,14 @@
final data = this.data as DartIOHttpRequestData;
final result = <Widget>[];
for (final instant in data.instantEvents) {
- final instantEventStart = data.instantEvents.first.timeRange!.start!;
- final timeRange = instant.timeRange!;
+ final instantEventStart = data.instantEvents.first.timeRange.start;
+ final timeRange = instant.timeRange;
final startDisplay = durationText(
- timeRange.start! - instantEventStart,
+ Duration(microseconds: timeRange.start - instantEventStart),
unit: DurationDisplayUnit.milliseconds,
);
final endDisplay = durationText(
- timeRange.end! - instantEventStart,
+ Duration(microseconds: timeRange.end - instantEventStart),
unit: DurationDisplayUnit.milliseconds,
);
final totalDisplay = durationText(
diff --git a/packages/devtools_app/lib/src/screens/performance/panes/flutter_frames/flutter_frame_model.dart b/packages/devtools_app/lib/src/screens/performance/panes/flutter_frames/flutter_frame_model.dart
index ee3e547..9b199db 100644
--- a/packages/devtools_app/lib/src/screens/performance/panes/flutter_frames/flutter_frame_model.dart
+++ b/packages/devtools_app/lib/src/screens/performance/panes/flutter_frames/flutter_frame_model.dart
@@ -2,8 +2,6 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file or at https://developers.google.com/open-source/licenses/bsd.
-import 'dart:math' as math;
-
import '../../../../shared/primitives/utils.dart';
import '../../performance_model.dart';
import '../controls/enhance_tracing/enhance_tracing_model.dart';
@@ -24,12 +22,10 @@
});
factory FlutterFrame.fromJson(Map<String, Object?> json) {
- final timeStart = Duration(microseconds: json[startTimeKey]! as int);
- final timeEnd =
- timeStart + Duration(microseconds: json[elapsedKey]! as int);
- final frameTime = TimeRange()
- ..start = timeStart
- ..end = timeEnd;
+ final frameTime = TimeRange.ofDuration(
+ json[elapsedKey]! as int,
+ start: json[startTimeKey]! as int,
+ );
return FlutterFrame._(
id: json[numberKey]! as int,
timeFromFrameTiming: frameTime,
@@ -58,14 +54,6 @@
/// which the data was parsed.
final TimeRange timeFromFrameTiming;
- /// The time range of the Flutter frame based on the frame's
- /// [timelineEventData], which contains timing information from the VM's
- /// timeline events.
- ///
- /// This time range should be used for activities related to timeline events,
- /// like scrolling a frame's timeline events into view, for example.
- TimeRange get timeFromEventFlows => timelineEventData.time;
-
/// Build time for this Flutter frame based on data from the FrameTiming API
/// sent over the extension stream as 'Flutter.Frame' events.
final Duration buildTime;
@@ -85,13 +73,12 @@
/// (e.g. when the 'Flutter.Frame' event for this frame was received).
///
/// If we did not have [EnhanceTracingState] information at the time that this
- /// frame was drawn (e.g. the DevTools performancd page was not opened and
+ /// frame was drawn (e.g. the DevTools performance page was not opened and
/// listening for frames yet), this value will be null.
EnhanceTracingState? enhanceTracingState;
FrameAnalysis? get frameAnalysis {
- final frameAnalysis_ = _frameAnalysis;
- if (frameAnalysis_ != null) return frameAnalysis_;
+ if (_frameAnalysis case final frameAnalysis?) return frameAnalysis;
if (timelineEventData.isNotEmpty) {
return _frameAnalysis = FrameAnalysis(this);
}
@@ -104,15 +91,17 @@
Duration get shaderDuration {
if (_shaderTime != null) return _shaderTime!;
- if (timelineEventData.rasterEvent == null) return Duration.zero;
- final shaderEvents = timelineEventData.rasterEvent!
- .shallowNodesWithCondition((event) => event.isShaderEvent);
- final duration = shaderEvents.fold<Duration>(Duration.zero, (
- previous,
- event,
- ) {
- return previous + event.time.duration;
- });
+ final rasterEvent = timelineEventData.rasterEvent;
+ if (rasterEvent == null) return Duration.zero;
+ final shaderEvents = rasterEvent.shallowNodesWithCondition(
+ (event) => event.isShaderEvent,
+ );
+ final duration = shaderEvents
+ .where((event) => event.isComplete)
+ .fold<Duration>(
+ Duration.zero,
+ (previous, event) => previous + event.time.duration,
+ );
return _shaderTime = duration;
}
@@ -150,7 +139,7 @@
Map<String, Object?> get json => {
numberKey: id,
- startTimeKey: timeFromFrameTiming.start!.inMicroseconds,
+ startTimeKey: timeFromFrameTiming.start,
elapsedKey: timeFromFrameTiming.duration.inMicroseconds,
buildKey: buildTime.inMicroseconds,
rasterKey: rasterTime.inMicroseconds,
@@ -197,42 +186,9 @@
bool get isNotEmpty => uiEvent != null || rasterEvent != null;
- final time = TimeRange();
-
- void setEventFlow({
- required FlutterTimelineEvent event,
- bool setTimeData = true,
- }) {
+ void setEventFlow({required FlutterTimelineEvent event}) {
final type = event.type!;
_eventFlows[type.index] = event;
- if (setTimeData) {
- if (type == TimelineEventType.ui) {
- time.start = event.time.start;
- // If [rasterEventFlow] has already completed, set the end time for this
- // frame to [event]'s end time.
- if (rasterEvent != null) {
- time.end = event.time.end;
- }
- } else if (type == TimelineEventType.raster) {
- // If [uiEventFlow] is null, that means that this raster event flow
- // completed before the ui event flow did for this frame. This means one
- // of two things: 1) there will never be a [uiEventFlow] for this frame
- // because the UI events are not present in the available timeline
- // events, or 2) the [uiEventFlow] has started but not completed yet. In
- // the event that 2) is true, do not set the frame end time here because
- // the end time for this frame will be set to the end time for
- // [uiEventFlow] once it finishes.
- final theUiEvent = uiEvent;
- if (theUiEvent != null) {
- time.end = Duration(
- microseconds: math.max(
- theUiEvent.time.end!.inMicroseconds,
- event.time.end?.inMicroseconds ?? 0,
- ),
- );
- }
- }
- }
}
FlutterTimelineEvent? eventByType(TimelineEventType type) {
diff --git a/packages/devtools_app/lib/src/screens/performance/panes/flutter_frames/flutter_frames_controller.dart b/packages/devtools_app/lib/src/screens/performance/panes/flutter_frames/flutter_frames_controller.dart
index 0c83414..ff97028 100644
--- a/packages/devtools_app/lib/src/screens/performance/panes/flutter_frames/flutter_frames_controller.dart
+++ b/packages/devtools_app/lib/src/screens/performance/panes/flutter_frames/flutter_frames_controller.dart
@@ -165,7 +165,7 @@
assert(frame.isWellFormed);
firstWellFormedFrameMicros = math.min(
firstWellFormedFrameMicros ?? maxJsInt,
- frame.timeFromFrameTiming.start!.inMicroseconds,
+ frame.timeFromFrameTiming.start,
);
}
diff --git a/packages/devtools_app/lib/src/screens/performance/panes/frame_analysis/frame_analysis_model.dart b/packages/devtools_app/lib/src/screens/performance/panes/frame_analysis/frame_analysis_model.dart
index eaa84ac..d1316e4 100644
--- a/packages/devtools_app/lib/src/screens/performance/panes/frame_analysis/frame_analysis_model.dart
+++ b/packages/devtools_app/lib/src/screens/performance/panes/frame_analysis/frame_analysis_model.dart
@@ -101,10 +101,7 @@
///
/// This is drawn from all events for this frame from the raster thread.
late FramePhase rasterPhase = FramePhase.raster(
- events: [
- if (frame.timelineEventData.rasterEvent != null)
- frame.timelineEventData.rasterEvent!,
- ],
+ events: [?frame.timelineEventData.rasterEvent],
);
late FramePhase longestUiPhase = _calculateLongestFramePhase();
@@ -276,9 +273,12 @@
: title = type.display,
duration =
duration ??
- events.fold<Duration>(Duration.zero, (previous, event) {
- return previous + event.time.duration;
- });
+ events
+ .where((event) => event.isComplete)
+ .fold<Duration>(
+ Duration.zero,
+ (previous, event) => previous + event.time.duration,
+ );
factory FramePhase.build({
required List<FlutterTimelineEvent> events,
diff --git a/packages/devtools_app/lib/src/screens/performance/panes/timeline_events/perfetto/_perfetto_web.dart b/packages/devtools_app/lib/src/screens/performance/panes/timeline_events/perfetto/_perfetto_web.dart
index 6cfcc20..2d5cafa 100644
--- a/packages/devtools_app/lib/src/screens/performance/panes/timeline_events/perfetto/_perfetto_web.dart
+++ b/packages/devtools_app/lib/src/screens/performance/panes/timeline_events/perfetto/_perfetto_web.dart
@@ -19,7 +19,6 @@
import '../../../../../shared/globals.dart';
import '../../../../../shared/primitives/utils.dart';
import '../../../../../shared/utils/utils.dart';
-import '../../../performance_utils.dart';
import '_perfetto_controller_web.dart';
import 'perfetto_controller.dart';
@@ -205,10 +204,6 @@
Future<void> _scrollToTimeRange(TimeRange? timeRange) async {
if (timeRange == null) return;
- if (!timeRange.isWellFormed) {
- pushNoTimelineEventsAvailableWarning();
- return;
- }
await _pingPerfettoUntilReady();
ga.select(
gac.performance,
@@ -217,8 +212,8 @@
_postMessage({
'perfetto': {
// Pass the values to Perfetto in seconds.
- 'timeStart': timeRange.start!.inMicroseconds / 1000000,
- 'timeEnd': timeRange.end!.inMicroseconds / 1000000,
+ 'timeStart': timeRange.start / 1000000,
+ 'timeEnd': timeRange.end / 1000000,
// The time range should take up 80% of the visible window.
'viewPercentage': 0.8,
},
diff --git a/packages/devtools_app/lib/src/screens/performance/panes/timeline_events/timeline_event_processor.dart b/packages/devtools_app/lib/src/screens/performance/panes/timeline_events/timeline_event_processor.dart
index a6b4e4c..8e9daae 100644
--- a/packages/devtools_app/lib/src/screens/performance/panes/timeline_events/timeline_event_processor.dart
+++ b/packages/devtools_app/lib/src/screens/performance/panes/timeline_events/timeline_event_processor.dart
@@ -116,8 +116,7 @@
// Since this event is complete, move back up the tree to the nearest
// incomplete event.
- while (current!.parent != null &&
- current.parent!.time.end?.inMicroseconds != null) {
+ while (current!.parent?.isComplete ?? false) {
current = current.parent;
}
currentTimelineEventsByTrackId[trackId] = current.parent;
diff --git a/packages/devtools_app/lib/src/screens/performance/panes/timeline_events/timeline_events_controller.dart b/packages/devtools_app/lib/src/screens/performance/panes/timeline_events/timeline_events_controller.dart
index 8c12811..5383078 100644
--- a/packages/devtools_app/lib/src/screens/performance/panes/timeline_events/timeline_events_controller.dart
+++ b/packages/devtools_app/lib/src/screens/performance/panes/timeline_events/timeline_events_controller.dart
@@ -436,9 +436,7 @@
);
}
- if (frame.timeFromFrameTiming.isWellFormed) {
- perfettoController.scrollToTimeRange(frame.timeFromFrameTiming);
- }
+ perfettoController.scrollToTimeRange(frame.timeFromFrameTiming);
}
void addTimelineEvent(FlutterTimelineEvent event) {
@@ -462,7 +460,7 @@
} else {
final unassignedEventsForFrame = _unassignedFlutterTimelineEvents
.putIfAbsent(frameNumber, () => FrameTimelineEventData());
- unassignedEventsForFrame.setEventFlow(event: event, setTimeData: false);
+ unassignedEventsForFrame.setEventFlow(event: event);
}
}
}
diff --git a/packages/devtools_app/lib/src/screens/performance/performance_model.dart b/packages/devtools_app/lib/src/screens/performance/performance_model.dart
index fbd4b4e..637eae3 100644
--- a/packages/devtools_app/lib/src/screens/performance/performance_model.dart
+++ b/packages/devtools_app/lib/src/screens/performance/performance_model.dart
@@ -98,11 +98,18 @@
}
class FlutterTimelineEvent extends TreeNode<FlutterTimelineEvent> {
- FlutterTimelineEvent(PerfettoTrackEvent firstTrackEvent)
- : trackEvents = [firstTrackEvent],
- type = firstTrackEvent.timelineEventType {
- time.start = Duration(microseconds: firstTrackEvent.timestampMicros);
- }
+ factory FlutterTimelineEvent(PerfettoTrackEvent firstTrackEvent) =>
+ FlutterTimelineEvent._(
+ trackEvents: [firstTrackEvent],
+ type: firstTrackEvent.timelineEventType,
+ timeBuilder: TimeRangeBuilder(start: firstTrackEvent.timestampMicros),
+ );
+
+ FlutterTimelineEvent._({
+ required this.trackEvents,
+ required this.type,
+ required TimeRangeBuilder timeBuilder,
+ }) : _timeBuilder = timeBuilder;
static const rasterEventName = 'Rasterizer::DoDraw';
static const uiEventName = 'Animator::BeginFrame';
@@ -110,9 +117,17 @@
/// Perfetto track events associated with this [FlutterTimelineEvent].
final List<PerfettoTrackEvent> trackEvents;
- TimelineEventType? type;
+ final TimelineEventType? type;
- TimeRange time = TimeRange();
+ final TimeRangeBuilder _timeBuilder;
+
+ /// The time range of this event.
+ ///
+ /// Throws if [isComplete] is false.
+ TimeRange get time => _timeBuilder.build();
+
+ /// Whether this event is complete and has received an end track event.
+ bool get isComplete => _timeBuilder.canBuild;
String? get name => trackEvents.first.name;
@@ -123,26 +138,17 @@
bool get isShaderEvent =>
trackEvents.first.isShaderEvent || trackEvents.last.isShaderEvent;
- bool get isWellFormed => time.start != null && time.end != null;
-
void addEndTrackEvent(PerfettoTrackEvent event) {
- time.end = Duration(microseconds: event.timestampMicros);
+ _timeBuilder.end = event.timestampMicros;
trackEvents.add(event);
}
@override
- FlutterTimelineEvent shallowCopy() {
- final copy = FlutterTimelineEvent(trackEvents.first);
- for (int i = 1; i < trackEvents.length; i++) {
- copy.trackEvents.add(trackEvents[i]);
- }
- copy
- ..type = type
- ..time = (TimeRange()
- ..start = time.start
- ..end = time.end);
- return copy;
- }
+ FlutterTimelineEvent shallowCopy() => FlutterTimelineEvent._(
+ trackEvents: trackEvents.toList(),
+ type: type,
+ timeBuilder: _timeBuilder.copy(),
+ );
@visibleForTesting
FlutterTimelineEvent deepCopy() {
@@ -174,7 +180,12 @@
}
void format(StringBuffer buf, String indent) {
- buf.writeln('$indent$name $time');
+ buf.write('$indent$name');
+ if (isComplete) {
+ buf.write(time);
+ }
+
+ buf.writeln(' ');
for (final child in children) {
child.format(buf, ' $indent');
}
diff --git a/packages/devtools_app/lib/src/screens/profiler/cpu_profile_model.dart b/packages/devtools_app/lib/src/screens/profiler/cpu_profile_model.dart
index 33413d7..f3c4297 100644
--- a/packages/devtools_app/lib/src/screens/profiler/cpu_profile_model.dart
+++ b/packages/devtools_app/lib/src/screens/profiler/cpu_profile_model.dart
@@ -196,11 +196,7 @@
samplePeriod: samplePeriod,
stackDepth: json.stackDepth ?? 0,
time: (timeOriginMicros != null && timeExtentMicros != null)
- ? (TimeRange()
- ..start = Duration(microseconds: timeOriginMicros)
- ..end = Duration(
- microseconds: timeOriginMicros + timeExtentMicros,
- ))
+ ? TimeRange.ofDuration(timeExtentMicros, start: timeOriginMicros)
: null,
);
@@ -246,11 +242,7 @@
// Each sample in [subSamples] will have the leaf stack
// frame id for a cpu sample within [subTimeRange].
final subSamples = superProfile.cpuSamples
- .where(
- (sample) => subTimeRange.contains(
- Duration(microseconds: sample.timestampMicros!),
- ),
- )
+ .where((sample) => subTimeRange.contains(sample.timestampMicros!))
.toList();
final subStackFrames = <String, CpuStackFrame>{};
@@ -399,13 +391,12 @@
// for this profile data, and the samples included in this data could be
// sparse over the original profile's time range, so true start and end
// times wouldn't be helpful.
- time: TimeRange()
- ..start = const Duration()
- ..end = Duration(
- microseconds: microsPerSample.isInfinite
- ? 0
- : (newSampleCount * microsPerSample).round(),
- ),
+ time: TimeRange(
+ start: 0,
+ end: microsPerSample.isInfinite
+ ? 0
+ : (newSampleCount * microsPerSample).round(),
+ ),
);
final stackFramesWithTag = <String, CpuStackFrame>{};
@@ -504,13 +495,12 @@
// for this profile data, and the samples included in this data could be
// sparse over the original profile's time range, so true start and end
// times wouldn't be helpful.
- time: TimeRange()
- ..start = const Duration()
- ..end = Duration(
- microseconds: microsPerSample.isInfinite || microsPerSample.isNaN
- ? 0
- : (filteredCpuSamples.length * microsPerSample).round(),
- ),
+ time: TimeRange(
+ start: 0,
+ end: microsPerSample.isInfinite || microsPerSample.isNaN
+ ? 0
+ : (filteredCpuSamples.length * microsPerSample).round(),
+ ),
);
void walkAndFilter(CpuStackFrame stackFrame) {
@@ -748,10 +738,8 @@
_samplePeriodKey: profileMetaData.samplePeriod,
_sampleCountKey: profileMetaData.sampleCount,
_stackDepthKey: profileMetaData.stackDepth,
- if (profileMetaData.time?.start case final startTime?)
- _timeOriginKey: startTime.inMicroseconds,
- if (profileMetaData.time?.duration case final duration?)
- _timeExtentKey: duration.inMicroseconds,
+ _timeOriginKey: ?profileMetaData.time?.start,
+ _timeExtentKey: ?profileMetaData.time?.duration.inMicroseconds,
_stackFramesKey: stackFramesJson,
_traceEventsKey: cpuSamples.map((sample) => sample.toJson).toList(),
};
@@ -1119,7 +1107,7 @@
return _profilesByLabel[label];
}
- if (!time!.isWellFormed) return null;
+ if (time == null) return null;
// If we have a profile for a time range encompassing [time], then we can
// generate and cache the profile for [time] without needing to pull data
diff --git a/packages/devtools_app/lib/src/shared/charts/flame_chart.dart b/packages/devtools_app/lib/src/shared/charts/flame_chart.dart
index e0eab38..37200e0 100644
--- a/packages/devtools_app/lib/src/shared/charts/flame_chart.dart
+++ b/packages/devtools_app/lib/src/shared/charts/flame_chart.dart
@@ -172,9 +172,7 @@
currentZoom /
startingPxPerMicro;
- return TimeRange()
- ..start = Duration(microseconds: startMicros.round())
- ..end = Duration(microseconds: endMicros.round());
+ return TimeRange(start: startMicros.round(), end: endMicros.round());
}
/// Starting pixels per microsecond in order to fit all the data in view at
@@ -182,7 +180,7 @@
double get startingPxPerMicro =>
widget.startingContentWidth / widget.time.duration.inMicroseconds;
- int get startTimeOffset => widget.time.start!.inMicroseconds;
+ int get startTimeOffset => widget.time.start;
double get maxZoomLevel {
// The max zoom level is hit when 1 microsecond is the width of each grid
diff --git a/packages/devtools_app/lib/src/shared/http/http_request_data.dart b/packages/devtools_app/lib/src/shared/http/http_request_data.dart
index 7b6c505..8be9458 100644
--- a/packages/devtools_app/lib/src/shared/http/http_request_data.dart
+++ b/packages/devtools_app/lib/src/shared/http/http_request_data.dart
@@ -30,10 +30,10 @@
DateTime get timestamp => _event.timestamp;
/// The amount of time since the last instant event completed.
- TimeRange? get timeRange => _timeRange;
+ TimeRange get timeRange => _timeRangeBuilder.build();
- // This is set from within HttpRequestData.
- TimeRange? _timeRange;
+ // This is modified from within HttpRequestData.
+ final TimeRangeBuilder _timeRangeBuilder = TimeRangeBuilder();
}
/// An abstraction of an HTTP request made through dart:io.
@@ -339,9 +339,9 @@
DateTime lastTime = _request.startTime;
for (final instant in instantEvents) {
final instantTime = instant.timestamp;
- instant._timeRange = TimeRange()
- ..start = Duration(microseconds: lastTime.microsecondsSinceEpoch)
- ..end = Duration(microseconds: instantTime.microsecondsSinceEpoch);
+ instant._timeRangeBuilder
+ ..start = lastTime.microsecondsSinceEpoch
+ ..end = instantTime.microsecondsSinceEpoch;
lastTime = instantTime;
}
}
diff --git a/packages/devtools_app/lib/src/shared/primitives/utils.dart b/packages/devtools_app/lib/src/shared/primitives/utils.dart
index b25709c..3da3540 100644
--- a/packages/devtools_app/lib/src/shared/primitives/utils.dart
+++ b/packages/devtools_app/lib/src/shared/primitives/utils.dart
@@ -442,84 +442,101 @@
}
}
+// If the need arises, this enum can be expanded to include any of the
+// remaining time units supported by [Duration] - (seconds, minutes, etc.).
/// Time unit for displaying time ranges.
-///
-/// If the need arises, this enum can be expanded to include any of the
-/// remaining time units supported by [Duration] - (seconds, minutes, etc.). If
-/// you add a unit of time to this enum, modify the toString() method in
-/// [TimeRange] to handle the new case.
enum TimeUnit { microseconds, milliseconds }
-class TimeRange {
- TimeRange({this.singleAssignment = true});
+/// A builder used to build a well-formed [TimeRange] incrementally.
+final class TimeRangeBuilder {
+ /// Creates a new [TimeRangeBuilder] to build a [TimeRange]
+ /// with the optionally specified [start] and [end] initial values.
+ TimeRangeBuilder({int? start, int? end}) : _start = start, _end = end;
- factory TimeRange.offset({
- required TimeRange original,
- required Duration offset,
- }) {
- final originalStart = original.start;
- final originalEnd = original.end;
- return TimeRange()
- ..start = originalStart != null ? originalStart + offset : null
- ..end = originalEnd != null ? originalEnd + offset : null;
+ int? _start;
+
+ /// Sets the start time of this builder.
+ ///
+ /// The start time should be less than or equal to the end time.
+ set start(int startTime) {
+ _start = startTime;
}
- final bool singleAssignment;
+ int? _end;
- Duration? get start => _start;
+ /// Sets the end time of this builder.
+ ///
+ /// The end time should be greater than or equal to the start time.
+ set end(int endTime) {
+ _end = endTime;
+ }
- Duration? _start;
+ /// Whether both `start` and `end` properties are set,
+ /// meaning [build] can safely be called.
+ bool get canBuild => _start != null && _end != null;
- set start(Duration? value) {
- if (singleAssignment) {
- assert(_start == null);
+ /// Returns a [TimeRange] built from the specified [start] and [end] values.
+ ///
+ /// If either [start] or [end] is `null`, throws an error.
+ ///
+ /// The [start] time must be less than or equal to the [end] time.
+ TimeRange build() {
+ final startTimestamp = _start;
+ if (startTimestamp == null) {
+ throw StateError('TimeRangeBuilder.start must be set before building!');
}
- if (value != null && _end != null) {
+
+ final endTimestamp = _end;
+ if (endTimestamp == null) {
+ throw StateError('TimeRangeBuilder.end must be set before building!');
+ }
+
+ return TimeRange(start: startTimestamp, end: endTimestamp);
+ }
+
+ /// Returns a new [TimeRangeBuilder] with the current values of this builder.
+ TimeRangeBuilder copy() => TimeRangeBuilder(start: _start, end: _end);
+}
+
+final class TimeRange {
+ /// Creates a [TimeRange] with the specified
+ /// [start] and [end] times in microseconds.
+ ///
+ /// The [start] time must be less than or equal to the [end] time.
+ TimeRange({required this.start, required this.end})
+ : assert(start <= end, '$start is not less than or equal to end time $end'),
assert(
- value <= _end!,
- '$value is not less than or equal to end time $_end',
+ end >= start,
+ '$end is not greater than or equal to start time $start',
);
- }
- _start = value;
- }
- Duration? get end => _end;
+ /// Creates a [TimeRange] with the specified [start] time in microseconds and
+ /// [end] calculated as being [duration] microseconds later.
+ factory TimeRange.ofDuration(int duration, {int start = 0}) =>
+ TimeRange(start: start, end: start + duration);
- Duration? _end;
+ /// The starting time in microseconds.
+ final int start;
- set end(Duration? value) {
- if (singleAssignment) {
- assert(_end == null);
- }
- if (value != null && _start != null) {
- assert(
- value >= _start!,
- '$value is not greater than or equal to start time $_start',
- );
- }
- _end = value;
- }
+ /// The ending time in microseconds.
+ final int end;
- Duration get duration => end! - start!;
+ /// The duration of time between the [start] and [end] microseconds.
+ Duration get duration => Duration(microseconds: end - start);
- bool contains(Duration target) => start! <= target && end! >= target;
+ /// Whether this time range contains the specified [target] microsecond.
+ bool contains(int target) => start <= target && end >= target;
- bool containsRange(TimeRange t) => start! <= t.start! && end! >= t.end!;
-
- bool overlaps(TimeRange t) => t.end! > start! && t.start! < end!;
-
- bool get isWellFormed => _start != null && _end != null;
+ /// Whether this time range completely contains the
+ /// specified [target] time range.
+ bool containsRange(TimeRange target) =>
+ start <= target.start && end >= target.end;
@override
- String toString({TimeUnit? unit}) {
- unit ??= TimeUnit.microseconds;
- switch (unit) {
- case TimeUnit.microseconds:
- return '[${_start?.inMicroseconds} μs - ${end?.inMicroseconds} μs]';
- case TimeUnit.milliseconds:
- return '[${_start?.inMilliseconds} ms - ${end?.inMilliseconds} ms]';
- }
- }
+ String toString({TimeUnit? unit}) => switch (unit ?? TimeUnit.microseconds) {
+ TimeUnit.microseconds => '[$start μs - $end μs]',
+ TimeUnit.milliseconds => '[${start ~/ 1000} ms - ${end ~/ 1000} ms]',
+ };
@override
bool operator ==(Object other) {
diff --git a/packages/devtools_app/test/screens/cpu_profiler/cpu_profile_model_test.dart b/packages/devtools_app/test/screens/cpu_profiler/cpu_profile_model_test.dart
index 18601f5..4f4759a 100644
--- a/packages/devtools_app/test/screens/cpu_profiler/cpu_profile_model_test.dart
+++ b/packages/devtools_app/test/screens/cpu_profiler/cpu_profile_model_test.dart
@@ -33,15 +33,12 @@
final cpuProfileEmptyData = CpuProfileData.fromJson(
cpuProfileResponseEmptyJson,
);
- expect(
- cpuProfileEmptyData.profileMetaData.time!.end!.inMilliseconds,
- 47377796,
- );
+ expect(cpuProfileEmptyData.profileMetaData.time!.end, 47377796685);
final filtered = CpuProfileData.filterFrom(
cpuProfileEmptyData,
(_) => true,
);
- expect(filtered.profileMetaData.time!.end!.inMilliseconds, 0);
+ expect(filtered.profileMetaData.time!.end, 0);
});
test('init from parse', () {
@@ -55,22 +52,14 @@
);
expect(cpuProfileData.profileMetaData.sampleCount, equals(8));
expect(cpuProfileData.profileMetaData.samplePeriod, equals(50));
- expect(
- cpuProfileData.profileMetaData.time!.start!.inMicroseconds,
- equals(47377796685),
- );
- expect(
- cpuProfileData.profileMetaData.time!.end!.inMicroseconds,
- equals(47377799685),
- );
+ expect(cpuProfileData.profileMetaData.time!.start, equals(47377796685));
+ expect(cpuProfileData.profileMetaData.time!.end, equals(47377799685));
});
test('subProfile', () {
final subProfile = CpuProfileData.subProfile(
cpuProfileData,
- TimeRange()
- ..start = const Duration(microseconds: 47377796685)
- ..end = const Duration(microseconds: 47377799063),
+ TimeRange(start: 47377796685, end: 47377799063),
);
expect(subProfile.stackFramesJson, equals(subProfileStackFrames));
diff --git a/packages/devtools_app/test/screens/cpu_profiler/cpu_profiler_test.dart b/packages/devtools_app/test/screens/cpu_profiler/cpu_profiler_test.dart
index 8185362..65ef96b 100644
--- a/packages/devtools_app/test/screens/cpu_profiler/cpu_profiler_test.dart
+++ b/packages/devtools_app/test/screens/cpu_profiler/cpu_profiler_test.dart
@@ -659,9 +659,7 @@
sampleCount: 100,
samplePeriod: 100,
stackDepth: 128,
- time: TimeRange()
- ..start = const Duration()
- ..end = const Duration(microseconds: 10000),
+ time: TimeRange.ofDuration(10000),
);
await tester.pumpWidget(wrap(CpuProfileStats(metadata: metadata)));
await tester.pumpAndSettle();
@@ -698,9 +696,7 @@
sampleCount: 100,
samplePeriod: 0,
stackDepth: 128,
- time: TimeRange()
- ..start = const Duration()
- ..end = const Duration(microseconds: 10000),
+ time: TimeRange.ofDuration(10000),
);
await tester.pumpWidget(wrap(CpuProfileStats(metadata: metadata)));
await tester.pumpAndSettle();
diff --git a/packages/devtools_app/test/screens/cpu_profiler/method_table/method_table_model_test.dart b/packages/devtools_app/test/screens/cpu_profiler/method_table/method_table_model_test.dart
index ebe54bd..5645be9 100644
--- a/packages/devtools_app/test/screens/cpu_profiler/method_table/method_table_model_test.dart
+++ b/packages/devtools_app/test/screens/cpu_profiler/method_table/method_table_model_test.dart
@@ -18,9 +18,10 @@
profileMetaData: ProfileMetaData(
sampleCount: 10,
samplePeriod: 250,
- time: TimeRange()
- ..start = Duration.zero
- ..end = const Duration(seconds: 1),
+ time: TimeRange(
+ start: 0,
+ end: const Duration(seconds: 1).inMicroseconds,
+ ),
),
stackFrameIds: {'1'},
);
@@ -33,9 +34,10 @@
profileMetaData: ProfileMetaData(
sampleCount: 10,
samplePeriod: 250,
- time: TimeRange()
- ..start = Duration.zero
- ..end = const Duration(seconds: 1),
+ time: TimeRange(
+ start: 0,
+ end: const Duration(seconds: 1).inMicroseconds,
+ ),
),
stackFrameIds: {'2'},
);
@@ -48,9 +50,10 @@
profileMetaData: ProfileMetaData(
sampleCount: 10,
samplePeriod: 250,
- time: TimeRange()
- ..start = Duration.zero
- ..end = const Duration(seconds: 1),
+ time: TimeRange(
+ start: 0,
+ end: const Duration(seconds: 1).inMicroseconds,
+ ),
),
stackFrameIds: {'3'},
);
@@ -63,9 +66,10 @@
profileMetaData: ProfileMetaData(
sampleCount: 10,
samplePeriod: 250,
- time: TimeRange()
- ..start = Duration.zero
- ..end = const Duration(seconds: 1),
+ time: TimeRange(
+ start: 0,
+ end: const Duration(seconds: 1).inMicroseconds,
+ ),
),
stackFrameIds: {'4'},
);
diff --git a/packages/devtools_app/test/shared/primitives/utils_test.dart b/packages/devtools_app/test/shared/primitives/utils_test.dart
index 267b22f..3fe14a5 100644
--- a/packages/devtools_app/test/shared/primitives/utils_test.dart
+++ b/packages/devtools_app/test/shared/primitives/utils_test.dart
@@ -252,13 +252,7 @@
group('TimeRange', () {
test('toString', () {
- final timeRange = TimeRange();
-
- expect(timeRange.toString(), equals('[null μs - null μs]'));
-
- timeRange
- ..start = const Duration(microseconds: 1000)
- ..end = const Duration(microseconds: 8000);
+ final timeRange = TimeRange(start: 1000, end: 8000);
expect(timeRange.duration.inMicroseconds, equals(7000));
expect(timeRange.toString(), equals('[1000 μs - 8000 μs]'));
@@ -268,53 +262,13 @@
);
});
- test('overlaps', () {
- final t = TimeRange()
- ..start = const Duration(milliseconds: 100)
- ..end = const Duration(milliseconds: 200);
- final overlapBeginning = TimeRange()
- ..start = const Duration(milliseconds: 50)
- ..end = const Duration(milliseconds: 150);
- final overlapMiddle = TimeRange()
- ..start = const Duration(milliseconds: 125)
- ..end = const Duration(milliseconds: 175);
- final overlapEnd = TimeRange()
- ..start = const Duration(milliseconds: 150)
- ..end = const Duration(milliseconds: 250);
- final overlapAll = TimeRange()
- ..start = const Duration(milliseconds: 50)
- ..end = const Duration(milliseconds: 250);
- final noOverlap = TimeRange()
- ..start = const Duration(milliseconds: 300)
- ..end = const Duration(milliseconds: 400);
-
- expect(t.overlaps(t), isTrue);
- expect(t.overlaps(overlapBeginning), isTrue);
- expect(t.overlaps(overlapMiddle), isTrue);
- expect(t.overlaps(overlapEnd), isTrue);
- expect(t.overlaps(overlapAll), isTrue);
- expect(t.overlaps(noOverlap), isFalse);
- });
-
test('containsRange', () {
- final t = TimeRange()
- ..start = const Duration(milliseconds: 100)
- ..end = const Duration(milliseconds: 200);
- final containsStart = TimeRange()
- ..start = const Duration(milliseconds: 50)
- ..end = const Duration(milliseconds: 150);
- final containsStartAndEnd = TimeRange()
- ..start = const Duration(milliseconds: 125)
- ..end = const Duration(milliseconds: 175);
- final containsEnd = TimeRange()
- ..start = const Duration(milliseconds: 150)
- ..end = const Duration(milliseconds: 250);
- final invertedContains = TimeRange()
- ..start = const Duration(milliseconds: 50)
- ..end = const Duration(milliseconds: 250);
- final containsNeither = TimeRange()
- ..start = const Duration(milliseconds: 300)
- ..end = const Duration(milliseconds: 400);
+ final t = TimeRange(start: 100, end: 200);
+ final containsStart = TimeRange(start: 50, end: 150);
+ final containsStartAndEnd = TimeRange(start: 125, end: 175);
+ final containsEnd = TimeRange(start: 150, end: 250);
+ final invertedContains = TimeRange(start: 50, end: 250);
+ final containsNeither = TimeRange(start: 300, end: 400);
expect(t.containsRange(containsStart), isFalse);
expect(t.containsRange(containsStartAndEnd), isTrue);
@@ -323,90 +277,59 @@
expect(t.containsRange(containsNeither), isFalse);
});
- test('start setter throws exception when single assignment is true', () {
+ test('throws exception when start is after end', () {
expect(() {
- final t = TimeRange()..start = Duration.zero;
- t.start = Duration.zero;
+ TimeRange(start: 2000, end: 1000);
}, throwsAssertionError);
});
+ });
- test('start setter throws exception when value is after end', () {
- expect(() {
- final t = TimeRange()..end = const Duration(seconds: 1);
- t.start = const Duration(seconds: 2);
- }, throwsAssertionError);
- });
-
- test('end setter throws exception when single assignment is true', () {
- expect(() {
- final t = TimeRange()..end = Duration.zero;
- t.end = Duration.zero;
- }, throwsAssertionError);
- });
-
- test('end setter throws exception when value is before start', () {
- expect(() {
- final t = TimeRange()..start = const Duration(seconds: 1);
- t.end = Duration.zero;
- }, throwsAssertionError);
- });
-
- test('isWellFormed', () {
+ group('TimeRangeBuilder', () {
+ test('throws if start or end are not set', () {
+ expect(() => TimeRangeBuilder().build(), throwsStateError);
+ expect(() => TimeRangeBuilder(start: 1000).build(), throwsStateError);
+ expect(() => TimeRangeBuilder(end: 1000).build(), throwsStateError);
expect(
- (TimeRange()
- ..start = Duration.zero
- ..end = Duration.zero)
- .isWellFormed,
- isTrue,
+ () => (TimeRangeBuilder()..start = 1000).build(),
+ throwsStateError,
);
- expect((TimeRange()..end = Duration.zero).isWellFormed, isFalse);
- expect((TimeRange()..start = Duration.zero).isWellFormed, isFalse);
+ expect(
+ () => (TimeRangeBuilder()..end = 1000).build(),
+ throwsStateError,
+ );
});
- group('offset', () {
- test('from well formed time range', () {
- final t = TimeRange()
- ..start = const Duration(milliseconds: 100)
- ..end = const Duration(milliseconds: 200);
- final offset = TimeRange.offset(
- original: t,
- offset: const Duration(milliseconds: 300),
- );
+ test('throws error if start is after end', () {
+ expect(
+ () => TimeRangeBuilder(start: 2000, end: 1000).build(),
+ throwsAssertionError,
+ );
+ });
- expect(offset.start, equals(const Duration(milliseconds: 400)));
- expect(offset.end, equals(const Duration(milliseconds: 500)));
- });
-
- test('from half formed time range', () {
- var t = TimeRange()..start = const Duration(milliseconds: 100);
- var offset = TimeRange.offset(
- original: t,
- offset: const Duration(milliseconds: 300),
- );
-
- expect(offset.start, equals(const Duration(milliseconds: 400)));
- expect(offset.end, isNull);
-
- t = TimeRange()..end = const Duration(milliseconds: 200);
- offset = TimeRange.offset(
- original: t,
- offset: const Duration(milliseconds: 300),
- );
-
- expect(offset.start, isNull);
- expect(offset.end, equals(const Duration(milliseconds: 500)));
- });
-
- test('from empty time range', () {
- final t = TimeRange();
- final offset = TimeRange.offset(
- original: t,
- offset: const Duration(milliseconds: 300),
- );
-
- expect(offset.start, isNull);
- expect(offset.end, isNull);
- });
+ test('builds expected TimeRange', () {
+ expect(
+ TimeRangeBuilder(start: 1000, end: 2000).build(),
+ TimeRange(start: 1000, end: 2000),
+ );
+ expect(
+ TimeRangeBuilder(start: 1000, end: 1000).build(),
+ TimeRange(start: 1000, end: 1000),
+ );
+ expect(
+ (TimeRangeBuilder(start: 150)..end = 250).build(),
+ TimeRange(start: 150, end: 250),
+ );
+ expect(
+ (TimeRangeBuilder(end: 400)..start = 300).build(),
+ TimeRange(start: 300, end: 400),
+ );
+ expect(
+ (TimeRangeBuilder()
+ ..start = 0
+ ..end = 100)
+ .build(),
+ TimeRange.ofDuration(100),
+ );
});
});
diff --git a/packages/devtools_app/test/test_infra/test_data/cpu_profiler/cpu_profile.dart b/packages/devtools_app/test/test_infra/test_data/cpu_profiler/cpu_profile.dart
index df193d7..7e13acf 100644
--- a/packages/devtools_app/test/test_infra/test_data/cpu_profiler/cpu_profile.dart
+++ b/packages/devtools_app/test/test_infra/test_data/cpu_profiler/cpu_profile.dart
@@ -1211,11 +1211,12 @@
sampleCount: 10,
samplePeriod: 1000,
stackDepth: 128,
- time: TimeRange()
- ..start = const Duration()
+ time: TimeRange(
+ start: 0,
// Note this intentionally adds 10000 microseconds more than what
// was measured, regression test for Issue #8870.
- ..end = const Duration(microseconds: 20000),
+ end: 20000,
+ ),
);
final tagFrameA = CpuStackFrame(
@@ -1529,9 +1530,7 @@
sampleCount: 0,
samplePeriod: 50,
stackDepth: 128,
- time: TimeRange()
- ..start = const Duration()
- ..end = const Duration(microseconds: 100),
+ time: TimeRange.ofDuration(100),
);
final zeroStackFrame = CpuStackFrame(