Add support for viewing CPU profiler ticks per instruction (#5258)
This change adds a `Profiler Ticks` column to the object inspector's
code viewer, allowing for compiler developers to determine how "hot"
specific instructions are in generated code.
RELEASE_NOTE_EXCEPTION=VM developer mode functionality
diff --git a/packages/devtools_app/lib/src/screens/debugger/codeview_controller.dart b/packages/devtools_app/lib/src/screens/debugger/codeview_controller.dart
index bd6aa36..759fe97 100644
--- a/packages/devtools_app/lib/src/screens/debugger/codeview_controller.dart
+++ b/packages/devtools_app/lib/src/screens/debugger/codeview_controller.dart
@@ -151,9 +151,8 @@
Future<void> _maybeSetUpProgramExplorer() async {
if (!programExplorerController.initialized.value) {
- programExplorerController
- ..initListeners()
- ..initialize();
+ programExplorerController.initListeners();
+ unawaited(programExplorerController.initialize());
}
if (currentScriptRef.value != null) {
await programExplorerController.selectScriptNode(currentScriptRef.value);
diff --git a/packages/devtools_app/lib/src/screens/debugger/program_explorer_controller.dart b/packages/devtools_app/lib/src/screens/debugger/program_explorer_controller.dart
index 5e93069..455d65d 100644
--- a/packages/devtools_app/lib/src/screens/debugger/program_explorer_controller.dart
+++ b/packages/devtools_app/lib/src/screens/debugger/program_explorer_controller.dart
@@ -66,7 +66,7 @@
}
/// Initializes the program structure.
- void initialize() {
+ Future<void> initialize() async {
if (_initializing) {
return;
}
@@ -80,6 +80,10 @@
.libraries!
: <LibraryRef>[];
+ if (scriptManager.sortedScripts.value.isEmpty && isolate != null) {
+ await scriptManager.retrieveAndSortScripts(isolate);
+ }
+
// Build the initial tree.
final nodes = VMServiceObjectNode.createRootsFrom(
this,
@@ -157,7 +161,7 @@
}
/// Clears controller state and re-initializes.
- void refresh() {
+ Future<void> refresh() {
_scriptSelection = null;
_outlineSelection = null;
_isLoadingOutline.value = true;
diff --git a/packages/devtools_app/lib/src/screens/vm_developer/object_inspector/object_inspector_view.dart b/packages/devtools_app/lib/src/screens/vm_developer/object_inspector/object_inspector_view.dart
index 2ddf21c..3f53b73 100644
--- a/packages/devtools_app/lib/src/screens/vm_developer/object_inspector/object_inspector_view.dart
+++ b/packages/devtools_app/lib/src/screens/vm_developer/object_inspector/object_inspector_view.dart
@@ -51,8 +51,8 @@
super.didChangeDependencies();
final vmDeveloperToolsController =
Provider.of<VMDeveloperToolsController>(context);
- controller = vmDeveloperToolsController.objectInspectorViewController
- ..init();
+ controller = vmDeveloperToolsController.objectInspectorViewController;
+ unawaited(controller.init());
}
@override
diff --git a/packages/devtools_app/lib/src/screens/vm_developer/object_inspector/object_inspector_view_controller.dart b/packages/devtools_app/lib/src/screens/vm_developer/object_inspector/object_inspector_view_controller.dart
index b2fcaef..039076e 100644
--- a/packages/devtools_app/lib/src/screens/vm_developer/object_inspector/object_inspector_view_controller.dart
+++ b/packages/devtools_app/lib/src/screens/vm_developer/object_inspector/object_inspector_view_controller.dart
@@ -2,6 +2,8 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
+import 'dart:async';
+
import 'package:collection/collection.dart';
import 'package:flutter/foundation.dart';
import 'package:vm_service/vm_service.dart';
@@ -51,11 +53,10 @@
bool _initialized = false;
- void init() {
+ Future<void> init() async {
if (!_initialized) {
- programExplorerController
- ..initialize()
- ..initListeners();
+ await programExplorerController.initialize();
+ programExplorerController.initListeners();
_initializeForCurrentIsolate();
_initialized = true;
}
diff --git a/packages/devtools_app/lib/src/screens/vm_developer/object_inspector/vm_code_display.dart b/packages/devtools_app/lib/src/screens/vm_developer/object_inspector/vm_code_display.dart
index d407145..e24c191 100644
--- a/packages/devtools_app/lib/src/screens/vm_developer/object_inspector/vm_code_display.dart
+++ b/packages/devtools_app/lib/src/screens/vm_developer/object_inspector/vm_code_display.dart
@@ -17,9 +17,6 @@
import 'object_inspector_view_controller.dart';
import 'vm_object_model.dart';
-// TODO(bkonyi): remove once profile ticks are populated for instructions.
-const profilerTicksEnabled = false;
-
abstract class _CodeColumnData extends ColumnData<Instruction> {
_CodeColumnData(super.title, {required super.fixedWidthPx});
_CodeColumnData.wide(super.title) : super.wide();
@@ -47,12 +44,36 @@
}
}
+// TODO(bkonyi): consider coloring the background similarly to how we indicate
+// code "hotness" in the debugger tab. To do this properly here, we'd need to
+// modify the table column padding logic to allow for custom column rendering
+// that can fill the entire column which is a can of worms I'd rather not open
+// for some rather niche functionality. We can revisit this once we can use the
+// table implementation from the Flutter framework.
class _ProfileTicksColumn extends _CodeColumnData {
- _ProfileTicksColumn(super.title) : super(fixedWidthPx: 80);
+ _ProfileTicksColumn(
+ super.title, {
+ required this.inclusive,
+ required this.ticks,
+ }) : super(fixedWidthPx: 140);
+
+ final bool inclusive;
+ final CpuProfilerTicksTable? ticks;
@override
- Object? getValue(Instruction dataObject) {
- return '';
+ int? getValue(Instruction dataObject) {
+ if (ticks == null) return null;
+ final tick = ticks![dataObject.unpaddedAddress];
+ return inclusive ? tick?.inclusiveTicks : tick?.exclusiveTicks;
+ }
+
+ @override
+ String getDisplayValue(Instruction dataObject) {
+ final value = getValue(dataObject);
+ if (value == null) return '';
+
+ final percentage = percent2(value / ticks!.sampleCount);
+ return '$percentage ($value)';
}
}
@@ -214,6 +235,7 @@
child: CodeTable(
code: code,
controller: controller,
+ ticks: code.ticksTable,
),
),
],
@@ -249,20 +271,30 @@
Key? key,
required this.code,
required this.controller,
+ required this.ticks,
}) : super(key: key);
late final columns = <ColumnData<Instruction>>[
_AddressColumn(),
_InstructionColumn(),
_DartObjectColumn(controller: controller),
- if (profilerTicksEnabled) ...[
- _ProfileTicksColumn('Inclusive'),
- _ProfileTicksColumn('Exclusive'),
+ if (ticks != null) ...[
+ _ProfileTicksColumn(
+ 'Total %',
+ ticks: code.ticksTable,
+ inclusive: true,
+ ),
+ _ProfileTicksColumn(
+ 'Self %',
+ ticks: code.ticksTable,
+ inclusive: false,
+ ),
],
];
final ObjectInspectorViewController controller;
final CodeObject code;
+ final CpuProfilerTicksTable? ticks;
@override
Widget build(BuildContext context) {
@@ -272,10 +304,10 @@
keyFactory: (instruction) => Key(instruction.address),
columnGroups: [
ColumnGroup.fromText(title: 'Instructions', range: const Range(0, 3)),
- if (profilerTicksEnabled)
+ if (ticks != null)
ColumnGroup.fromText(
title: 'Profiler Ticks',
- range: const Range(4, 6),
+ range: const Range(3, 5),
),
],
columns: columns,
@@ -284,3 +316,44 @@
);
}
}
+
+/// A mapping of [Instruction] addresses to corresponding CPU profiler ticks.
+class CpuProfilerTicksTable {
+ CpuProfilerTicksTable.parse({
+ required this.sampleCount,
+ required List<dynamic> ticks,
+ }) : assert(ticks.length % 3 == 0) {
+ // Ticks are built up of groups of 3 elements:
+ // [address, exclusiveTicks, inclusiveTicks]
+ for (int i = 0; i < ticks.length; i += 3) {
+ _table[ticks[i] as String] = CodeTicks(
+ exclusiveTicks: ticks[i + 1],
+ inclusiveTicks: ticks[i + 2],
+ );
+ }
+ }
+
+ /// The total number of samples in the original [CpuSamples] response.
+ final int sampleCount;
+
+ /// Retrieves CPU profiler [CodeTicks] associated with a given [Instruction]
+ /// address.
+ ///
+ /// If no CPU samples were collected for a given instruction address, null is
+ /// returned.
+ CodeTicks? operator [](String address) => _table[address];
+
+ final _table = <String, CodeTicks>{};
+}
+
+/// Tracks inclusive and exclusive CPU profiler ticks for a single
+/// [Instruction].
+class CodeTicks {
+ const CodeTicks({
+ required this.inclusiveTicks,
+ required this.exclusiveTicks,
+ });
+
+ final int exclusiveTicks;
+ final int inclusiveTicks;
+}
diff --git a/packages/devtools_app/lib/src/screens/vm_developer/object_inspector/vm_object_model.dart b/packages/devtools_app/lib/src/screens/vm_developer/object_inspector/vm_object_model.dart
index 971e688..668fe7c 100644
--- a/packages/devtools_app/lib/src/screens/vm_developer/object_inspector/vm_object_model.dart
+++ b/packages/devtools_app/lib/src/screens/vm_developer/object_inspector/vm_object_model.dart
@@ -11,6 +11,7 @@
import '../../../shared/globals.dart';
import '../../../shared/primitives/utils.dart';
import '../vm_service_private_extensions.dart';
+import 'vm_code_display.dart';
/// Wrapper class for storing Dart VM objects with their relevant VM
/// information.
@@ -240,7 +241,6 @@
DateTime get loadTime => DateTime.fromMillisecondsSinceEpoch(obj.loadTime);
}
-//TODO(mtaylee): finish class implementation.
class InstanceObject extends VmObject {
InstanceObject({required super.ref, super.scriptRef});
@@ -265,6 +265,42 @@
@override
String? get name => obj.name;
+
+ /// A collection of CPU profiler information for individual [Instruction]s.
+ ///
+ /// Returns null if the CPU profiler is disabled.
+ CpuProfilerTicksTable? get ticksTable => _table;
+ CpuProfilerTicksTable? _table;
+
+ @override
+ Future<void> initialize() async {
+ await super.initialize();
+
+ final service = serviceManager.service!;
+ final isolateId = serviceManager.isolateManager.selectedIsolate.value!.id!;
+
+ // Attempt to retrieve the CPU profile data for this code object.
+ try {
+ final samples = await service.getCpuSamples(isolateId, 0, maxJsInt);
+ final codes = samples.codes;
+
+ final match = codes.firstWhereOrNull(
+ (profileCode) => profileCode.code == ref,
+ );
+
+ if (match == null) {
+ throw StateError('Unable to find matching ProfileCode');
+ }
+
+ _table = CpuProfilerTicksTable.parse(
+ sampleCount: samples.sampleCount!,
+ ticks: match.ticks!,
+ );
+ } on RPCError {
+ // This can happen when the profiler is disabled, so we just can't show
+ // CPU profiling ticks for the code disassembly.
+ }
+ }
}
/// Stores an 'ObjectPool' VM object and provides an interface for obtaining
diff --git a/packages/devtools_app/lib/src/screens/vm_developer/vm_service_private_extensions.dart b/packages/devtools_app/lib/src/screens/vm_developer/vm_service_private_extensions.dart
index e098978..cc1059b 100644
--- a/packages/devtools_app/lib/src/screens/vm_developer/vm_service_private_extensions.dart
+++ b/packages/devtools_app/lib/src/screens/vm_developer/vm_service_private_extensions.dart
@@ -352,6 +352,10 @@
/// The instruction's address in memory.
final String address;
+ /// The instruction's address in memory with leading zeros removed.
+ String get unpaddedAddress =>
+ address.substring(address.indexOf(RegExp(r'[^0]')));
+
/// TODO(bkonyi): figure out what this value is for.
final String unknown;
diff --git a/packages/devtools_app/test/debugger/debugger_screen_test.dart b/packages/devtools_app/test/debugger/debugger_screen_test.dart
index 9919a3c..ee8e7e0 100644
--- a/packages/devtools_app/test/debugger/debugger_screen_test.dart
+++ b/packages/devtools_app/test/debugger/debugger_screen_test.dart
@@ -68,7 +68,7 @@
controller.rootObjectNodesInternal.add(libraryNode);
},
);
- programExplorerController.initialize();
+ await programExplorerController.initialize();
await programExplorerController.selectNode(libraryNode);
final codeViewController = createMockCodeViewControllerWithDefaults(
diff --git a/packages/devtools_app/test/debugger/program_explorer_test.dart b/packages/devtools_app/test/debugger/program_explorer_test.dart
index 3063255..b0b96b4 100644
--- a/packages/devtools_app/test/debugger/program_explorer_test.dart
+++ b/packages/devtools_app/test/debugger/program_explorer_test.dart
@@ -104,7 +104,7 @@
},
);
final explorer = ProgramExplorer(controller: programExplorerController);
- programExplorerController.initialize();
+ await programExplorerController.initialize();
await tester.pumpWidget(
wrap(
Builder(
diff --git a/packages/devtools_app/test/vm_developer/object_inspector/object_inspector_view_test.dart b/packages/devtools_app/test/vm_developer/object_inspector/object_inspector_view_test.dart
index 3c458b1..e225494 100644
--- a/packages/devtools_app/test/vm_developer/object_inspector/object_inspector_view_test.dart
+++ b/packages/devtools_app/test/vm_developer/object_inspector/object_inspector_view_test.dart
@@ -12,6 +12,8 @@
import 'package:mockito/mockito.dart';
import 'package:vm_service/vm_service.dart';
+import '../vm_developer_test_utils.dart';
+
void main() {
late ObjectInspectorView objectInspector;
@@ -26,7 +28,13 @@
fakeServiceManager = FakeServiceManager();
scriptManager = MockScriptManager();
- when(scriptManager.sortedScripts).thenReturn(ValueNotifier(<ScriptRef>[]));
+ when(scriptManager.sortedScripts).thenReturn(
+ ValueNotifier(<ScriptRef>[testScript]),
+ );
+ // ignore: discarded_futures
+ when(scriptManager.retrieveAndSortScripts(any)).thenAnswer(
+ (_) => Future.value([testScript]),
+ );
when(fakeServiceManager.connectedApp!.isProfileBuildNow).thenReturn(false);
when(fakeServiceManager.connectedApp!.isDartWebAppNow).thenReturn(false);
diff --git a/packages/devtools_app/test/vm_developer/object_inspector/vm_code_display_test.dart b/packages/devtools_app/test/vm_developer/object_inspector/vm_code_display_test.dart
index 02a0065..8375e71 100644
--- a/packages/devtools_app/test/vm_developer/object_inspector/vm_code_display_test.dart
+++ b/packages/devtools_app/test/vm_developer/object_inspector/vm_code_display_test.dart
@@ -39,8 +39,9 @@
}
};
final offset = pow(2, 20) as int;
+ const addressCount = 1000;
testCode.disassembly = Disassembly.parse(<Object?>[
- for (int i = 0; i < 1000; ++i) ...[
+ for (int i = 0; i < addressCount; ++i) ...[
(i * 4 + offset).toRadixString(16),
'unknown',
'noop',
@@ -48,6 +49,17 @@
]
]);
+ final ticksTable = CpuProfilerTicksTable.parse(
+ sampleCount: 1000,
+ ticks: [
+ for (int i = 0; i < addressCount; ++i) ...[
+ (i * 4 + offset).toRadixString(16),
+ 1,
+ 1,
+ ],
+ ],
+ );
+
when(mockCodeObject.obj).thenReturn(testCode);
when(mockCodeObject.script).thenReturn(null);
when(mockCodeObject.retainingPath).thenReturn(
@@ -64,14 +76,23 @@
);
when(mockCodeObject.retainedSize).thenReturn(null);
when(mockCodeObject.reachableSize).thenReturn(null);
+ when(mockCodeObject.ticksTable).thenReturn(ticksTable);
});
- void verifyAddressOrder(List<Instruction> data) {
+ void verifyAddressOrder(
+ List<Instruction> data,
+ CpuProfilerTicksTable? ticks,
+ ) {
int lastAddress = 0;
for (final instr in data) {
final currentAddress = int.parse(instr.address, radix: 16);
expect(currentAddress > lastAddress, isTrue);
lastAddress = currentAddress;
+
+ final tick = ticks![instr.unpaddedAddress];
+ expect(tick, isNotNull);
+ expect(tick!.inclusiveTicks, 1);
+ expect(tick.exclusiveTicks, 1);
}
}
@@ -91,8 +112,22 @@
final FlatTableState<Instruction> state =
tester.state(find.byType(FlatTable<Instruction>));
+ // Check that the profiler columns render ticks correctly.
+ final profilerColumns = state.tableController.columns.where(
+ (c) => c.title == 'Total %' || c.title == 'Self %',
+ );
+ expect(profilerColumns.length, 2);
+ for (final profilerColumn in profilerColumns) {
+ for (final instr in state.tableController.tableData.value.data) {
+ expect(profilerColumn.getDisplayValue(instr), '0.10% (1)');
+ }
+ }
+
// Ensure ordering is correct.
- verifyAddressOrder(state.tableController.tableData.value.data);
+ verifyAddressOrder(
+ state.tableController.tableData.value.data,
+ mockCodeObject.ticksTable,
+ );
final columns = state.widget.columns;
@@ -100,7 +135,10 @@
for (final column in columns) {
await tester.tap(find.text(column.title));
await tester.pumpAndSettle();
- verifyAddressOrder(state.tableController.tableData.value.data);
+ verifyAddressOrder(
+ state.tableController.tableData.value.data,
+ mockCodeObject.ticksTable,
+ );
}
});
});
diff --git a/packages/devtools_test/lib/src/mocks/fake_program_explorer_controller.dart b/packages/devtools_test/lib/src/mocks/fake_program_explorer_controller.dart
index a503e6f..a270048 100644
--- a/packages/devtools_test/lib/src/mocks/fake_program_explorer_controller.dart
+++ b/packages/devtools_test/lib/src/mocks/fake_program_explorer_controller.dart
@@ -2,6 +2,8 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
+import 'dart:async';
+
import 'package:devtools_app/devtools_app.dart';
import 'package:flutter/foundation.dart';
import 'package:vm_service/vm_service.dart';
@@ -15,14 +17,14 @@
ValueListenable<bool> get initialized => _initialized;
final _initialized = ValueNotifier<bool>(false);
- final Function(TestProgramExplorerController) initializer;
+ final FutureOr<void> Function(TestProgramExplorerController) initializer;
@override
- void initialize() {
+ Future<void> initialize() async {
if (_initialized.value) {
return;
}
- initializer(this);
+ await initializer(this);
_initialized.value = true;
}