Skip to content
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

Merged
merged 40 commits into from Jul 16, 2021

Conversation

kenzieschmoll
Copy link
Member

@kenzieschmoll kenzieschmoll commented Jun 28, 2021

live-timeline

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.

@@ -356,14 +517,37 @@ class PerformanceController

void addTimelineEvent(TimelineEvent event) {
data.addTimelineEvent(event);
_timelineEvents.value.add(event);
Copy link
Contributor

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?

Copy link
Member Author

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>>{};
Copy link
Contributor

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.

Copy link
Member Author

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']
Copy link
Contributor

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?

Copy link
Member Author

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 =
Copy link
Contributor

@jacob314 jacob314 Jun 29, 2021

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.

Copy link
Member Author

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;
Copy link
Contributor

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.

Copy link
Member Author

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();
Copy link
Contributor

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.

Copy link
Member Author

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),
Copy link
Contributor

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.

Copy link
Member Author

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) {
Copy link
Contributor

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

Copy link
Member Author

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];

Copy link
Contributor

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.

Copy link
Member Author

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) {
Copy link
Contributor

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?

Copy link
Member Author

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) {
Copy link
Contributor

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.

Copy link
Member Author

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.

Copy link
Member Author

@kenzieschmoll kenzieschmoll left a 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];

Copy link
Member Author

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) {
Copy link
Member Author

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) {
Copy link
Member Author

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),
Copy link
Member Author

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>>{};
Copy link
Member Author

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 =
Copy link
Member Author

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;
Copy link
Member Author

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) {
Copy link
Member Author

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);
Copy link
Member Author

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();
Copy link
Member Author

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.

@jacob314
Copy link
Contributor

lgtm

Future<void> _pullTraceEventsFromVmTimeline({
bool shouldPrimeThreadIds = false,
}) async {
final currentVmTime = await serviceManager.service.getVMTimelineMicros();
Copy link
Contributor

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.

DateTime.now().millisecondsSinceEpoch,
);
allTraceEvents.add(eventWrapper);
debugTraceEventLog(eventWrapper.event.json.toString());
Copy link
Contributor

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.

Copy link
Member Author

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

Copy link
Contributor

@jacob314 jacob314 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lgtm
once case where fetching polls could take more than 2 secs is handled.

@@ -208,15 +210,17 @@ class PerformanceController extends DisposableController
await processTraceEvents(allTraceEvents);
_processing.value = false;

_timelinePollingRateLimiter = RateLimiter(
timelinePollingRateLimit,
() async => await _pullTraceEventsFromVmTimeline(),
Copy link
Contributor

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?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

Copy link
Contributor

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.

@kenzieschmoll kenzieschmoll merged commit 6fe6871 into flutter:master Jul 16, 2021
@kenzieschmoll kenzieschmoll deleted the frametiming branch July 16, 2021 22:21
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Migrate flutter frames chart (performance page) to use data from FrameTiming API
3 participants