Skip to content

Commit

Permalink
Cache CPU profiles for selected frames (#3121)
Browse files Browse the repository at this point in the history
  • Loading branch information
kenzieschmoll authored Jun 14, 2021
1 parent d50bf5b commit 870f4e3
Show file tree
Hide file tree
Showing 9 changed files with 257 additions and 92 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -170,33 +170,38 @@ class LegacyPerformanceController
}
}

Future<void> selectTimelineEvent(LegacyTimelineEvent event) async {
Future<void> selectTimelineEvent(
LegacyTimelineEvent event, {
bool updateProfiler = true,
}) async {
if (event == null || data.selectedEvent == event) return;

data.selectedEvent = event;
_selectedTimelineEventNotifier.value = event;

cpuProfilerController.reset();

// Fetch a profile if not in offline mode and if the profiler is enabled.
if ((!offlineMode || offlinePerformanceData == null) &&
cpuProfilerController.profilerEnabled) {
await getCpuProfileForSelectedEvent();
if (event.isUiEvent && updateProfiler) {
final storedProfile =
cpuProfilerController.cpuProfileStore.lookupProfile(event.time);
if (storedProfile != null) {
await cpuProfilerController.processAndSetData(
storedProfile,
processId: 'Stored profile for ${event.time}',
);
data.cpuProfileData = cpuProfilerController.dataNotifier.value;
} else if ((!offlineMode || offlinePerformanceData == null) &&
cpuProfilerController.profilerEnabled) {
// Fetch a profile if not in offline mode and if the profiler is enabled
cpuProfilerController.reset();
await cpuProfilerController.pullAndProcessProfile(
startMicros: event.time.start.inMicroseconds,
extentMicros: event.time.duration.inMicroseconds,
processId: '${event.traceEvents.first.id}',
);
data.cpuProfileData = cpuProfilerController.dataNotifier.value;
}
}
}

Future<void> getCpuProfileForSelectedEvent() async {
final selectedEvent = data.selectedEvent;
if (!selectedEvent.isUiEvent) return;

await cpuProfilerController.pullAndProcessProfile(
startMicros: selectedEvent.time.start.inMicroseconds,
extentMicros: selectedEvent.time.duration.inMicroseconds,
processId: '${selectedEvent.traceEvents.first.id}',
);
data.cpuProfileData = cpuProfilerController.dataNotifier.value;
}

ValueListenable<double> get displayRefreshRate => _displayRefreshRate;
final _displayRefreshRate = ValueNotifier<double>(defaultRefreshRate);

Expand All @@ -215,9 +220,29 @@ class LegacyPerformanceController
data.selectedFrame = frame;
_selectedFrameNotifier.value = frame;

await selectTimelineEvent(frame.uiEventFlow);
// We do not need to pull the CPU profile because we will pull the profile
// for the entire frame. The order of selecting the timeline event and
// pulling the CPU profile for the frame (directly below) matters here.
// If the selected timeline event is null, the event details section will
// not show the progress bar while we are processing the CPU profile.
await selectTimelineEvent(frame.uiEventFlow, updateProfiler: false);

final storedProfileForFrame =
cpuProfilerController.cpuProfileStore.lookupProfile(frame.time);
if (storedProfileForFrame == null) {
cpuProfilerController.reset();
await cpuProfilerController.pullAndProcessProfile(
startMicros: frame.time.start.inMicroseconds,
extentMicros: frame.time.duration.inMicroseconds,
processId: 'Flutter frame ${frame.id}',
);
data.cpuProfileData = cpuProfilerController.dataNotifier.value;
} else {
data.cpuProfileData = storedProfileForFrame;
cpuProfilerController.loadProcessedData(storedProfileForFrame);
}

if (debugTimeline && frame != null) {
if (debugTimeline) {
final buf = StringBuffer();
buf.writeln('UI timeline event for frame ${frame.id}:');
frame.uiEventFlow.format(buf, ' ');
Expand Down Expand Up @@ -351,7 +376,8 @@ class LegacyPerformanceController
}

FutureOr<void> processOfflineData(
LegacyOfflinePerformanceData offlineData) async {
LegacyOfflinePerformanceData offlineData,
) async {
await clearData();
final traceEvents = [
for (var trace in offlineData.traceEvents)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -447,8 +447,6 @@ class LegacyFlutterFrame {
double get rasterDurationMs =>
rasterDuration != null ? rasterDuration / 1000 : null;

CpuProfileData cpuProfileData;

void setEventFlow(LegacySyncTimelineEvent event, {TimelineEventType type}) {
type ??= event?.type;
if (type == TimelineEventType.ui) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -161,33 +161,38 @@ class PerformanceController
data?.displayRefreshRate = _displayRefreshRate.value;
}

Future<void> selectTimelineEvent(TimelineEvent event) async {
Future<void> selectTimelineEvent(
TimelineEvent event, {
bool updateProfiler = true,
}) async {
if (event == null || data.selectedEvent == event) return;

data.selectedEvent = event;
_selectedTimelineEventNotifier.value = event;

cpuProfilerController.reset();

// Fetch a profile if not in offline mode and if the profiler is enabled.
if ((!offlineMode || offlinePerformanceData == null) &&
cpuProfilerController.profilerEnabled) {
await getCpuProfileForSelectedEvent();
if (event.isUiEvent && updateProfiler) {
final storedProfile =
cpuProfilerController.cpuProfileStore.lookupProfile(event.time);
if (storedProfile != null) {
await cpuProfilerController.processAndSetData(
storedProfile,
processId: 'Stored profile for ${event.time}',
);
data.cpuProfileData = cpuProfilerController.dataNotifier.value;
} else if ((!offlineMode || offlinePerformanceData == null) &&
cpuProfilerController.profilerEnabled) {
// Fetch a profile if not in offline mode and if the profiler is enabled
cpuProfilerController.reset();
await cpuProfilerController.pullAndProcessProfile(
startMicros: event.time.start.inMicroseconds,
extentMicros: event.time.duration.inMicroseconds,
processId: '${event.traceEvents.first.id}',
);
data.cpuProfileData = cpuProfilerController.dataNotifier.value;
}
}
}

Future<void> getCpuProfileForSelectedEvent() async {
final selectedEvent = data.selectedEvent;
if (!selectedEvent.isUiEvent) return;

await cpuProfilerController.pullAndProcessProfile(
startMicros: selectedEvent.time.start.inMicroseconds,
extentMicros: selectedEvent.time.duration.inMicroseconds,
processId: '${selectedEvent.traceEvents.first.id}',
);
data.cpuProfileData = cpuProfilerController.dataNotifier.value;
}

ValueListenable<double> get displayRefreshRate => _displayRefreshRate;
final _displayRefreshRate = ValueNotifier<double>(defaultRefreshRate);

Expand All @@ -206,9 +211,29 @@ class PerformanceController
data.selectedFrame = frame;
_selectedFrameNotifier.value = frame;

await selectTimelineEvent(frame.uiEventFlow);
// We do not need to pull the CPU profile because we will pull the profile
// for the entire frame. The order of selecting the timeline event and
// pulling the CPU profile for the frame (directly below) matters here.
// If the selected timeline event is null, the event details section will
// not show the progress bar while we are processing the CPU profile.
await selectTimelineEvent(frame.uiEventFlow, updateProfiler: false);

final storedProfileForFrame =
cpuProfilerController.cpuProfileStore.lookupProfile(frame.time);
if (storedProfileForFrame == null) {
cpuProfilerController.reset();
await cpuProfilerController.pullAndProcessProfile(
startMicros: frame.time.start.inMicroseconds,
extentMicros: frame.time.duration.inMicroseconds,
processId: 'Flutter frame ${frame.id}',
);
data.cpuProfileData = cpuProfilerController.dataNotifier.value;
} else {
data.cpuProfileData = storedProfileForFrame;
cpuProfilerController.loadProcessedData(storedProfileForFrame);
}

if (debugTimeline && frame != null) {
if (debugTimeline) {
final buf = StringBuffer();
buf.writeln('UI timeline event for frame ${frame.id}:');
frame.uiEventFlow.format(buf, ' ');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -436,8 +436,6 @@ class FlutterFrame {
double get rasterDurationMs =>
rasterDuration != null ? rasterDuration / 1000 : null;

CpuProfileData cpuProfileData;

void setEventFlow(SyncTimelineEvent event, {TimelineEventType type}) {
type ??= event?.type;
if (type == TimelineEventType.ui) {
Expand Down
19 changes: 19 additions & 0 deletions packages/devtools_app/lib/src/profiler/cpu_profile_controller.dart
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@ class CpuProfilerController with SearchControllerMixin<CpuStackFrame> {
/// base state where recording instructions should be shown.
static CpuProfileData baseStateCpuProfileData = CpuProfileData.empty();

/// Store of cached CPU profiles.
final cpuProfileStore = CpuProfileStore();

/// Notifies that new cpu profile data is available.
ValueListenable<CpuProfileData> get dataNotifier => _dataNotifier;
final _dataNotifier = ValueNotifier<CpuProfileData>(baseStateCpuProfileData);
Expand Down Expand Up @@ -96,6 +99,21 @@ class CpuProfilerController with SearchControllerMixin<CpuStackFrame> {
extentMicros: extentMicros,
);

await processAndSetData(cpuProfileData);
cpuProfileStore.addProfile(
TimeRange()
..start = Duration(microseconds: startMicros)
..end = Duration(microseconds: startMicros + extentMicros),
_dataNotifier.value,
);
}

Future<void> processAndSetData(
CpuProfileData cpuProfileData, {
String processId,
}) async {
_processingNotifier.value = true;
_dataNotifier.value = null;
try {
await transformer.processData(cpuProfileData, processId: processId);
_dataNotifier.value = cpuProfileData;
Expand Down Expand Up @@ -175,6 +193,7 @@ class CpuProfilerController with SearchControllerMixin<CpuStackFrame> {

Future<void> clear() async {
reset();
cpuProfileStore.clear();
await serviceManager.service.clearSamples();
}

Expand Down
67 changes: 67 additions & 0 deletions packages/devtools_app/lib/src/profiler/cpu_profile_model.dart
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import '../trace_event.dart';
import '../trees.dart';
import '../ui/search.dart';
import '../utils.dart';
import 'cpu_profile_transformer.dart';

/// Data model for DevTools CPU profile.
class CpuProfileData {
Expand Down Expand Up @@ -108,6 +109,21 @@ class CpuProfileData {
/// Marks whether this data has already been processed.
bool processed = false;

List<CpuStackFrame> get callTreeRoots {
if (!processed) return <CpuStackFrame>[];
return _callTreeRoots ??= [_cpuProfileRoot.deepCopy()];
}

List<CpuStackFrame> _callTreeRoots;

List<CpuStackFrame> get bottomUpRoots {
if (!processed) return <CpuStackFrame>[];
return _bottomUpRoots ??=
BottomUpProfileTransformer.processData(_cpuProfileRoot);
}

List<CpuStackFrame> _bottomUpRoots;

final Map<String, dynamic> stackFramesJson;

/// Trace events associated with the last stackFrame in each sample (i.e. the
Expand Down Expand Up @@ -407,3 +423,54 @@ int stackFrameIdCompare(String a, String b) {
throw error;
}
}

class CpuProfileStore {
final _profiles = <TimeRange, CpuProfileData>{};

/// Lookup a profile from the cache [_profiles] for the given range [time].
///
/// If [_profiles] contains a CPU profile for a time range that encompasses
/// [time], a sub profile will be generated, cached in [_profiles] and then
/// returned. This method will return null if no profiles are cached for
/// [time] or if a sub profile cannot be generated for [time].
CpuProfileData lookupProfile(TimeRange time) {
// 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
// from the vm service.
_maybeGenerateSubProfile(time);
return _profiles[time];
}

void addProfile(TimeRange time, CpuProfileData profile) {
_profiles[time] = profile;
}

void _maybeGenerateSubProfile(TimeRange time) {
if (_profiles.containsKey(time)) return;
final encompassingTimeRange = _encompassingTimeRange(time);
if (encompassingTimeRange != null) {
final encompassingProfile = _profiles[encompassingTimeRange];

final subProfile = CpuProfileData.subProfile(encompassingProfile, time);
_profiles[time] = subProfile;
}
}

TimeRange _encompassingTimeRange(TimeRange time) {
int shortestDurationMicros = maxJsInt;
TimeRange encompassingTimeRange;
for (final t in _profiles.keys) {
// We want to find the shortest encompassing time range for [time].
if (t.containsRange(time) &&
t.duration.inMicroseconds < shortestDurationMicros) {
shortestDurationMicros = t.duration.inMicroseconds;
encompassingTimeRange = t;
}
}
return encompassingTimeRange;
}

void clear() {
_profiles.clear();
}
}
15 changes: 7 additions & 8 deletions packages/devtools_app/lib/src/profiler/cpu_profiler.dart
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ import 'cpu_profile_call_tree.dart';
import 'cpu_profile_controller.dart';
import 'cpu_profile_flame_chart.dart';
import 'cpu_profile_model.dart';
import 'cpu_profile_transformer.dart';

// TODO(kenz): provide useful UI upon selecting a CPU stack frame.

Expand All @@ -26,10 +25,8 @@ class CpuProfiler extends StatefulWidget {
@required this.controller,
this.searchFieldKey,
this.standaloneProfiler = true,
}) : callTreeRoots = data != null ? [data.cpuProfileRoot.deepCopy()] : [],
bottomUpRoots = data != null
? BottomUpProfileTransformer.processData(data.cpuProfileRoot)
: [];
}) : callTreeRoots = data?.callTreeRoots ?? [],
bottomUpRoots = data?.bottomUpRoots ?? [];

final CpuProfileData data;

Expand Down Expand Up @@ -316,9 +313,11 @@ class UserTagDropdown extends StatelessWidget {
value: tag,
),
],
onChanged: userTags.isNotEmpty
? (String tag) => _onUserTagChanged(tag, context)
: null,
onChanged: userTags.isEmpty ||
(userTags.length == 1 &&
userTags.first == UserTag.defaultTag.label)
? null
: (String tag) => _onUserTagChanged(tag, context),
),
);
},
Expand Down
2 changes: 2 additions & 0 deletions packages/devtools_app/lib/src/utils.dart
Original file line number Diff line number Diff line change
Expand Up @@ -555,6 +555,8 @@ class TimeRange {

bool contains(Duration target) => target >= start && target <= end;

bool containsRange(TimeRange t) => contains(t.start) && contains(t.end);

set end(Duration value) {
if (singleAssignment) {
assert(_end == null);
Expand Down
Loading

0 comments on commit 870f4e3

Please sign in to comment.