Make timeline flame chart section labels sticky (#2079)

diff --git a/packages/devtools_app/lib/src/charts/flame_chart.dart b/packages/devtools_app/lib/src/charts/flame_chart.dart
index 2c2c3f5..44657f0 100644
--- a/packages/devtools_app/lib/src/charts/flame_chart.dart
+++ b/packages/devtools_app/lib/src/charts/flame_chart.dart
@@ -501,9 +501,13 @@
       child: GestureDetector(
         behavior: HitTestBehavior.opaque,
         onTapUp: _handleTapUp,
-        child: SizedBox(
+        child: Container(
           height: rowHeightWithPadding,
           width: widget.width,
+          color: alternatingColorForIndexWithContext(
+            nodes.first.sectionIndex,
+            context,
+          ),
           // TODO(kenz): investigate if `addAutomaticKeepAlives: false` and
           // `addRepaintBoundaries: false` are needed here for perf improvement.
           child: ExtentDelegateListView(
@@ -756,19 +760,6 @@
     this.sectionIndex,
   });
 
-  FlameChartNode.sectionLabel({
-    this.key,
-    @required this.text,
-    @required this.textColor,
-    @required this.backgroundColor,
-    @required double top,
-    @required double width,
-  })  : rect = Rect.fromLTRB(rowPadding, top, width, top + rowHeight),
-        tooltip = text,
-        data = null,
-        onSelected = ((_) {}),
-        selectable = false;
-
   static const _selectedTextColor = Colors.black;
   // We would like this value to be smaller, but zoom performance does not allow
   // for that. We should decrease this value if we can improve flame chart zoom
diff --git a/packages/devtools_app/lib/src/common_widgets.dart b/packages/devtools_app/lib/src/common_widgets.dart
index d0620dd..64c1411 100644
--- a/packages/devtools_app/lib/src/common_widgets.dart
+++ b/packages/devtools_app/lib/src/common_widgets.dart
@@ -767,3 +767,23 @@
 
   TextStyle get selectedTextStyle => TextStyle(color: textSelectionColor);
 }
+
+/// Gets an alternating color to use for indexed UI elements.
+Color alternatingColorForIndexWithContext(int index, BuildContext context) {
+  final theme = Theme.of(context);
+  final color = theme.canvasColor;
+  return _colorForIndex(color, index, isDarkTheme: theme.isDarkTheme);
+}
+
+Color alternatingColorForIndex(int index, {@required bool isDarkTheme}) {
+  final color = defaultBackgroundColor;
+  return _colorForIndex(color, index, isDarkTheme: isDarkTheme);
+}
+
+Color _colorForIndex(Color color, int index, {@required bool isDarkTheme}) {
+  if (index % 2 == 1) {
+    return color;
+  } else {
+    return isDarkTheme ? color.brighten() : color.darken();
+  }
+}
diff --git a/packages/devtools_app/lib/src/profiler/cpu_profile_flame_chart.dart b/packages/devtools_app/lib/src/profiler/cpu_profile_flame_chart.dart
index b214aa6..c841db3 100644
--- a/packages/devtools_app/lib/src/profiler/cpu_profile_flame_chart.dart
+++ b/packages/devtools_app/lib/src/profiler/cpu_profile_flame_chart.dart
@@ -62,7 +62,7 @@
         textColor: Colors.black,
         data: stackFrame,
         onSelected: (dynamic frame) => widget.onSelected(frame),
-      );
+      )..sectionIndex = 0;
 
       rows[row].addNode(node);
 
diff --git a/packages/devtools_app/lib/src/split.dart b/packages/devtools_app/lib/src/split.dart
index 4dc40fd..ef14dd8 100644
--- a/packages/devtools_app/lib/src/split.dart
+++ b/packages/devtools_app/lib/src/split.dart
@@ -290,9 +290,7 @@
 }
 
 class DefaultSplitter extends StatelessWidget {
-  const DefaultSplitter({
-    @required this.isHorizontal,
-  });
+  const DefaultSplitter({@required this.isHorizontal});
 
   static const double size = 24.0;
 
diff --git a/packages/devtools_app/lib/src/table.dart b/packages/devtools_app/lib/src/table.dart
index da78179..135613e 100644
--- a/packages/devtools_app/lib/src/table.dart
+++ b/packages/devtools_app/lib/src/table.dart
@@ -6,7 +6,7 @@
 import 'auto_dispose_mixin.dart';
 import 'collapsible_mixin.dart';
 import 'common_widgets.dart';
-import 'common_widgets.dart' show ColorExtension, ScrollControllerAutoScroll;
+import 'common_widgets.dart' show ScrollControllerAutoScroll;
 import 'flutter_widgets/linked_scroll_controller.dart';
 import 'table_data.dart';
 import 'theme.dart';
@@ -153,7 +153,7 @@
       onPressed: widget.onItemSelected,
       columns: widget.columns,
       columnWidths: columnWidths,
-      backgroundColor: TableRow.colorFor(context, index),
+      backgroundColor: alternatingColorForIndexWithContext(index, context),
     );
   }
 
@@ -445,7 +445,7 @@
         onPressed: (item) => _onItemPressed(item, index),
         backgroundColor: isNodeSelected
             ? Theme.of(context).selectedRowColor
-            : TableRow.colorFor(context, index),
+            : alternatingColorForIndexWithContext(index, context),
         columns: widget.columns,
         columnWidths: columnWidths,
         expandableColumn: widget.treeColumn,
@@ -916,18 +916,6 @@
 
   final Function(ColumnData<T> column, SortDirection direction) onSortChanged;
 
-  /// Gets a color to use for this row at a given index.
-  static Color colorFor(BuildContext context, int index) {
-    final theme = Theme.of(context);
-    final color = theme.canvasColor;
-
-    if (index % 2 == 1) {
-      return color;
-    } else {
-      return theme.isDarkTheme ? color.brighten() : color.darken();
-    }
-  }
-
   @override
   _TableRowState<T> createState() => _TableRowState<T>();
 }
diff --git a/packages/devtools_app/lib/src/timeline/timeline_flame_chart.dart b/packages/devtools_app/lib/src/timeline/timeline_flame_chart.dart
index fe3ea22..53bd8bc 100644
--- a/packages/devtools_app/lib/src/timeline/timeline_flame_chart.dart
+++ b/packages/devtools_app/lib/src/timeline/timeline_flame_chart.dart
@@ -1,6 +1,7 @@
 // Copyright 2019 The Chromium Authors. All rights reserved.
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
+import 'dart:collection';
 import 'dart:math' as math;
 import 'dart:ui';
 
@@ -10,6 +11,7 @@
 import 'package:provider/provider.dart';
 
 import '../charts/flame_chart.dart';
+import '../common_widgets.dart';
 import '../geometry.dart';
 import '../theme.dart';
 import '../ui/colors.dart';
@@ -18,6 +20,11 @@
 import 'timeline_controller.dart';
 import 'timeline_model.dart';
 
+// TODO(kenz): for sections with more than one row deep, add empty row for
+// section label.
+
+// TODO(kenz): make flame chart sections collapsible.
+
 class TimelineFlameChart extends FlameChart<TimelineData, TimelineEvent> {
   TimelineFlameChart(
     TimelineData data, {
@@ -34,15 +41,18 @@
         );
 
   static double _calculateStartInset(TimelineData data) {
-    // TODO(kenz): we need to calculate start inset based on the width of the
-    // section labels. We should also set a max, ellipsize, and rely on tooltip
-    // to give the full name in the event that the section name exceeds max.
-    //
-    // Alternatively, we could make the label section a column of it's own that
-    // is resizeable. It would need to link scroll controllers with the list
-    // view holding the flame chart nodes. This would make section labels sticky
-    // to the left as an inherent bonus.
-    return 140.0;
+    const spaceFor0msText = 55.0;
+    const maxStartInset = 300.0;
+    var maxMeasuredWidth = 0.0;
+    for (String groupName in data.eventGroups.keys) {
+      final textPainter = TextPainter(
+        text: TextSpan(text: groupName),
+        textDirection: TextDirection.ltr,
+      )..layout();
+      maxMeasuredWidth =
+          math.max(maxMeasuredWidth, textPainter.width + 2 * densePadding);
+    }
+    return math.min(maxStartInset, maxMeasuredWidth) + spaceFor0msText;
   }
 
   /// Offset for drawing async guidelines.
@@ -232,32 +242,6 @@
       );
       sections.add(section);
 
-      // Add section label node.
-      Color sectionLabelBackgroundColor;
-      switch (groupName) {
-        case TimelineData.uiKey:
-          sectionLabelBackgroundColor = mainUiColor;
-          break;
-        case TimelineData.rasterKey:
-          sectionLabelBackgroundColor = mainRasterColor;
-          break;
-        case TimelineData.unknownKey:
-          sectionLabelBackgroundColor = mainUnknownColor;
-          break;
-        default:
-          sectionLabelBackgroundColor = mainAsyncColor;
-      }
-
-      final currentSectionLabel = FlameChartNode.sectionLabel(
-        text: groupName,
-        textColor: Colors.black,
-        backgroundColor: sectionLabelBackgroundColor,
-        top: flameChartNodeTop,
-        width: 120.0,
-      );
-
-      rows[currentRowIndex].addNode(currentSectionLabel, index: 0);
-
       // Increment for next section.
       currentRowIndex += group.displaySize;
       currentSectionIndex++;
@@ -316,6 +300,17 @@
               _calculateVerticalGuidelineStartY(event) - rowHeight,
         ),
       ),
+      CustomPaint(
+        painter: SectionLabelPainter(
+          _timelineController.data.eventGroups,
+          isDarkTheme: Theme.of(context).isDarkTheme,
+          zoom: zoom,
+          constraints: constraints,
+          verticalScrollOffset: verticalScrollOffset,
+          horizontalScrollOffset: horizontalScrollOffset,
+          chartStartInset: widget.startInset,
+        ),
+      ),
     ];
   }
 
@@ -452,6 +447,88 @@
   }
 }
 
+class SectionLabelPainter extends FlameChartPainter {
+  SectionLabelPainter(
+    this.eventGroups, {
+    @required this.isDarkTheme,
+    @required double zoom,
+    @required BoxConstraints constraints,
+    @required double verticalScrollOffset,
+    @required double horizontalScrollOffset,
+    @required double chartStartInset,
+  }) : super(
+          zoom: zoom,
+          constraints: constraints,
+          verticalScrollOffset: verticalScrollOffset,
+          horizontalScrollOffset: horizontalScrollOffset,
+          chartStartInset: chartStartInset,
+        );
+
+  final bool isDarkTheme;
+
+  final SplayTreeMap<String, TimelineEventGroup> eventGroups;
+
+  @override
+  void paint(Canvas canvas, Size size) {
+    canvas.clipRect(Rect.fromLTWH(
+      0.0,
+      rowHeight, // We do not want to paint inside the timestamp section.
+      constraints.maxWidth,
+      constraints.maxHeight - rowHeight,
+    ));
+
+    // Start at row height to account for timestamps at top of chart.
+    var startSectionPx = sectionSpacing * 2;
+    for (String groupName in eventGroups.keys) {
+      final group = eventGroups[groupName];
+      final labelTop = startSectionPx - verticalScrollOffset;
+
+      final textPainter = TextPainter(
+        text: TextSpan(
+          text: groupName,
+          style: const TextStyle(color: chartTextColor),
+        ),
+        textDirection: TextDirection.ltr,
+      )..layout();
+
+      final labelWidth = textPainter.width + 2 * densePadding;
+      final backgroundColor = alternatingColorForIndex(
+        eventGroups.values.toList().indexOf(group),
+        isDarkTheme: isDarkTheme,
+      );
+      final backgroundWithAlpha = Color.fromRGBO(
+        backgroundColor.red,
+        backgroundColor.green,
+        backgroundColor.blue,
+        0.75,
+      );
+
+      canvas.drawRect(
+        Rect.fromLTWH(
+          0.0,
+          labelTop,
+          labelWidth,
+          rowHeightWithPadding,
+        ),
+        Paint()..color = backgroundWithAlpha,
+      );
+
+      textPainter.paint(
+        canvas,
+        Offset(densePadding, labelTop + rowPadding),
+      );
+
+      startSectionPx += group.displaySizePx;
+    }
+  }
+
+  @override
+  bool shouldRepaint(SectionLabelPainter oldDelegate) {
+    return verticalScrollOffset != oldDelegate.verticalScrollOffset ||
+        eventGroups != oldDelegate.eventGroups;
+  }
+}
+
 class AsyncGuidelinePainter extends FlameChartPainter {
   AsyncGuidelinePainter({
     @required double zoom,
@@ -596,10 +673,6 @@
 
   static const baseGridIntervalPx = 150.0;
   static const timestampOffset = 6.0;
-  static const timestampColor = ThemedColor(
-    Color(0xFF24292E),
-    Color(0xFFFAFBFC),
-  );
 
   final double chartEndInset;
 
@@ -655,7 +728,7 @@
     final textPainter = TextPainter(
       text: TextSpan(
         text: timestampText,
-        style: const TextStyle(color: timestampColor),
+        style: const TextStyle(color: chartTextColor),
       ),
       textAlign: TextAlign.right,
       textDirection: TextDirection.ltr,
diff --git a/packages/devtools_app/test/flame_chart_test.dart b/packages/devtools_app/test/flame_chart_test.dart
index 81c1ff3..0839999 100644
--- a/packages/devtools_app/test/flame_chart_test.dart
+++ b/packages/devtools_app/test/flame_chart_test.dart
@@ -151,15 +151,15 @@
     final linkedScrollControllerGroup = LinkedScrollControllerGroup();
     final testRow = ScrollingFlameChartRow(
       linkedScrollControllerGroup: linkedScrollControllerGroup,
-      nodes: testNodesWithLabel,
-      width: 610.0, // 610.0 fits all test nodes and sideInsets of 70.0.
+      nodes: testNodes,
+      width: 680.0, // 680.0 fits all test nodes and sideInsets of 70.0.
       startInset: sideInset,
       selected: null,
       zoom: FlameChart.minZoomLevel,
     );
     final zoomedTestRow = ScrollingFlameChartRow(
       linkedScrollControllerGroup: linkedScrollControllerGroup,
-      nodes: testNodesWithLabel,
+      nodes: testNodes,
       // 1080.0 fits all test nodes at zoom level 2.0 and sideInsets of 70.0.
       width: 1080.0,
       startInset: sideInset,
@@ -200,9 +200,8 @@
       expect(find.byWidget(currentRow), findsOneWidget);
       expect(find.byType(MouseRegion), findsOneWidget);
 
-      final sizedBoxFinder = find.byType(SizedBox);
-      final SizedBox box = tester.firstWidget(sizedBoxFinder);
-      expect(box.height, equals(rowHeightWithPadding));
+      // 1 for row container and 4 for node containers.
+      expect(tester.widgetList(find.byType(Container)).length, equals(5));
     });
 
     testWidgets('builds for empty nodes list', (WidgetTester tester) async {
@@ -228,7 +227,6 @@
         (WidgetTester tester) async {
       final rowState = await pumpRowAndGetState(tester);
       expect(rowState.binarySearchForNode(-10.0), isNull);
-      expect(rowState.binarySearchForNode(10.0), equals(labelNode));
       expect(rowState.binarySearchForNode(49.0), isNull);
       expect(rowState.binarySearchForNode(70.0), equals(testNode));
       expect(rowState.binarySearchForNode(120.0), equals(testNode2));
@@ -241,7 +239,6 @@
         (WidgetTester tester) async {
       final rowState = await pumpRowAndGetState(tester, row: zoomedTestRow);
       expect(rowState.binarySearchForNode(-10.0), isNull);
-      expect(rowState.binarySearchForNode(10.0), equals(labelNode));
       expect(rowState.binarySearchForNode(49.0), isNull);
       expect(rowState.binarySearchForNode(70.0), equals(testNode));
       expect(rowState.binarySearchForNode(130.0), equals(testNode));
@@ -417,26 +414,9 @@
   });
 
   group('NodeListExtension', () {
-    test(
-        'toPaddedZoomedIntervals calculation is accurate for unzoomed row with'
-        ' label', () {
-      final paddedZoomedIntervals = testNodesWithLabel.toPaddedZoomedIntervals(
-        zoom: 1.0,
-        chartStartInset: sideInset,
-        chartWidth: 610.0,
-      );
-      expect(paddedZoomedIntervals[0], equals(const Range(0.0, 70.0)));
-      expect(paddedZoomedIntervals[1], equals(const Range(70.0, 120.0)));
-      expect(paddedZoomedIntervals[2], equals(const Range(120.0, 180.0)));
-      expect(paddedZoomedIntervals[3], equals(const Range(180.0, 240.0)));
-      expect(paddedZoomedIntervals[4], equals(const Range(240.0, 1000540.0)));
-    });
-
-    test(
-        'toPaddedZoomedIntervals calculation is accurate for unzoomed row '
-        'without label', () {
-      final paddedZoomedIntervals =
-          testNodesWithoutLabel.toPaddedZoomedIntervals(
+    test('toPaddedZoomedIntervals calculation is accurate for unzoomed row',
+        () {
+      final paddedZoomedIntervals = testNodes.toPaddedZoomedIntervals(
         zoom: 1.0,
         chartStartInset: sideInset,
         chartWidth: 610.0,
@@ -447,26 +427,8 @@
       expect(paddedZoomedIntervals[3], equals(const Range(240.0, 1000540.0)));
     });
 
-    test(
-        'toPaddedZoomedIntervals calculation is accurate for zoomed row with '
-        'label', () {
-      final paddedZoomedIntervals = testNodesWithLabel.toPaddedZoomedIntervals(
-        zoom: 2.0,
-        chartStartInset: sideInset,
-        chartWidth: 1080.0,
-      );
-      expect(paddedZoomedIntervals[0], equals(const Range(0.0, 70.0)));
-      expect(paddedZoomedIntervals[1], equals(const Range(70.0, 170.0)));
-      expect(paddedZoomedIntervals[2], equals(const Range(170.0, 290.0)));
-      expect(paddedZoomedIntervals[3], equals(const Range(290.0, 410.0)));
-      expect(paddedZoomedIntervals[4], equals(const Range(410.0, 1001010.0)));
-    });
-
-    test(
-        'toPaddedZoomedIntervals calculation is accurate for zoomed row without'
-        ' label', () {
-      final paddedZoomedIntervals =
-          testNodesWithoutLabel.toPaddedZoomedIntervals(
+    test('toPaddedZoomedIntervals calculation is accurate for zoomed row', () {
+      final paddedZoomedIntervals = testNodes.toPaddedZoomedIntervals(
         zoom: 2.0,
         chartStartInset: sideInset,
         chartWidth: 1080.0,
@@ -480,137 +442,82 @@
 
   group('FlameChartUtils', () {
     test('leftPaddingForNode returns correct value for un-zoomed row', () {
-      // Row with label.
       expect(
-          FlameChartUtils.leftPaddingForNode(0, testNodesWithLabel,
-              chartZoom: 1.0, chartStartInset: sideInset),
-          equals(2.0));
-      expect(
-          FlameChartUtils.leftPaddingForNode(1, testNodesWithLabel,
-              chartZoom: 1.0, chartStartInset: sideInset),
-          equals(0.0));
-      expect(
-          FlameChartUtils.leftPaddingForNode(2, testNodesWithLabel,
-              chartZoom: 1.0, chartStartInset: sideInset),
-          equals(0.0));
-      expect(
-          FlameChartUtils.leftPaddingForNode(3, testNodesWithLabel,
-              chartZoom: 1.0, chartStartInset: sideInset),
-          equals(0.0));
-      expect(
-          FlameChartUtils.leftPaddingForNode(4, testNodesWithLabel,
-              chartZoom: 1.0, chartStartInset: sideInset),
-          equals(0.0));
-
-      // Row without label.
-      expect(
-          FlameChartUtils.leftPaddingForNode(0, testNodesWithoutLabel,
+          FlameChartUtils.leftPaddingForNode(0, testNodes,
               chartZoom: 1.0, chartStartInset: sideInset),
           equals(70.0));
       expect(
-          FlameChartUtils.leftPaddingForNode(1, testNodesWithoutLabel,
+          FlameChartUtils.leftPaddingForNode(1, testNodes,
               chartZoom: 1.0, chartStartInset: sideInset),
           equals(0.0));
       expect(
-          FlameChartUtils.leftPaddingForNode(2, testNodesWithoutLabel,
+          FlameChartUtils.leftPaddingForNode(2, testNodes,
               chartZoom: 1.0, chartStartInset: sideInset),
           equals(0.0));
       expect(
-          FlameChartUtils.leftPaddingForNode(3, testNodesWithoutLabel,
+          FlameChartUtils.leftPaddingForNode(3, testNodes,
               chartZoom: 1.0, chartStartInset: sideInset),
           equals(0.0));
     });
 
     test('rightPaddingForNode returns correct value for un-zoomed row', () {
-      // Row with label.
       expect(
-          FlameChartUtils.rightPaddingForNode(0, testNodesWithLabel,
-              chartZoom: 1.0, chartStartInset: sideInset, chartWidth: 610.0),
-          equals(48.0));
-      expect(
-          FlameChartUtils.rightPaddingForNode(1, testNodesWithLabel,
+          FlameChartUtils.rightPaddingForNode(0, testNodes,
               chartZoom: 1.0, chartStartInset: sideInset, chartWidth: 610.0),
           equals(20.0));
       expect(
-          FlameChartUtils.rightPaddingForNode(2, testNodesWithLabel,
+          FlameChartUtils.rightPaddingForNode(1, testNodes,
               chartZoom: 1.0, chartStartInset: sideInset, chartWidth: 610.0),
           equals(10.0));
       expect(
-          FlameChartUtils.rightPaddingForNode(3, testNodesWithLabel,
+          FlameChartUtils.rightPaddingForNode(2, testNodes,
               chartZoom: 1.0, chartStartInset: sideInset, chartWidth: 610.0),
           equals(10.0));
       expect(
-          FlameChartUtils.rightPaddingForNode(4, testNodesWithLabel,
-              chartZoom: 1.0, chartStartInset: sideInset, chartWidth: 610.0),
-          equals(1000000.0));
-
-      // Row without label.
-      expect(
-          FlameChartUtils.rightPaddingForNode(0, testNodesWithoutLabel,
-              chartZoom: 1.0, chartStartInset: sideInset, chartWidth: 610.0),
-          equals(20.0));
-      expect(
-          FlameChartUtils.rightPaddingForNode(1, testNodesWithoutLabel,
-              chartZoom: 1.0, chartStartInset: sideInset, chartWidth: 610.0),
-          equals(10.0));
-      expect(
-          FlameChartUtils.rightPaddingForNode(2, testNodesWithoutLabel,
-              chartZoom: 1.0, chartStartInset: sideInset, chartWidth: 610.0),
-          equals(10.0));
-      expect(
-          FlameChartUtils.rightPaddingForNode(3, testNodesWithoutLabel,
+          FlameChartUtils.rightPaddingForNode(3, testNodes,
               chartZoom: 1.0, chartStartInset: sideInset, chartWidth: 610.0),
           equals(1000000.0));
     });
 
     test('leftPaddingForNode returns correct value for zoomed row', () {
       expect(
-          FlameChartUtils.leftPaddingForNode(0, testNodesWithLabel,
+          FlameChartUtils.leftPaddingForNode(0, testNodes,
               chartZoom: 2.0, chartStartInset: sideInset),
-          equals(2.0));
+          equals(70.0));
       expect(
-          FlameChartUtils.leftPaddingForNode(1, testNodesWithLabel,
+          FlameChartUtils.leftPaddingForNode(1, testNodes,
               chartZoom: 2.0, chartStartInset: sideInset),
           equals(0.0));
       expect(
-          FlameChartUtils.leftPaddingForNode(2, testNodesWithLabel,
+          FlameChartUtils.leftPaddingForNode(2, testNodes,
               chartZoom: 2.0, chartStartInset: sideInset),
           equals(0.0));
       expect(
-          FlameChartUtils.leftPaddingForNode(3, testNodesWithLabel,
-              chartZoom: 2.0, chartStartInset: sideInset),
-          equals(0.0));
-      expect(
-          FlameChartUtils.leftPaddingForNode(4, testNodesWithLabel,
+          FlameChartUtils.leftPaddingForNode(3, testNodes,
               chartZoom: 2.0, chartStartInset: sideInset),
           equals(0.0));
     });
 
     test('rightPaddingForNode returns correct value for zoomed row', () {
       expect(
-          FlameChartUtils.rightPaddingForNode(0, testNodesWithLabel,
-              chartZoom: 2.0, chartStartInset: sideInset, chartWidth: 1080.0),
-          equals(48.0));
-      expect(
-          FlameChartUtils.rightPaddingForNode(1, testNodesWithLabel,
+          FlameChartUtils.rightPaddingForNode(0, testNodes,
               chartZoom: 2.0, chartStartInset: sideInset, chartWidth: 1080.0),
           equals(40.0));
       expect(
-          FlameChartUtils.rightPaddingForNode(2, testNodesWithLabel,
+          FlameChartUtils.rightPaddingForNode(1, testNodes,
               chartZoom: 2.0, chartStartInset: sideInset, chartWidth: 1080.0),
           equals(20.0));
       expect(
-          FlameChartUtils.rightPaddingForNode(3, testNodesWithLabel,
+          FlameChartUtils.rightPaddingForNode(2, testNodes,
               chartZoom: 2.0, chartStartInset: sideInset, chartWidth: 1080.0),
           equals(20.0));
       expect(
-          FlameChartUtils.rightPaddingForNode(4, testNodesWithLabel,
+          FlameChartUtils.rightPaddingForNode(3, testNodes,
               chartZoom: 2.0, chartStartInset: sideInset, chartWidth: 1080.0),
           equals(1000000.0));
     });
 
     test('zoomForNode returns correct values', () {
-      expect(FlameChartUtils.zoomForNode(labelNode, 2.0), equals(1.0));
       expect(FlameChartUtils.zoomForNode(testNode, 3.0), equals(3.0));
       expect(FlameChartUtils.zoomForNode(testNode4, 10.0), equals(10.0));
     });
@@ -627,21 +534,7 @@
   textColor: Colors.white,
   data: goldenAsyncTimelineEvent,
   onSelected: (_) {},
-);
-
-const labelNodeKey = Key('label node');
-final labelNode = FlameChartNode<TimelineEvent>(
-  key: labelNodeKey,
-  text: 'label test node',
-  tooltip: 'label test node',
-  // 30.0 is the minimum node width for text.
-  rect: const Rect.fromLTWH(2.0, 0.0, 20.0, rowHeight),
-  backgroundColor: Colors.blue,
-  textColor: Colors.white,
-  data: goldenAsyncTimelineEvent,
-  selectable: false,
-  onSelected: (_) {},
-);
+)..sectionIndex = 0;
 
 const Key testNodeKey = Key('test node');
 final testNode = FlameChartNode<TimelineEvent>(
@@ -654,7 +547,7 @@
   textColor: Colors.white,
   data: goldenAsyncTimelineEvent,
   onSelected: (_) {},
-);
+)..sectionIndex = 0;
 
 final testNode2 = FlameChartNode<TimelineEvent>(
   key: narrowNodeKey,
@@ -665,7 +558,7 @@
   textColor: Colors.white,
   data: goldenAsyncTimelineEvent,
   onSelected: (_) {},
-);
+)..sectionIndex = 0;
 
 final testNode3 = FlameChartNode<TimelineEvent>(
   key: narrowNodeKey,
@@ -676,7 +569,7 @@
   textColor: Colors.white,
   data: goldenAsyncTimelineEvent,
   onSelected: (_) {},
-);
+)..sectionIndex = 0;
 
 final testNode4 = FlameChartNode<TimelineEvent>(
   key: narrowNodeKey,
@@ -687,17 +580,9 @@
   textColor: Colors.white,
   data: goldenAsyncTimelineEvent,
   onSelected: (_) {},
-);
+)..sectionIndex = 0;
 
-final testNodesWithLabel = [
-  labelNode,
-  testNode,
-  testNode2,
-  testNode3,
-  testNode4,
-];
-
-final testNodesWithoutLabel = [
+final testNodes = [
   testNode,
   testNode2,
   testNode3,
@@ -714,4 +599,4 @@
   textColor: Colors.white,
   data: goldenAsyncTimelineEvent,
   onSelected: (_) {},
-);
+)..sectionIndex = 0;
diff --git a/packages/devtools_app/test/goldens/timeline_flame_chart_with_selected_frame.png b/packages/devtools_app/test/goldens/timeline_flame_chart_with_selected_frame.png
index 5bcb780..3258b3c 100644
--- a/packages/devtools_app/test/goldens/timeline_flame_chart_with_selected_frame.png
+++ b/packages/devtools_app/test/goldens/timeline_flame_chart_with_selected_frame.png
Binary files differ
diff --git a/packages/devtools_app/test/timeline_flame_chart_test.dart b/packages/devtools_app/test/timeline_flame_chart_test.dart
index c8bb185..4052631 100644
--- a/packages/devtools_app/test/timeline_flame_chart_test.dart
+++ b/packages/devtools_app/test/timeline_flame_chart_test.dart
@@ -2,12 +2,15 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
+import 'dart:io';
+
 import 'package:devtools_app/src/globals.dart';
 import 'package:devtools_app/src/service_manager.dart';
 import 'package:devtools_app/src/timeline/timeline_flame_chart.dart';
 import 'package:devtools_app/src/timeline/timeline_screen.dart';
 import 'package:devtools_app/src/timeline/timeline_controller.dart';
 import 'package:devtools_testing/support/timeline_test_data.dart';
+import 'package:flutter/foundation.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter_test/flutter_test.dart';
 import 'package:mockito/mockito.dart';
@@ -18,7 +21,7 @@
 
 void main() {
   FakeServiceManager fakeServiceManager;
-  group('TimelineFlameChart', () {
+  group('TimelineFlameChartContent', () {
     void _setupForTimeline(Map<String, dynamic> timelineJson) {
       fakeServiceManager = FakeServiceManager(
         useFakeService: true,
@@ -82,10 +85,13 @@
       await tester.pumpAndSettle();
 
       expect(find.byType(TimelineFlameChart), findsOneWidget);
-      expect(
+      await expectLater(
           find.byType(TimelineFlameChart),
           matchesGoldenFile(
               'goldens/timeline_flame_chart_with_selected_frame.png'));
-    });
+
+      // Await delay for golden comparison.
+      await tester.pumpAndSettle(const Duration(seconds: 2));
+    }, skip: kIsWeb || !Platform.isMacOS);
   });
 }