Fix error in bottom up timing calculation (#5348)

diff --git a/packages/.vscode/launch.json b/packages/.vscode/launch.json
index 3e68f70..2d11ebd 100644
--- a/packages/.vscode/launch.json
+++ b/packages/.vscode/launch.json
@@ -4,6 +4,7 @@
     // see mock errors in devtools_test.
     "version": "0.2.0",
     "configurations": [
+
         {
             "name": "opened test or devtools",
             "cwd": "devtools_app",
@@ -51,6 +52,12 @@
             "program": "devtools_app/test/test_infra/scenes/performance/default.stager_app.dart",
         },
         {
+            "name": "profiler/default",
+            "request": "launch",
+            "type": "dart",
+            "program": "devtools_app/test/test_infra/scenes/cpu_profiler/default.stager_app.dart",
+        },
+        {
             "name": "fixtures/flutter_app",
             "request": "launch",
             "type": "dart",
diff --git a/packages/devtools_app/lib/src/screens/profiler/cpu_profile_model.dart b/packages/devtools_app/lib/src/screens/profiler/cpu_profile_model.dart
index 1c595ea..e76b496 100644
--- a/packages/devtools_app/lib/src/screens/profiler/cpu_profile_model.dart
+++ b/packages/devtools_app/lib/src/screens/profiler/cpu_profile_model.dart
@@ -190,9 +190,10 @@
       final resolvedUrl = (stackFrameJson[resolvedUrlKey] as String?) ?? '';
       final packageUri =
           (stackFrameJson[resolvedPackageUriKey] as String?) ?? resolvedUrl;
+      final name = getSimpleStackFrameName(stackFrameJson[nameKey] as String?);
       final stackFrame = CpuStackFrame(
         id: entry.key,
-        name: getSimpleStackFrameName(stackFrameJson[nameKey] as String?),
+        name: name,
         verboseName: stackFrameJson[nameKey] as String?,
         category: stackFrameJson[categoryKey] as String?,
         // If the user is on a version of Flutter where resolvedUrl is not
@@ -995,7 +996,6 @@
     int? sourceLine,
     CpuProfileMetaData? profileMetaData,
     bool copySampleCounts = true,
-    bool resetInclusiveSampleCount = true,
   }) {
     final copy = CpuStackFrame._(
       id: id ?? this.id,
@@ -1012,8 +1012,7 @@
     if (copySampleCounts) {
       copy
         ..exclusiveSampleCount = exclusiveSampleCount
-        ..inclusiveSampleCount =
-            resetInclusiveSampleCount ? null : inclusiveSampleCount;
+        ..inclusiveSampleCount = inclusiveSampleCount;
     }
     return copy;
   }
@@ -1023,7 +1022,7 @@
   /// The returned copy stack frame will have a null parent.
   @override
   CpuStackFrame deepCopy() {
-    final copy = shallowCopy(resetInclusiveSampleCount: false);
+    final copy = shallowCopy();
     for (CpuStackFrame child in children) {
       copy.addChild(child.deepCopy());
     }
diff --git a/packages/devtools_app/lib/src/screens/profiler/cpu_profiler.dart b/packages/devtools_app/lib/src/screens/profiler/cpu_profiler.dart
index df074a0..8608934 100644
--- a/packages/devtools_app/lib/src/screens/profiler/cpu_profiler.dart
+++ b/packages/devtools_app/lib/src/screens/profiler/cpu_profiler.dart
@@ -306,7 +306,7 @@
         valueListenable: preferences.cpuProfiler.displayTreeGuidelines,
         builder: (context, displayTreeGuidelines, _) {
           return CpuBottomUpTable(
-            widget.bottomUpRoots,
+            bottomUpRoots: widget.bottomUpRoots,
             displayTreeGuidelines: displayTreeGuidelines,
           );
         },
@@ -317,7 +317,7 @@
         valueListenable: preferences.cpuProfiler.displayTreeGuidelines,
         builder: (context, displayTreeGuidelines, _) {
           return CpuCallTreeTable(
-            widget.callTreeRoots,
+            dataRoots: widget.callTreeRoots,
             displayTreeGuidelines: displayTreeGuidelines,
           );
         },
diff --git a/packages/devtools_app/lib/src/screens/profiler/panes/bottom_up.dart b/packages/devtools_app/lib/src/screens/profiler/panes/bottom_up.dart
index 0ef4a63..dac210e 100644
--- a/packages/devtools_app/lib/src/screens/profiler/panes/bottom_up.dart
+++ b/packages/devtools_app/lib/src/screens/profiler/panes/bottom_up.dart
@@ -13,52 +13,28 @@
 
 /// A table of the bottom up tree for a CPU profile.
 class CpuBottomUpTable extends StatelessWidget {
-  factory CpuBottomUpTable(
-    List<CpuStackFrame> bottomUpRoots, {
-    Key? key,
-    required bool displayTreeGuidelines,
-  }) {
-    final treeColumn = MethodAndSourceColumn();
-    final selfTimeColumn = SelfTimeColumn(
-      titleTooltip: selfTimeTooltip,
-      dataTooltipProvider: (stackFrame, context) =>
-          _bottomUpTimeTooltipBuilder('Self', stackFrame, context),
-    );
-    final totalTimeColumn = TotalTimeColumn(
-      titleTooltip: totalTimeTooltip,
-      dataTooltipProvider: (stackFrame, context) =>
-          _bottomUpTimeTooltipBuilder('Total', stackFrame, context),
-    );
-    final columns = List<ColumnData<CpuStackFrame>>.unmodifiable([
-      totalTimeColumn,
-      selfTimeColumn,
-      treeColumn,
-    ]);
+  const CpuBottomUpTable({
+    required this.bottomUpRoots,
+    required this.displayTreeGuidelines,
+    super.key,
+  });
 
-    return CpuBottomUpTable._(
-      key,
-      bottomUpRoots,
-      treeColumn,
-      selfTimeColumn,
-      columns,
-      displayTreeGuidelines,
-    );
-  }
-
-  const CpuBottomUpTable._(
-    Key? key,
-    this.bottomUpRoots,
-    this.treeColumn,
-    this.sortColumn,
-    this.columns,
-    this.displayTreeGuidelines,
-  ) : super(key: key);
-
-  final TreeColumnData<CpuStackFrame> treeColumn;
-  final ColumnData<CpuStackFrame> sortColumn;
-  final List<ColumnData<CpuStackFrame>> columns;
-  final List<CpuStackFrame> bottomUpRoots;
-  final bool displayTreeGuidelines;
+  static final methodColumn = MethodAndSourceColumn();
+  static final selfTimeColumn = SelfTimeColumn(
+    titleTooltip: selfTimeTooltip,
+    dataTooltipProvider: (stackFrame, context) =>
+        _bottomUpTimeTooltipBuilder(_TimeType.self, stackFrame, context),
+  );
+  static final totalTimeColumn = TotalTimeColumn(
+    titleTooltip: totalTimeTooltip,
+    dataTooltipProvider: (stackFrame, context) =>
+        _bottomUpTimeTooltipBuilder(_TimeType.total, stackFrame, context),
+  );
+  static final columns = List<ColumnData<CpuStackFrame>>.unmodifiable([
+    totalTimeColumn,
+    selfTimeColumn,
+    methodColumn,
+  ]);
 
   static const totalTimeTooltip = '''
 For top-level methods in the bottom-up tree (stack frames that were at the top of at
@@ -75,27 +51,55 @@
 For children methods in the bottom-up tree (the callers), this is the self time of
 the top-level method (the callee) when called through the child method (the caller).''';
 
+  final List<CpuStackFrame> bottomUpRoots;
+
+  final bool displayTreeGuidelines;
+
   static InlineSpan? _bottomUpTimeTooltipBuilder(
-    String type,
+    _TimeType type,
     CpuStackFrame stackFrame,
     BuildContext context,
   ) {
-    // TODO(kenz): consider adding a tooltip for root nodes as well if this is
-    // a point of confusion for the user.
-    if (stackFrame.isRoot) {
-      return null;
-    }
     final fixedStyle = Theme.of(context).tooltipFixedFontStyle;
+    if (stackFrame.isRoot) {
+      switch (type) {
+        case _TimeType.total:
+          return TextSpan(
+            children: [
+              const TextSpan(text: 'Time that '),
+              TextSpan(
+                text: '[${stackFrame.name}]',
+                style: fixedStyle,
+              ),
+              const TextSpan(
+                text: ' spent executing its own code,\nas well as the code for'
+                    ' any methods that it called.',
+              ),
+            ],
+          );
+        case _TimeType.self:
+          return TextSpan(
+            children: [
+              const TextSpan(text: 'Time that '),
+              TextSpan(
+                text: '[${stackFrame.name}]',
+                style: fixedStyle,
+              ),
+              const TextSpan(text: ' spent executing its own code.'),
+            ],
+          );
+      }
+    }
     return TextSpan(
       children: [
-        TextSpan(text: '$type time for '),
+        TextSpan(text: '$type time for root '),
         TextSpan(
-          text: stackFrame.root.name,
+          text: '[${stackFrame.root.name}]',
           style: fixedStyle,
         ),
         const TextSpan(text: '\nwhen called through '),
         TextSpan(
-          text: stackFrame.name,
+          text: '[${stackFrame.name}]',
           style: fixedStyle,
         ),
       ],
@@ -110,9 +114,24 @@
       dataRoots: bottomUpRoots,
       dataKey: 'cpu-bottom-up',
       columns: columns,
-      treeColumn: treeColumn,
-      defaultSortColumn: sortColumn,
+      treeColumn: methodColumn,
+      defaultSortColumn: selfTimeColumn,
       defaultSortDirection: SortDirection.descending,
     );
   }
 }
+
+enum _TimeType {
+  self,
+  total;
+
+  @override
+  String toString() {
+    switch (this) {
+      case self:
+        return 'Self';
+      case total:
+        return 'Total';
+    }
+  }
+}
diff --git a/packages/devtools_app/lib/src/screens/profiler/panes/call_tree.dart b/packages/devtools_app/lib/src/screens/profiler/panes/call_tree.dart
index 64494a4..239f351 100644
--- a/packages/devtools_app/lib/src/screens/profiler/panes/call_tree.dart
+++ b/packages/devtools_app/lib/src/screens/profiler/panes/call_tree.dart
@@ -12,38 +12,24 @@
 
 /// A table of the CPU's top-down call tree.
 class CpuCallTreeTable extends StatelessWidget {
-  factory CpuCallTreeTable(
-    List<CpuStackFrame> dataRoots, {
-    Key? key,
-    required bool displayTreeGuidelines,
-  }) {
-    final treeColumn = MethodAndSourceColumn();
-    final selfTimeColumn = SelfTimeColumn(titleTooltip: selfTimeTooltip);
-    final totalTimeColumn = TotalTimeColumn(titleTooltip: totalTimeTooltip);
-    final columns = List<ColumnData<CpuStackFrame>>.unmodifiable([
-      totalTimeColumn,
-      selfTimeColumn,
-      treeColumn,
-    ]);
+  const CpuCallTreeTable({
+    required this.dataRoots,
+    required this.displayTreeGuidelines,
+    super.key,
+  });
 
-    return CpuCallTreeTable._(
-      key,
-      dataRoots,
-      treeColumn,
-      totalTimeColumn,
-      columns,
-      displayTreeGuidelines,
-    );
-  }
+  static final methodColumn = MethodAndSourceColumn();
 
-  const CpuCallTreeTable._(
-    Key? key,
-    this.dataRoots,
-    this.treeColumn,
-    this.sortColumn,
-    this.columns,
-    this.displayTreeGuidelines,
-  ) : super(key: key);
+  static final selfTimeColumn = SelfTimeColumn(titleTooltip: selfTimeTooltip);
+
+  static final totalTimeColumn =
+      TotalTimeColumn(titleTooltip: totalTimeTooltip);
+
+  static final columns = List<ColumnData<CpuStackFrame>>.unmodifiable([
+    totalTimeColumn,
+    selfTimeColumn,
+    methodColumn,
+  ]);
 
   static const totalTimeTooltip =
       'Time that a method spent executing its own code\nas well as the code for '
@@ -52,10 +38,8 @@
   static const selfTimeTooltip =
       'Time that a method spent executing only its own code.';
 
-  final TreeColumnData<CpuStackFrame> treeColumn;
-  final ColumnData<CpuStackFrame> sortColumn;
-  final List<ColumnData<CpuStackFrame>> columns;
   final List<CpuStackFrame> dataRoots;
+
   final bool displayTreeGuidelines;
 
   @override
@@ -65,8 +49,8 @@
       dataRoots: dataRoots,
       dataKey: 'cpu-call-tree',
       columns: columns,
-      treeColumn: treeColumn,
-      defaultSortColumn: sortColumn,
+      treeColumn: methodColumn,
+      defaultSortColumn: totalTimeColumn,
       displayTreeGuidelines: displayTreeGuidelines,
       defaultSortDirection: SortDirection.descending,
     );
diff --git a/packages/devtools_app/lib/src/shared/profiler_utils.dart b/packages/devtools_app/lib/src/shared/profiler_utils.dart
index 25694c4..f3ba32f 100644
--- a/packages/devtools_app/lib/src/shared/profiler_utils.dart
+++ b/packages/devtools_app/lib/src/shared/profiler_utils.dart
@@ -145,6 +145,7 @@
         node: topDownRoot,
         currentBottomUpRoot: null,
         bottomUpRoots: <T>[],
+        skipRoot: true,
       );
 
       // Set the bottom up sample counts for each sample.
@@ -156,13 +157,13 @@
     }
 
     if (rootedAtTags) {
-      // Calculate the total time for each tag root. The sum of the inclusive
+      // Calculate the total time for each tag root. The sum of the exclusive
       // times for each child for the tag node is the total time spent with the
       // given tag active.
       for (final tagRoot in bottomUpRoots) {
-        tagRoot._inclusiveSampleCount = tagRoot.children.fold<int>(
+        tagRoot.inclusiveSampleCount = tagRoot.children.fold<int>(
           0,
-          (prev, e) => prev + e._inclusiveSampleCount!,
+          (prev, e) => prev + e.exclusiveSampleCount,
         );
       }
     }
@@ -183,19 +184,35 @@
     required T node,
     required T? currentBottomUpRoot,
     required List<T> bottomUpRoots,
+    bool skipRoot = false,
   }) {
-    final copy = node.shallowCopy() as T;
+    if (skipRoot && node.isRoot) {
+      // When [skipRoot] is true, do not include the root node at the leaf of
+      // each bottom up tree. This is to avoid having the 'all' node at the
+      // at the bottom of each bottom up path.
+    } else {
+      // Inclusive and exclusive sample counts are copied by default.
+      final copy = node.shallowCopy() as T;
 
-    if (currentBottomUpRoot != null) {
-      copy.addChild(currentBottomUpRoot.deepCopy());
-    }
+      if (currentBottomUpRoot != null) {
+        copy.addChild(currentBottomUpRoot.deepCopy());
+      }
 
-    // [copy] is the new root of the bottom up stack.
-    currentBottomUpRoot = copy;
+      // [copy] is the new root of the bottom up stack.
+      currentBottomUpRoot = copy;
 
-    if (node.exclusiveSampleCount > 0) {
-      // This node is a leaf node, meaning it is a bottom up root.
-      bottomUpRoots.add(currentBottomUpRoot);
+      if (node.exclusiveSampleCount > 0) {
+        // This node is a leaf node, meaning that one or more CPU samples
+        // contain [currentBottomUpRoot] as the top stack frame. This means it
+        // is a bottom up root.
+        bottomUpRoots.add(currentBottomUpRoot);
+      } else {
+        // If [currentBottomUpRoot] is not a bottom up root, the inclusive count
+        // should be set to null. This will allow the inclusive count to be
+        // recalculated now that this node is part of it's parent's bottom up
+        // tree, not its own.
+        currentBottomUpRoot.inclusiveSampleCount = null;
+      }
     }
     for (final child in node.children.cast<T>()) {
       generateBottomUpRoots(
@@ -207,7 +224,8 @@
     return bottomUpRoots;
   }
 
-  /// Sets sample counts of [node] and all children to [exclusiveSampleCount].
+  /// Cascades the [exclusiveSampleCount] and [inclusiveSampleCount] of [node]
+  /// to all of its children (recursive).
   ///
   /// This is necessary for the transformation of a [ProfilableDataMixin] to its
   /// bottom-up representation. This is an intermediate step between
@@ -215,9 +233,9 @@
   /// [bottomUpRootsFor].
   @visibleForTesting
   void cascadeSampleCounts(T node) {
-    node.inclusiveSampleCount = node.exclusiveSampleCount;
     for (final child in node.children.cast<T>()) {
       child.exclusiveSampleCount = node.exclusiveSampleCount;
+      child.inclusiveSampleCount = node.inclusiveSampleCount;
       cascadeSampleCounts(child);
     }
   }
diff --git a/packages/devtools_app/release_notes/NEXT_RELEASE_NOTES.md b/packages/devtools_app/release_notes/NEXT_RELEASE_NOTES.md
index acfebb8..9ae7f4b 100644
--- a/packages/devtools_app/release_notes/NEXT_RELEASE_NOTES.md
+++ b/packages/devtools_app/release_notes/NEXT_RELEASE_NOTES.md
@@ -17,6 +17,7 @@
 ## CPU profiler updates
 * Add ability to inspect statistics for a CPU profile - [#5340](https://github.com/flutter/devtools/pull/5340)
 * Fix a bug where Native stack frames were missing their name - [#5344](https://github.com/flutter/devtools/pull/5344)
+* Fix an error in total and self time calculations for the bottom up tree - [#5348](https://github.com/flutter/devtools/pull/5348)
 
 ## Memory updates
 TODO: Remove this section if there are not any general updates.
diff --git a/packages/devtools_app/test/cpu_profiler/cpu_profile_model_test.dart b/packages/devtools_app/test/cpu_profiler/cpu_profile_model_test.dart
index 1157c47..75ebf57 100644
--- a/packages/devtools_app/test/cpu_profiler/cpu_profile_model_test.dart
+++ b/packages/devtools_app/test/cpu_profiler/cpu_profile_model_test.dart
@@ -10,7 +10,7 @@
 import 'package:flutter_test/flutter_test.dart';
 import 'package:vm_service/vm_service.dart';
 
-import '../test_infra/test_data/cpu_profile.dart';
+import '../test_infra/test_data/cpu_profiler/cpu_profile.dart';
 
 void main() {
   group('CpuProfileData', () {
@@ -273,32 +273,23 @@
     });
 
     test('shallowCopy', () {
-      expect(stackFrameD.children.length, equals(2));
-      expect(stackFrameD.parent, equals(stackFrameB));
-      CpuStackFrame copy =
-          stackFrameD.shallowCopy(resetInclusiveSampleCount: false);
+      expect(stackFrameD.children.length, 2);
+      expect(stackFrameD.parent, stackFrameB);
+      CpuStackFrame copy = stackFrameD.shallowCopy();
       expect(copy.children, isEmpty);
       expect(copy.parent, isNull);
-      expect(
-        copy.exclusiveSampleCount,
-        equals(stackFrameD.exclusiveSampleCount),
-      );
-      expect(
-        copy.inclusiveSampleCount,
-        equals(stackFrameD.inclusiveSampleCount),
-      );
+      expect(copy.exclusiveSampleCount, stackFrameD.exclusiveSampleCount);
+      expect(copy.inclusiveSampleCount, stackFrameD.inclusiveSampleCount);
+      expect(copy.sourceLine, stackFrameD.sourceLine);
 
-      expect(stackFrameD.children.length, equals(2));
-      expect(stackFrameD.parent, equals(stackFrameB));
-      copy = stackFrameD.shallowCopy();
+      expect(stackFrameD.children.length, 2);
+      expect(stackFrameD.parent, stackFrameB);
+      copy = stackFrameD.shallowCopy(copySampleCounts: false);
       expect(copy.children, isEmpty);
       expect(copy.parent, isNull);
-      expect(
-        copy.exclusiveSampleCount,
-        equals(stackFrameD.exclusiveSampleCount),
-      );
-      expect(copy.inclusiveSampleCount, copy.exclusiveSampleCount);
-      expect(copy.sourceLine, equals(stackFrameD.sourceLine));
+      expect(copy.exclusiveSampleCount, 0);
+      expect(copy.inclusiveSampleCount, 0);
+      expect(copy.sourceLine, stackFrameD.sourceLine);
     });
 
     test('shallowCopy overrides', () {
diff --git a/packages/devtools_app/test/cpu_profiler/cpu_profile_transformer_test.dart b/packages/devtools_app/test/cpu_profiler/cpu_profile_transformer_test.dart
index d3d8463..01fa98d 100644
--- a/packages/devtools_app/test/cpu_profiler/cpu_profile_transformer_test.dart
+++ b/packages/devtools_app/test/cpu_profiler/cpu_profile_transformer_test.dart
@@ -7,7 +7,7 @@
 import 'package:devtools_app/src/shared/profiler_utils.dart';
 import 'package:flutter_test/flutter_test.dart';
 
-import '../test_infra/test_data/cpu_profile.dart';
+import '../test_infra/test_data/cpu_profiler/cpu_profile.dart';
 
 void main() {
   group('CpuProfileTransformer', () {
@@ -28,7 +28,7 @@
       expect(cpuProfileData.processed, isTrue);
       expect(
         cpuProfileData.cpuProfileRoot.profileAsString(),
-        equals(goldenCpuProfileString),
+        goldenCpuProfileString,
       );
     });
 
@@ -71,25 +71,34 @@
     });
 
     test('cascadeSampleCounts', () {
-      void verifySampleCount(CpuStackFrame stackFrame, int targetCount) {
-        expect(stackFrame.exclusiveSampleCount, equals(0));
-        expect(stackFrame.inclusiveSampleCount, equals(0));
+      void verifySampleCount(
+        CpuStackFrame stackFrame, {
+        required int targetExcl,
+        required int targetIncl,
+      }) {
+        expect(stackFrame.exclusiveSampleCount, targetExcl);
+        expect(stackFrame.inclusiveSampleCount, targetIncl);
         for (CpuStackFrame child in stackFrame.children) {
-          verifySampleCount(child, targetCount);
+          verifySampleCount(
+            child,
+            targetExcl: targetExcl,
+            targetIncl: targetIncl,
+          );
         }
       }
 
       final stackFrame = testStackFrame.deepCopy();
       transformer.cascadeSampleCounts(stackFrame);
 
-      verifySampleCount(stackFrame, 0);
+      verifySampleCount(
+        stackFrame,
+        targetExcl: testStackFrame.exclusiveSampleCount,
+        targetIncl: testStackFrame.inclusiveSampleCount,
+      );
     });
 
     test('processData step by step', () {
-      expect(
-        testStackFrame.profileAsString(),
-        equals(testStackFrameStringGolden),
-      );
+      expect(testStackFrame.profileAsString(), testStackFrameStringGolden);
       final List<CpuStackFrame> bottomUpRoots =
           transformer.generateBottomUpRoots(
         node: testStackFrame,
@@ -98,12 +107,9 @@
       );
 
       // Verify the original stack frame was not modified.
-      expect(
-        testStackFrame.profileAsString(),
-        equals(testStackFrameStringGolden),
-      );
+      expect(testStackFrame.profileAsString(), testStackFrameStringGolden);
 
-      expect(bottomUpRoots.length, equals(6));
+      expect(bottomUpRoots.length, 6);
 
       // Set the bottom up sample counts for the roots.
       bottomUpRoots.forEach(transformer.cascadeSampleCounts);
@@ -112,50 +118,82 @@
       for (CpuStackFrame stackFrame in bottomUpRoots) {
         buf.writeln(stackFrame.profileAsString());
       }
-      expect(buf.toString(), equals(bottomUpPreMergeGolden));
+      expect(buf.toString(), bottomUpPreMergeGolden);
 
       // Merge the bottom up roots.
       mergeCpuProfileRoots(bottomUpRoots);
 
-      expect(bottomUpRoots.length, equals(4));
+      expect(bottomUpRoots.length, 4);
 
       buf.clear();
       for (CpuStackFrame stackFrame in bottomUpRoots) {
         buf.writeln(stackFrame.profileAsString());
       }
-      expect(buf.toString(), equals(bottomUpGolden));
+      expect(buf.toString(), bottomUpGolden);
+    });
+
+    test('processData step by step when skipping the root node', () {
+      final List<CpuStackFrame> bottomUpRoots =
+          transformer.generateBottomUpRoots(
+        node: testStackFrameWithRoot,
+        currentBottomUpRoot: null,
+        bottomUpRoots: [],
+        skipRoot: true,
+      );
+
+      expect(bottomUpRoots.length, 6);
+
+      // Set the bottom up sample counts for the roots.
+      bottomUpRoots.forEach(transformer.cascadeSampleCounts);
+
+      final buf = StringBuffer();
+      for (CpuStackFrame stackFrame in bottomUpRoots) {
+        buf.writeln(stackFrame.profileAsString());
+      }
+      expect(buf.toString(), bottomUpPreMergeGolden);
+
+      // Merge the bottom up roots.
+      mergeCpuProfileRoots(bottomUpRoots);
+
+      expect(bottomUpRoots.length, 4);
+
+      buf.clear();
+      for (CpuStackFrame stackFrame in bottomUpRoots) {
+        buf.writeln(stackFrame.profileAsString());
+      }
+      expect(buf.toString(), bottomUpGolden);
     });
 
     test('bottomUpRootsFor', () {
       expect(
-        testStackFrame.profileAsString(),
-        equals(testStackFrameStringGolden),
+        testStackFrameWithRoot.profileAsString(),
+        testStackFrameWithRootStringGolden,
       );
       final List<CpuStackFrame> bottomUpRoots = transformer.bottomUpRootsFor(
-        topDownRoot: testStackFrame,
+        topDownRoot: testStackFrameWithRoot,
         mergeSamples: mergeCpuProfileRoots,
         rootedAtTags: false,
       );
 
       // Verify the original stack frame was not modified.
       expect(
-        testStackFrame.profileAsString(),
-        equals(testStackFrameStringGolden),
+        testStackFrameWithRoot.profileAsString(),
+        testStackFrameWithRootStringGolden,
       );
 
-      expect(bottomUpRoots.length, equals(4));
+      expect(bottomUpRoots.length, 4);
 
       final buf = StringBuffer();
       for (CpuStackFrame stackFrame in bottomUpRoots) {
         buf.writeln(stackFrame.profileAsString());
       }
-      expect(buf.toString(), equals(bottomUpGolden));
+      expect(buf.toString(), bottomUpGolden);
     });
 
     test('bottomUpRootsFor rootedAtTags', () {
       expect(
         testTagRootedStackFrame.profileAsString(),
-        equals(testTagRootedStackFrameStringGolden),
+        testTagRootedStackFrameStringGolden,
       );
 
       // Note: this needs to be rooted at a root frame before transforming as
@@ -170,7 +208,7 @@
       // Verify the original stack frame was not modified.
       expect(
         testTagRootedStackFrame.profileAsString(),
-        equals(testTagRootedStackFrameStringGolden),
+        testTagRootedStackFrameStringGolden,
       );
 
       expect(bottomUpRoots.length, equals(1));
@@ -179,7 +217,7 @@
       for (CpuStackFrame stackFrame in bottomUpRoots) {
         buf.writeln(stackFrame.profileAsString());
       }
-      expect(buf.toString(), equals(tagRootedBottomUpGolden));
+      expect(buf.toString(), tagRootedBottomUpGolden);
     });
   });
 }
diff --git a/packages/devtools_app/test/cpu_profiler/cpu_profiler_controller_test.dart b/packages/devtools_app/test/cpu_profiler/cpu_profiler_controller_test.dart
index ccebd63..a6512bc 100644
--- a/packages/devtools_app/test/cpu_profiler/cpu_profiler_controller_test.dart
+++ b/packages/devtools_app/test/cpu_profiler/cpu_profiler_controller_test.dart
@@ -11,7 +11,7 @@
 import 'package:mockito/mockito.dart';
 import 'package:vm_service/vm_service.dart';
 
-import '../test_infra/test_data/cpu_profile.dart';
+import '../test_infra/test_data/cpu_profiler/cpu_profile.dart';
 
 void main() {
   final ServiceConnectionManager fakeServiceManager = FakeServiceManager(
diff --git a/packages/devtools_app/test/cpu_profiler/cpu_profiler_test.dart b/packages/devtools_app/test/cpu_profiler/cpu_profiler_test.dart
index 7217928..9698802 100644
--- a/packages/devtools_app/test/cpu_profiler/cpu_profiler_test.dart
+++ b/packages/devtools_app/test/cpu_profiler/cpu_profiler_test.dart
@@ -21,7 +21,7 @@
 import 'package:vm_service/vm_service.dart';
 
 import '../test_infra/matchers/matchers.dart';
-import '../test_infra/test_data/cpu_profile.dart';
+import '../test_infra/test_data/cpu_profiler/cpu_profile.dart';
 import '../test_infra/utils/test_utils.dart';
 
 void main() {
diff --git a/packages/devtools_app/test/performance/performance_model_test.dart b/packages/devtools_app/test/performance/performance_model_test.dart
index 401aa3a..2428acf 100644
--- a/packages/devtools_app/test/performance/performance_model_test.dart
+++ b/packages/devtools_app/test/performance/performance_model_test.dart
@@ -9,7 +9,7 @@
 import 'package:devtools_app/src/screens/performance/panes/rebuild_stats/rebuild_stats_model.dart';
 import 'package:flutter_test/flutter_test.dart';
 
-import '../test_infra/test_data/cpu_profile.dart';
+import '../test_infra/test_data/cpu_profiler/cpu_profile.dart';
 import '../test_infra/test_data/performance.dart';
 import '../test_infra/test_data/performance_raster_stats.dart';
 
diff --git a/packages/devtools_app/test/shared/flame_chart_test.dart b/packages/devtools_app/test/shared/flame_chart_test.dart
index cab7ed1..68761e2 100644
--- a/packages/devtools_app/test/shared/flame_chart_test.dart
+++ b/packages/devtools_app/test/shared/flame_chart_test.dart
@@ -15,7 +15,7 @@
 import 'package:flutter_test/flutter_test.dart';
 import 'package:mockito/mockito.dart';
 
-import '../test_infra/test_data/cpu_profile.dart';
+import '../test_infra/test_data/cpu_profiler/cpu_profile.dart';
 import '../test_infra/test_data/performance.dart';
 
 void main() {
diff --git a/packages/devtools_app/test/test_infra/goldens/cpu_profiler_bottom_up_guidelines.png b/packages/devtools_app/test/test_infra/goldens/cpu_profiler_bottom_up_guidelines.png
index c9c0f5a..1f8f2b9 100644
--- a/packages/devtools_app/test/test_infra/goldens/cpu_profiler_bottom_up_guidelines.png
+++ b/packages/devtools_app/test/test_infra/goldens/cpu_profiler_bottom_up_guidelines.png
Binary files differ
diff --git a/packages/devtools_app/test/test_infra/goldens/cpu_profiler_bottom_up_no_guidelines.png b/packages/devtools_app/test/test_infra/goldens/cpu_profiler_bottom_up_no_guidelines.png
index df7109f..46c6252 100644
--- a/packages/devtools_app/test/test_infra/goldens/cpu_profiler_bottom_up_no_guidelines.png
+++ b/packages/devtools_app/test/test_infra/goldens/cpu_profiler_bottom_up_no_guidelines.png
Binary files differ
diff --git a/packages/devtools_app/test/test_infra/scenes/cpu_profiler/default.dart b/packages/devtools_app/test/test_infra/scenes/cpu_profiler/default.dart
index 4395129..c31cc6c 100644
--- a/packages/devtools_app/test/test_infra/scenes/cpu_profiler/default.dart
+++ b/packages/devtools_app/test/test_infra/scenes/cpu_profiler/default.dart
@@ -12,7 +12,7 @@
 import 'package:stager/stager.dart';
 import 'package:vm_service/vm_service.dart';
 
-import '../../test_data/cpu_profile.dart';
+import '../../test_data/cpu_profiler/cpu_profile.dart';
 
 /// To run:
 /// flutter run -t test/test_infra/scenes/cpu_profiler/default.stager_app.dart -d macos
diff --git a/packages/devtools_app/test/test_infra/test_data/cpu_profile.dart b/packages/devtools_app/test/test_infra/test_data/cpu_profiler/cpu_profile.dart
similarity index 94%
rename from packages/devtools_app/test/test_infra/test_data/cpu_profile.dart
rename to packages/devtools_app/test/test_infra/test_data/cpu_profiler/cpu_profile.dart
index 5899038..4518f21 100644
--- a/packages/devtools_app/test/test_infra/test_data/cpu_profile.dart
+++ b/packages/devtools_app/test/test_infra/test_data/cpu_profiler/cpu_profile.dart
@@ -1367,6 +1367,9 @@
   isTag: false,
 )..exclusiveSampleCount = 1;
 
+final CpuStackFrame testStackFrameWithRoot = CpuStackFrame.root(profileMetaData)
+  ..addChild(testStackFrame.deepCopy());
+
 final CpuStackFrame testStackFrame = stackFrameA
   ..addChild(
     stackFrameB
@@ -1380,9 +1383,22 @@
 
 final CpuStackFrame testTagRootedStackFrame = tagFrameA
   ..addChild(
-    testStackFrame,
+    testStackFrame.deepCopy(),
   );
 
+const String testStackFrameWithRootStringGolden = '''
+  all - children: 1 - excl: 0 - incl: 10
+    A - children: 1 - excl: 0 - incl: 10
+      B - children: 2 - excl: 0 - incl: 10
+        C - children: 0 - excl: 2 - incl: 2
+        D - children: 2 - excl: 2 - incl: 8
+          E - children: 1 - excl: 1 - incl: 2
+            F - children: 1 - excl: 0 - incl: 1
+              C - children: 0 - excl: 1 - incl: 1
+          F - children: 1 - excl: 3 - incl: 4
+            C - children: 0 - excl: 1 - incl: 1
+''';
+
 const String testStackFrameStringGolden = '''
   A - children: 1 - excl: 0 - incl: 10
     B - children: 2 - excl: 0 - incl: 10
@@ -1400,14 +1416,14 @@
     B - children: 1 - excl: 2 - incl: 2
       A - children: 0 - excl: 2 - incl: 2
 
-  D - children: 1 - excl: 2 - incl: 2
-    B - children: 1 - excl: 2 - incl: 2
-      A - children: 0 - excl: 2 - incl: 2
+  D - children: 1 - excl: 2 - incl: 8
+    B - children: 1 - excl: 2 - incl: 8
+      A - children: 0 - excl: 2 - incl: 8
 
-  E - children: 1 - excl: 1 - incl: 1
-    D - children: 1 - excl: 1 - incl: 1
-      B - children: 1 - excl: 1 - incl: 1
-        A - children: 0 - excl: 1 - incl: 1
+  E - children: 1 - excl: 1 - incl: 2
+    D - children: 1 - excl: 1 - incl: 2
+      B - children: 1 - excl: 1 - incl: 2
+        A - children: 0 - excl: 1 - incl: 2
 
   C - children: 1 - excl: 1 - incl: 1
     F - children: 1 - excl: 1 - incl: 1
@@ -1416,10 +1432,10 @@
           B - children: 1 - excl: 1 - incl: 1
             A - children: 0 - excl: 1 - incl: 1
 
-  F - children: 1 - excl: 3 - incl: 3
-    D - children: 1 - excl: 3 - incl: 3
-      B - children: 1 - excl: 3 - incl: 3
-        A - children: 0 - excl: 3 - incl: 3
+  F - children: 1 - excl: 3 - incl: 4
+    D - children: 1 - excl: 3 - incl: 4
+      B - children: 1 - excl: 3 - incl: 4
+        A - children: 0 - excl: 3 - incl: 4
 
   C - children: 1 - excl: 1 - incl: 1
     F - children: 1 - excl: 1 - incl: 1
@@ -1442,19 +1458,19 @@
         B - children: 1 - excl: 1 - incl: 1
           A - children: 0 - excl: 1 - incl: 1
 
-  D - children: 1 - excl: 2 - incl: 2
-    B - children: 1 - excl: 2 - incl: 2
-      A - children: 0 - excl: 2 - incl: 2
+  D - children: 1 - excl: 2 - incl: 8
+    B - children: 1 - excl: 2 - incl: 8
+      A - children: 0 - excl: 2 - incl: 8
 
-  E - children: 1 - excl: 1 - incl: 1
-    D - children: 1 - excl: 1 - incl: 1
-      B - children: 1 - excl: 1 - incl: 1
-        A - children: 0 - excl: 1 - incl: 1
+  E - children: 1 - excl: 1 - incl: 2
+    D - children: 1 - excl: 1 - incl: 2
+      B - children: 1 - excl: 1 - incl: 2
+        A - children: 0 - excl: 1 - incl: 2
 
-  F - children: 1 - excl: 3 - incl: 3
-    D - children: 1 - excl: 3 - incl: 3
-      B - children: 1 - excl: 3 - incl: 3
-        A - children: 0 - excl: 3 - incl: 3
+  F - children: 1 - excl: 3 - incl: 4
+    D - children: 1 - excl: 3 - incl: 4
+      B - children: 1 - excl: 3 - incl: 4
+        A - children: 0 - excl: 3 - incl: 4
 
 ''';
 
@@ -1484,17 +1500,17 @@
         D - children: 1 - excl: 1 - incl: 1
           B - children: 1 - excl: 1 - incl: 1
             A - children: 0 - excl: 1 - incl: 1
-    D - children: 1 - excl: 2 - incl: 2
-      B - children: 1 - excl: 2 - incl: 2
-        A - children: 0 - excl: 2 - incl: 2
-    E - children: 1 - excl: 1 - incl: 1
-      D - children: 1 - excl: 1 - incl: 1
-        B - children: 1 - excl: 1 - incl: 1
-          A - children: 0 - excl: 1 - incl: 1
-    F - children: 1 - excl: 3 - incl: 3
-      D - children: 1 - excl: 3 - incl: 3
-        B - children: 1 - excl: 3 - incl: 3
-          A - children: 0 - excl: 3 - incl: 3
+    D - children: 1 - excl: 2 - incl: 8
+      B - children: 1 - excl: 2 - incl: 8
+        A - children: 0 - excl: 2 - incl: 8
+    E - children: 1 - excl: 1 - incl: 2
+      D - children: 1 - excl: 1 - incl: 2
+        B - children: 1 - excl: 1 - incl: 2
+          A - children: 0 - excl: 1 - incl: 2
+    F - children: 1 - excl: 3 - incl: 4
+      D - children: 1 - excl: 3 - incl: 4
+        B - children: 1 - excl: 3 - incl: 4
+          A - children: 0 - excl: 3 - incl: 4
 
 ''';
 
diff --git a/packages/devtools_app/test/test_infra/test_data/performance.dart b/packages/devtools_app/test/test_infra/test_data/performance.dart
index 930bb17..c7b3db0 100644
--- a/packages/devtools_app/test/test_infra/test_data/performance.dart
+++ b/packages/devtools_app/test/test_infra/test_data/performance.dart
@@ -7,7 +7,7 @@
 import 'package:devtools_app/devtools_app.dart';
 
 import '../../test_infra/utils/test_utils.dart';
-import 'cpu_profile.dart';
+import 'cpu_profiler/cpu_profile.dart';
 import 'performance_raster_stats.dart';
 
 const testUiThreadId = 1;