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