Skip to content

Commit 119684c

Browse files
authored
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.
1 parent 14543db commit 119684c

File tree

17 files changed

+246
-373
lines changed

17 files changed

+246
-373
lines changed

packages/devtools_app/lib/src/screens/network/network_request_inspector_views.dart

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -696,7 +696,7 @@ class NetworkRequestOverviewView extends StatelessWidget {
696696
// flex so that we can set a minimum width for small timing chunks.
697697
final timingWidgets = <Widget>[];
698698
for (final instant in data.instantEvents) {
699-
final duration = instant.timeRange!.duration;
699+
final duration = instant.timeRange.duration;
700700
timingWidgets.add(_buildTimingRow(nextColor(), instant.name, duration));
701701
}
702702
final duration = Duration(
@@ -714,14 +714,14 @@ class NetworkRequestOverviewView extends StatelessWidget {
714714
final data = this.data as DartIOHttpRequestData;
715715
final result = <Widget>[];
716716
for (final instant in data.instantEvents) {
717-
final instantEventStart = data.instantEvents.first.timeRange!.start!;
718-
final timeRange = instant.timeRange!;
717+
final instantEventStart = data.instantEvents.first.timeRange.start;
718+
final timeRange = instant.timeRange;
719719
final startDisplay = durationText(
720-
timeRange.start! - instantEventStart,
720+
Duration(microseconds: timeRange.start - instantEventStart),
721721
unit: DurationDisplayUnit.milliseconds,
722722
);
723723
final endDisplay = durationText(
724-
timeRange.end! - instantEventStart,
724+
Duration(microseconds: timeRange.end - instantEventStart),
725725
unit: DurationDisplayUnit.milliseconds,
726726
);
727727
final totalDisplay = durationText(

packages/devtools_app/lib/src/screens/performance/panes/flutter_frames/flutter_frame_model.dart

Lines changed: 19 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,6 @@
22
// Use of this source code is governed by a BSD-style license that can be
33
// found in the LICENSE file or at https://developers.google.com/open-source/licenses/bsd.
44

5-
import 'dart:math' as math;
6-
75
import '../../../../shared/primitives/utils.dart';
86
import '../../performance_model.dart';
97
import '../controls/enhance_tracing/enhance_tracing_model.dart';
@@ -24,12 +22,10 @@ class FlutterFrame {
2422
});
2523

2624
factory FlutterFrame.fromJson(Map<String, Object?> json) {
27-
final timeStart = Duration(microseconds: json[startTimeKey]! as int);
28-
final timeEnd =
29-
timeStart + Duration(microseconds: json[elapsedKey]! as int);
30-
final frameTime = TimeRange()
31-
..start = timeStart
32-
..end = timeEnd;
25+
final frameTime = TimeRange.ofDuration(
26+
json[elapsedKey]! as int,
27+
start: json[startTimeKey]! as int,
28+
);
3329
return FlutterFrame._(
3430
id: json[numberKey]! as int,
3531
timeFromFrameTiming: frameTime,
@@ -58,14 +54,6 @@ class FlutterFrame {
5854
/// which the data was parsed.
5955
final TimeRange timeFromFrameTiming;
6056

61-
/// The time range of the Flutter frame based on the frame's
62-
/// [timelineEventData], which contains timing information from the VM's
63-
/// timeline events.
64-
///
65-
/// This time range should be used for activities related to timeline events,
66-
/// like scrolling a frame's timeline events into view, for example.
67-
TimeRange get timeFromEventFlows => timelineEventData.time;
68-
6957
/// Build time for this Flutter frame based on data from the FrameTiming API
7058
/// sent over the extension stream as 'Flutter.Frame' events.
7159
final Duration buildTime;
@@ -85,13 +73,12 @@ class FlutterFrame {
8573
/// (e.g. when the 'Flutter.Frame' event for this frame was received).
8674
///
8775
/// If we did not have [EnhanceTracingState] information at the time that this
88-
/// frame was drawn (e.g. the DevTools performancd page was not opened and
76+
/// frame was drawn (e.g. the DevTools performance page was not opened and
8977
/// listening for frames yet), this value will be null.
9078
EnhanceTracingState? enhanceTracingState;
9179

9280
FrameAnalysis? get frameAnalysis {
93-
final frameAnalysis_ = _frameAnalysis;
94-
if (frameAnalysis_ != null) return frameAnalysis_;
81+
if (_frameAnalysis case final frameAnalysis?) return frameAnalysis;
9582
if (timelineEventData.isNotEmpty) {
9683
return _frameAnalysis = FrameAnalysis(this);
9784
}
@@ -104,15 +91,17 @@ class FlutterFrame {
10491

10592
Duration get shaderDuration {
10693
if (_shaderTime != null) return _shaderTime!;
107-
if (timelineEventData.rasterEvent == null) return Duration.zero;
108-
final shaderEvents = timelineEventData.rasterEvent!
109-
.shallowNodesWithCondition((event) => event.isShaderEvent);
110-
final duration = shaderEvents.fold<Duration>(Duration.zero, (
111-
previous,
112-
event,
113-
) {
114-
return previous + event.time.duration;
115-
});
94+
final rasterEvent = timelineEventData.rasterEvent;
95+
if (rasterEvent == null) return Duration.zero;
96+
final shaderEvents = rasterEvent.shallowNodesWithCondition(
97+
(event) => event.isShaderEvent,
98+
);
99+
final duration = shaderEvents
100+
.where((event) => event.isComplete)
101+
.fold<Duration>(
102+
Duration.zero,
103+
(previous, event) => previous + event.time.duration,
104+
);
116105
return _shaderTime = duration;
117106
}
118107

@@ -150,7 +139,7 @@ class FlutterFrame {
150139

151140
Map<String, Object?> get json => {
152141
numberKey: id,
153-
startTimeKey: timeFromFrameTiming.start!.inMicroseconds,
142+
startTimeKey: timeFromFrameTiming.start,
154143
elapsedKey: timeFromFrameTiming.duration.inMicroseconds,
155144
buildKey: buildTime.inMicroseconds,
156145
rasterKey: rasterTime.inMicroseconds,
@@ -197,42 +186,9 @@ class FrameTimelineEventData {
197186

198187
bool get isNotEmpty => uiEvent != null || rasterEvent != null;
199188

200-
final time = TimeRange();
201-
202-
void setEventFlow({
203-
required FlutterTimelineEvent event,
204-
bool setTimeData = true,
205-
}) {
189+
void setEventFlow({required FlutterTimelineEvent event}) {
206190
final type = event.type!;
207191
_eventFlows[type.index] = event;
208-
if (setTimeData) {
209-
if (type == TimelineEventType.ui) {
210-
time.start = event.time.start;
211-
// If [rasterEventFlow] has already completed, set the end time for this
212-
// frame to [event]'s end time.
213-
if (rasterEvent != null) {
214-
time.end = event.time.end;
215-
}
216-
} else if (type == TimelineEventType.raster) {
217-
// If [uiEventFlow] is null, that means that this raster event flow
218-
// completed before the ui event flow did for this frame. This means one
219-
// of two things: 1) there will never be a [uiEventFlow] for this frame
220-
// because the UI events are not present in the available timeline
221-
// events, or 2) the [uiEventFlow] has started but not completed yet. In
222-
// the event that 2) is true, do not set the frame end time here because
223-
// the end time for this frame will be set to the end time for
224-
// [uiEventFlow] once it finishes.
225-
final theUiEvent = uiEvent;
226-
if (theUiEvent != null) {
227-
time.end = Duration(
228-
microseconds: math.max(
229-
theUiEvent.time.end!.inMicroseconds,
230-
event.time.end?.inMicroseconds ?? 0,
231-
),
232-
);
233-
}
234-
}
235-
}
236192
}
237193

238194
FlutterTimelineEvent? eventByType(TimelineEventType type) {

packages/devtools_app/lib/src/screens/performance/panes/flutter_frames/flutter_frames_controller.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -165,7 +165,7 @@ class FlutterFramesController extends PerformanceFeatureController {
165165
assert(frame.isWellFormed);
166166
firstWellFormedFrameMicros = math.min(
167167
firstWellFormedFrameMicros ?? maxJsInt,
168-
frame.timeFromFrameTiming.start!.inMicroseconds,
168+
frame.timeFromFrameTiming.start,
169169
);
170170
}
171171

packages/devtools_app/lib/src/screens/performance/panes/frame_analysis/frame_analysis_model.dart

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -101,10 +101,7 @@ class FrameAnalysis {
101101
///
102102
/// This is drawn from all events for this frame from the raster thread.
103103
late FramePhase rasterPhase = FramePhase.raster(
104-
events: [
105-
if (frame.timelineEventData.rasterEvent != null)
106-
frame.timelineEventData.rasterEvent!,
107-
],
104+
events: [?frame.timelineEventData.rasterEvent],
108105
);
109106

110107
late FramePhase longestUiPhase = _calculateLongestFramePhase();
@@ -276,9 +273,12 @@ class FramePhase {
276273
: title = type.display,
277274
duration =
278275
duration ??
279-
events.fold<Duration>(Duration.zero, (previous, event) {
280-
return previous + event.time.duration;
281-
});
276+
events
277+
.where((event) => event.isComplete)
278+
.fold<Duration>(
279+
Duration.zero,
280+
(previous, event) => previous + event.time.duration,
281+
);
282282

283283
factory FramePhase.build({
284284
required List<FlutterTimelineEvent> events,

packages/devtools_app/lib/src/screens/performance/panes/timeline_events/perfetto/_perfetto_web.dart

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@ import '../../../../../shared/analytics/constants.dart' as gac;
1919
import '../../../../../shared/globals.dart';
2020
import '../../../../../shared/primitives/utils.dart';
2121
import '../../../../../shared/utils/utils.dart';
22-
import '../../../performance_utils.dart';
2322
import '_perfetto_controller_web.dart';
2423
import 'perfetto_controller.dart';
2524

@@ -205,10 +204,6 @@ class _PerfettoViewController extends DisposableController
205204
Future<void> _scrollToTimeRange(TimeRange? timeRange) async {
206205
if (timeRange == null) return;
207206

208-
if (!timeRange.isWellFormed) {
209-
pushNoTimelineEventsAvailableWarning();
210-
return;
211-
}
212207
await _pingPerfettoUntilReady();
213208
ga.select(
214209
gac.performance,
@@ -217,8 +212,8 @@ class _PerfettoViewController extends DisposableController
217212
_postMessage({
218213
'perfetto': {
219214
// Pass the values to Perfetto in seconds.
220-
'timeStart': timeRange.start!.inMicroseconds / 1000000,
221-
'timeEnd': timeRange.end!.inMicroseconds / 1000000,
215+
'timeStart': timeRange.start / 1000000,
216+
'timeEnd': timeRange.end / 1000000,
222217
// The time range should take up 80% of the visible window.
223218
'viewPercentage': 0.8,
224219
},

packages/devtools_app/lib/src/screens/performance/panes/timeline_events/timeline_event_processor.dart

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -116,8 +116,7 @@ class FlutterTimelineEventProcessor {
116116

117117
// Since this event is complete, move back up the tree to the nearest
118118
// incomplete event.
119-
while (current!.parent != null &&
120-
current.parent!.time.end?.inMicroseconds != null) {
119+
while (current!.parent?.isComplete ?? false) {
121120
current = current.parent;
122121
}
123122
currentTimelineEventsByTrackId[trackId] = current.parent;

packages/devtools_app/lib/src/screens/performance/panes/timeline_events/timeline_events_controller.dart

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -436,9 +436,7 @@ class TimelineEventsController extends PerformanceFeatureController
436436
);
437437
}
438438

439-
if (frame.timeFromFrameTiming.isWellFormed) {
440-
perfettoController.scrollToTimeRange(frame.timeFromFrameTiming);
441-
}
439+
perfettoController.scrollToTimeRange(frame.timeFromFrameTiming);
442440
}
443441

444442
void addTimelineEvent(FlutterTimelineEvent event) {
@@ -462,7 +460,7 @@ class TimelineEventsController extends PerformanceFeatureController
462460
} else {
463461
final unassignedEventsForFrame = _unassignedFlutterTimelineEvents
464462
.putIfAbsent(frameNumber, () => FrameTimelineEventData());
465-
unassignedEventsForFrame.setEventFlow(event: event, setTimeData: false);
463+
unassignedEventsForFrame.setEventFlow(event: event);
466464
}
467465
}
468466
}

packages/devtools_app/lib/src/screens/performance/performance_model.dart

Lines changed: 34 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -98,21 +98,36 @@ extension type _PerformanceDataJson(Map<String, Object?> json) {
9898
}
9999

100100
class FlutterTimelineEvent extends TreeNode<FlutterTimelineEvent> {
101-
FlutterTimelineEvent(PerfettoTrackEvent firstTrackEvent)
102-
: trackEvents = [firstTrackEvent],
103-
type = firstTrackEvent.timelineEventType {
104-
time.start = Duration(microseconds: firstTrackEvent.timestampMicros);
105-
}
101+
factory FlutterTimelineEvent(PerfettoTrackEvent firstTrackEvent) =>
102+
FlutterTimelineEvent._(
103+
trackEvents: [firstTrackEvent],
104+
type: firstTrackEvent.timelineEventType,
105+
timeBuilder: TimeRangeBuilder(start: firstTrackEvent.timestampMicros),
106+
);
107+
108+
FlutterTimelineEvent._({
109+
required this.trackEvents,
110+
required this.type,
111+
required TimeRangeBuilder timeBuilder,
112+
}) : _timeBuilder = timeBuilder;
106113

107114
static const rasterEventName = 'Rasterizer::DoDraw';
108115
static const uiEventName = 'Animator::BeginFrame';
109116

110117
/// Perfetto track events associated with this [FlutterTimelineEvent].
111118
final List<PerfettoTrackEvent> trackEvents;
112119

113-
TimelineEventType? type;
120+
final TimelineEventType? type;
121+
122+
final TimeRangeBuilder _timeBuilder;
123+
124+
/// The time range of this event.
125+
///
126+
/// Throws if [isComplete] is false.
127+
TimeRange get time => _timeBuilder.build();
114128

115-
TimeRange time = TimeRange();
129+
/// Whether this event is complete and has received an end track event.
130+
bool get isComplete => _timeBuilder.canBuild;
116131

117132
String? get name => trackEvents.first.name;
118133

@@ -123,26 +138,17 @@ class FlutterTimelineEvent extends TreeNode<FlutterTimelineEvent> {
123138
bool get isShaderEvent =>
124139
trackEvents.first.isShaderEvent || trackEvents.last.isShaderEvent;
125140

126-
bool get isWellFormed => time.start != null && time.end != null;
127-
128141
void addEndTrackEvent(PerfettoTrackEvent event) {
129-
time.end = Duration(microseconds: event.timestampMicros);
142+
_timeBuilder.end = event.timestampMicros;
130143
trackEvents.add(event);
131144
}
132145

133146
@override
134-
FlutterTimelineEvent shallowCopy() {
135-
final copy = FlutterTimelineEvent(trackEvents.first);
136-
for (int i = 1; i < trackEvents.length; i++) {
137-
copy.trackEvents.add(trackEvents[i]);
138-
}
139-
copy
140-
..type = type
141-
..time = (TimeRange()
142-
..start = time.start
143-
..end = time.end);
144-
return copy;
145-
}
147+
FlutterTimelineEvent shallowCopy() => FlutterTimelineEvent._(
148+
trackEvents: trackEvents.toList(),
149+
type: type,
150+
timeBuilder: _timeBuilder.copy(),
151+
);
146152

147153
@visibleForTesting
148154
FlutterTimelineEvent deepCopy() {
@@ -174,7 +180,12 @@ class FlutterTimelineEvent extends TreeNode<FlutterTimelineEvent> {
174180
}
175181

176182
void format(StringBuffer buf, String indent) {
177-
buf.writeln('$indent$name $time');
183+
buf.write('$indent$name');
184+
if (isComplete) {
185+
buf.write(time);
186+
}
187+
188+
buf.writeln(' ');
178189
for (final child in children) {
179190
child.format(buf, ' $indent');
180191
}

0 commit comments

Comments
 (0)