New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Make Flutter Frame timeline live and migrate to the FrameTiming API #3168
Conversation
packages/devtools_app/lib/src/performance/performance_controller.dart
Outdated
Show resolved
Hide resolved
@@ -356,14 +517,37 @@ class PerformanceController | |||
|
|||
void addTimelineEvent(TimelineEvent event) { | |||
data.addTimelineEvent(event); | |||
_timelineEvents.value.add(event); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Adding elements to a ValueListenable without notifying isn't safe. Can you use ListValueNotifier or change the _timelineEvents so they are not a ValueListenable?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
turns out we weren't even using the _timelineEvents notifier in the UI. removed it completely.
/// These timeline events are keyed by the [FlutterFrame] ID specified in the | ||
/// event arguments, which matches the ID for the corresponding | ||
/// [FlutterFrame]. | ||
final unassignedFlutterFrameEvents = <int, List<TimelineEvent>>{}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe replace List with a class with a clearer api.
Having the indexes in the List happen to be FlutterFrame ids is a little surprising.
class could be UnasignedEvents
or similar.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
added a class FrameTimelineEventData to store the timeline event data for a flutter frame.
autoDispose(serviceManager.service.onTimelineEvent.listen((event) { | ||
final eventBatch = <TraceEventWrapper>[]; | ||
if (event.json['kind'] == 'TimelineEvents') { | ||
final List<dynamic> traceEvents = event.json['timelineEvents'] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
why is this a List?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is how events are streamed over service.onTimelineEvent
), | ||
) | ||
.toList(); | ||
final List<TraceEventWrapper> wrappedTraceEvents = |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
avoid types on LHS when not needed.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
removed
} | ||
|
||
FutureOr<void> processAvailableEvents() async { | ||
_processing.value = true; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: add
assert(!_processing.value) at the start of the method to make sure we don't get into a state where we have two pending processing requests.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
done
// Only try to pull timeline events for frames that are after the first | ||
// well formed frame. Timeline events that occurred before this frame will | ||
// have already fallen out of the buffer. | ||
await processAvailableEvents(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Are we concerned this will hide timeline events on startup that occur before the first flutter frame (assuming such events can exist)?
We could always add an artificial "before startup" frame to disambiguate this case from the case you are probably trying to avoid of incomplete frames due to full buffers.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The timeline events that occur before the first frame will still be present in the timeline flame chart. This check is to prevent us from trying to process new events for frames that we know occurred before the first frame-related timeline events that we have data for. This code is only triggered when a frame is selected, so it is not responsible for the initial load of timeline events.
final unassignedEventsForFrame = | ||
unassignedFlutterFrameEvents.putIfAbsent( | ||
frameNumber, | ||
() => List.generate(2, (_) => null), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is a case where having an UnasignedEvents class would make it clearer.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
added a class FrameTimelineEventData
to store the timeline event data for a flutter frame.
} | ||
} | ||
} | ||
|
||
void toggleRecordingFrames(bool recording) { | ||
_recordingFrames.value = recording; | ||
if (_recordingFrames.value) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
tiny nit: maybe condition on recording
instead of the value of _recordingFrames.value
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
done
@@ -284,7 +292,15 @@ class OfflinePerformanceData extends PerformanceData { | |||
final CpuProfileData cpuProfileData = | |||
cpuProfileJson.isNotEmpty ? CpuProfileData.parse(cpuProfileJson) : null; | |||
|
|||
final String selectedFrameId = json[PerformanceData.selectedFrameIdKey]; | |||
final int selectedFrameId = json[PerformanceData.selectedFrameIdKey]; | |||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: avoid dynamic where possible preferring Object so we get better static errors.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
done
Notifications.of(context) | ||
.push('No timeline events available for the selected frame'); | ||
return; | ||
} else if (_selectedFrame.uiEventFlow == null) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
should these checks be added to a helper on _slectedFrame object? get the current time and event?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
added helpers as extension methods since these really have more to do with the flame chart than the FlutterFrame class
} else if (_selectedFrame.uiEventFlow == null) { | ||
time = _selectedFrame.rasterEventFlow.time; | ||
event = _selectedFrame.rasterEventFlow; | ||
} else if (_selectedFrame.rasterEventFlow == null) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also document why these checks are the way they are. I would have expected the check to be that
uiEventFlow != null
rather than checking that rasterEventFlow == null
as the current way would lead to a NPE if both were null for a frame but perhaps that isn't possible.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
if both were null for a frame, we would have already returned above. I'll switch to use the != null check though as that may make it more clear.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
addressed review comments - working on adding tests
@@ -284,7 +292,15 @@ class OfflinePerformanceData extends PerformanceData { | |||
final CpuProfileData cpuProfileData = | |||
cpuProfileJson.isNotEmpty ? CpuProfileData.parse(cpuProfileJson) : null; | |||
|
|||
final String selectedFrameId = json[PerformanceData.selectedFrameIdKey]; | |||
final int selectedFrameId = json[PerformanceData.selectedFrameIdKey]; | |||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
done
} else if (_selectedFrame.uiEventFlow == null) { | ||
time = _selectedFrame.rasterEventFlow.time; | ||
event = _selectedFrame.rasterEventFlow; | ||
} else if (_selectedFrame.rasterEventFlow == null) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
if both were null for a frame, we would have already returned above. I'll switch to use the != null check though as that may make it more clear.
Notifications.of(context) | ||
.push('No timeline events available for the selected frame'); | ||
return; | ||
} else if (_selectedFrame.uiEventFlow == null) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
added helpers as extension methods since these really have more to do with the flame chart than the FlutterFrame class
final unassignedEventsForFrame = | ||
unassignedFlutterFrameEvents.putIfAbsent( | ||
frameNumber, | ||
() => List.generate(2, (_) => null), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
added a class FrameTimelineEventData
to store the timeline event data for a flutter frame.
/// These timeline events are keyed by the [FlutterFrame] ID specified in the | ||
/// event arguments, which matches the ID for the corresponding | ||
/// [FlutterFrame]. | ||
final unassignedFlutterFrameEvents = <int, List<TimelineEvent>>{}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
added a class FrameTimelineEventData to store the timeline event data for a flutter frame.
), | ||
) | ||
.toList(); | ||
final List<TraceEventWrapper> wrappedTraceEvents = |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
removed
} | ||
|
||
FutureOr<void> processAvailableEvents() async { | ||
_processing.value = true; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
done
} | ||
} | ||
} | ||
|
||
void toggleRecordingFrames(bool recording) { | ||
_recordingFrames.value = recording; | ||
if (_recordingFrames.value) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
done
@@ -356,14 +517,37 @@ class PerformanceController | |||
|
|||
void addTimelineEvent(TimelineEvent event) { | |||
data.addTimelineEvent(event); | |||
_timelineEvents.value.add(event); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
turns out we weren't even using the _timelineEvents notifier in the UI. removed it completely.
// Only try to pull timeline events for frames that are after the first | ||
// well formed frame. Timeline events that occurred before this frame will | ||
// have already fallen out of the buffer. | ||
await processAvailableEvents(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The timeline events that occur before the first frame will still be present in the timeline flame chart. This check is to prevent us from trying to process new events for frames that we know occurred before the first frame-related timeline events that we have data for. This code is only triggered when a frame is selected, so it is not responsible for the initial load of timeline events.
Future<void> _pullTraceEventsFromVmTimeline({ | ||
bool shouldPrimeThreadIds = false, | ||
}) async { | ||
final currentVmTime = await serviceManager.service.getVMTimelineMicros(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
need to make sure we don't have multiple simultaneous timeline event pulls at once for the case a pull took more than 2 seconds.
Use the RateLimiter class and perhaps a delay of only 200 ms to achieve that goal.
As you are now guaranteed to only have one pull at a time it would be safe to pull more frequently.
class RateLimiter { |
DateTime.now().millisecondsSinceEpoch, | ||
); | ||
allTraceEvents.add(eventWrapper); | ||
debugTraceEventLog(eventWrapper.event.json.toString()); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you remove this debugTraceEventLog or pass the raw json object instead of calling toString? This code will call the toString on the JSON even in release mode which could be somewhat expensive.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
changing this method to be a callback so that the toString() won't get called unless the callback is called
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@@ -208,15 +210,17 @@ class PerformanceController extends DisposableController | |||
await processTraceEvents(allTraceEvents); | |||
_processing.value = false; | |||
|
|||
_timelinePollingRateLimiter = RateLimiter( | |||
timelinePollingRateLimit, | |||
() async => await _pullTraceEventsFromVmTimeline(), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
replace:
() async => await _pullTraceEventsFromVmTimeline()
with
_pullTraceEventFromVmTimeline
@pq is there a lint tracking this case?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
done
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hrmmm, closest thing would be unnecessary_lambdas
.
Fixes #2918
Tests are WIP - will work on those while this change is under review.
Apologies for the messy commit history - avoiding a nasty rebase.