Add a ring buffer for Perfetto timeline traces (#7403)

diff --git a/packages/.vscode/launch.json b/packages/.vscode/launch.json
index 3e537d8..a8ac621 100644
--- a/packages/.vscode/launch.json
+++ b/packages/.vscode/launch.json
@@ -33,6 +33,13 @@
             "flutterMode": "profile",
         },
         {
+            "name": "devtools + release",
+            "request": "launch",
+            "type": "dart",
+            "program": "devtools_app/lib/main.dart",
+            "flutterMode": "release",
+        },
+        {
             "name": "devtools + profile + experiments",
             "request": "launch",
             "type": "dart",
diff --git a/packages/devtools_app/integration_test/test/live_connection/performance_screen_event_recording_test.dart b/packages/devtools_app/integration_test/test/live_connection/performance_screen_event_recording_test.dart
index 3f1edaa..835e9a4 100644
--- a/packages/devtools_app/integration_test/test/live_connection/performance_screen_event_recording_test.dart
+++ b/packages/devtools_app/integration_test/test/live_connection/performance_screen_event_recording_test.dart
@@ -9,6 +9,7 @@
 import 'package:flutter/material.dart';
 import 'package:flutter_test/flutter_test.dart';
 import 'package:integration_test/integration_test.dart';
+import 'package:vm_service_protos/vm_service_protos.dart';
 
 // To run:
 // dart run integration_test/run_tests.dart --target=integration_test/test/live_connection/performance_screen_event_recording_test.dart
@@ -50,17 +51,16 @@
       final performanceController = screenState.controller;
 
       logStatus('Verifying that data is processed upon first load');
-      final initialTrace = List.of(
-        performanceController
-            .timelineEventsController.fullPerfettoTrace!.packet,
-        growable: false,
+      final initialTrace = Trace.fromBuffer(
+        performanceController.timelineEventsController.fullPerfettoTrace,
       );
+      final initialTracePacket = List.of(initialTrace.packet, growable: false);
       final initialTrackDescriptors =
-          initialTrace.where((e) => e.hasTrackDescriptor());
-      expect(initialTrace, isNotEmpty);
+          initialTracePacket.where((e) => e.hasTrackDescriptor());
+      expect(initialTracePacket, isNotEmpty);
       expect(initialTrackDescriptors, isNotEmpty);
 
-      final trackEvents = initialTrace.where((e) => e.hasTrackEvent());
+      final trackEvents = initialTracePacket.where((e) => e.hasTrackEvent());
       expect(trackEvents, isNotEmpty);
 
       expect(
@@ -95,14 +95,14 @@
       await tester.pump(longPumpDuration);
 
       logStatus('Verifying that we have recorded new events');
-      final refreshedTrace = List.of(
-        performanceController
-            .timelineEventsController.fullPerfettoTrace!.packet,
-        growable: false,
+      final refreshedTrace = Trace.fromBuffer(
+        performanceController.timelineEventsController.fullPerfettoTrace,
       );
+      final refreshedTracePacket =
+          List.of(refreshedTrace.packet, growable: false);
       expect(
-        refreshedTrace.length,
-        greaterThan(initialTrace.length),
+        refreshedTracePacket.length,
+        greaterThan(initialTracePacket.length),
         reason: 'Expected new events to have been recorded, but none were.',
       );
     },
diff --git a/packages/devtools_app/lib/src/screens/performance/panes/timeline_events/perfetto/_perfetto_controller_web.dart b/packages/devtools_app/lib/src/screens/performance/panes/timeline_events/perfetto/_perfetto_controller_web.dart
index 78a05b2..137b615 100644
--- a/packages/devtools_app/lib/src/screens/performance/panes/timeline_events/perfetto/_perfetto_controller_web.dart
+++ b/packages/devtools_app/lib/src/screens/performance/panes/timeline_events/perfetto/_perfetto_controller_web.dart
@@ -6,7 +6,6 @@
 import 'dart:ui_web' as ui_web;
 
 import 'package:flutter/foundation.dart';
-import 'package:vm_service_protos/vm_service_protos.dart';
 import 'package:web/web.dart';
 
 import '../../../../../shared/globals.dart';
@@ -145,7 +144,7 @@
 
   /// Trace data that we should load, but have not yet since the trace viewer
   /// is not visible (i.e. [TimelineEventsController.isActiveFeature] is false).
-  Trace? pendingTraceToLoad;
+  Uint8List? pendingTraceToLoad;
 
   /// Time range we should scroll to, but have not yet since the trace viewer
   /// is not visible (i.e. [TimelineEventsController.isActiveFeature] is false).
@@ -202,13 +201,13 @@
   }
 
   @override
-  Future<void> loadTrace(Trace trace) async {
+  Future<void> loadTrace(Uint8List traceBinary) async {
     if (!timelineEventsController.isActiveFeature) {
-      pendingTraceToLoad = trace;
+      pendingTraceToLoad = traceBinary;
       return;
     }
     pendingTraceToLoad = null;
-    activeTrace.trace = trace;
+    activeTrace.trace = traceBinary;
     await Future.delayed(_postTraceDelay);
   }
 
@@ -230,6 +229,6 @@
   @override
   Future<void> clear() async {
     processor.clear();
-    await loadTrace(Trace());
+    await loadTrace(Uint8List(0));
   }
 }
diff --git a/packages/devtools_app/lib/src/screens/performance/panes/timeline_events/perfetto/_perfetto_web.dart b/packages/devtools_app/lib/src/screens/performance/panes/timeline_events/perfetto/_perfetto_web.dart
index 4fb9aff..29859b2 100644
--- a/packages/devtools_app/lib/src/screens/performance/panes/timeline_events/perfetto/_perfetto_web.dart
+++ b/packages/devtools_app/lib/src/screens/performance/panes/timeline_events/perfetto/_perfetto_web.dart
@@ -10,12 +10,10 @@
 import 'package:devtools_app_shared/utils.dart';
 import 'package:devtools_app_shared/web_utils.dart';
 import 'package:flutter/material.dart';
-import 'package:vm_service_protos/vm_service_protos.dart';
 import 'package:web/web.dart';
 
 import '../../../../../shared/analytics/analytics.dart' as ga;
 import '../../../../../shared/analytics/constants.dart' as gac;
-import '../../../../../shared/development_helpers.dart';
 import '../../../../../shared/globals.dart';
 import '../../../../../shared/primitives/utils.dart';
 import '../../../performance_utils.dart';
@@ -47,7 +45,7 @@
 
     // If [_perfettoController.activeTrace.trace] has a null value, the trace
     // data has not yet been initialized.
-    if (_perfettoController.activeTrace.trace != null) {
+    if (_perfettoController.activeTrace.traceBinary != null) {
       _loadActiveTrace();
     }
     addAutoDisposeListener(_perfettoController.activeTrace, _loadActiveTrace);
@@ -60,10 +58,10 @@
   }
 
   void _loadActiveTrace() {
-    assert(_perfettoController.activeTrace.trace != null);
+    assert(_perfettoController.activeTrace.traceBinary != null);
     unawaited(
       _viewController._loadPerfettoTrace(
-        _perfettoController.activeTrace.trace!,
+        _perfettoController.activeTrace.traceBinary!,
       ),
     );
   }
@@ -161,26 +159,22 @@
     );
   }
 
-  Future<void> _loadPerfettoTrace(Trace trace) async {
-    late Uint8List buffer;
-    debugTimeSync(
-      () => buffer = trace.writeToBuffer(),
-      debugName: 'Trace.writeToBuffer',
-    );
-
-    if (buffer.isEmpty) {
+  Future<void> _loadPerfettoTrace(Uint8List traceBinary) async {
+    if (traceBinary.isEmpty) {
       // TODO(kenz): is there a better way to create an empty data set using the
       // protozero format? I think this is still using the legacy Chrome format.
       // We can't use `Trace()` because the Perfetto post message handler throws
       // an exception if an empty buffer is posted.
-      buffer = Uint8List.fromList(jsonEncode({'traceEvents': []}).codeUnits);
+      traceBinary = Uint8List.fromList(
+        jsonEncode({'traceEvents': []}).codeUnits,
+      );
     }
 
     await _pingPerfettoUntilReady();
     ga.select(gac.performance, gac.PerformanceEvents.perfettoLoadTrace.name);
     _postMessage({
       'perfetto': {
-        'buffer': buffer,
+        'buffer': traceBinary,
         'title': 'DevTools timeline trace',
         'keepApiOpen': true,
       },
diff --git a/packages/devtools_app/lib/src/screens/performance/panes/timeline_events/perfetto/perfetto_controller.dart b/packages/devtools_app/lib/src/screens/performance/panes/timeline_events/perfetto/perfetto_controller.dart
index 5805218..d1e28b8 100644
--- a/packages/devtools_app/lib/src/screens/performance/panes/timeline_events/perfetto/perfetto_controller.dart
+++ b/packages/devtools_app/lib/src/screens/performance/panes/timeline_events/perfetto/perfetto_controller.dart
@@ -2,15 +2,16 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
+import 'dart:typed_data';
+
 import 'package:devtools_app_shared/utils.dart';
-import 'package:vm_service_protos/vm_service_protos.dart';
 
 import '../../../../../shared/primitives/utils.dart';
 import '../../../performance_controller.dart';
+import '../timeline_event_processor.dart';
 import '../timeline_events_controller.dart';
 import '_perfetto_controller_desktop.dart'
     if (dart.library.js_interop) '_perfetto_controller_web.dart';
-import 'perfetto_event_processor.dart';
 
 PerfettoControllerImpl createPerfettoController(
   PerformanceController performanceController,
@@ -42,7 +43,7 @@
 
   void onBecomingActive() {}
 
-  Future<void> loadTrace(Trace trace) async {}
+  Future<void> loadTrace(Uint8List traceBinary) async {}
 
   void scrollToTimeRange(TimeRange timeRange) {}
 
diff --git a/packages/devtools_app/lib/src/screens/performance/panes/timeline_events/perfetto/tracing/model.dart b/packages/devtools_app/lib/src/screens/performance/panes/timeline_events/perfetto/tracing/model.dart
index 51edf7b..801bac5 100644
--- a/packages/devtools_app/lib/src/screens/performance/panes/timeline_events/perfetto/tracing/model.dart
+++ b/packages/devtools_app/lib/src/screens/performance/panes/timeline_events/perfetto/tracing/model.dart
@@ -9,31 +9,24 @@
 
 import '../../../../performance_model.dart';
 
-/// A change notifer that contains a Perfetto [Trace] object.
+/// A change notifer that contains a Perfetto trace binary object [Uint8List].
 ///
-/// We use this custom change notifier instead of a raw ValueNotifier<Trace?> so
-/// that listeners are notified when the inner value of [_trace] is updated. For
-/// example, on method calls like [Trace.mergeFromBuffer], the inner value of
-/// the [Trace] object is changed by merging new data into the existing object.
-/// However, the object identity does not change for operations like this, which
-/// means that set calls to ValueNotifier.value would not notify listeners.
-///
-/// Using [PerfettoTrace] instead ensures that listeners are updated for calls
-/// to set the value of [trace], even when the existing [trace] and the new
-/// [value] satisfy Object equality.
+/// We use this custom change notifier instead of a raw
+/// ValueNotifier<Uint8List?> so that listeners are notified when the content of
+/// the [Uint8List] changes, even if the [Uint8List] object does not change.
 class PerfettoTrace extends ChangeNotifier {
-  PerfettoTrace(Trace? trace) : _trace = trace;
+  PerfettoTrace(Uint8List? traceBinary) : _traceBinary = traceBinary;
 
-  Trace? get trace => _trace;
-  Trace? _trace;
+  Uint8List? get traceBinary => _traceBinary;
+  Uint8List? _traceBinary;
 
-  /// Sets the value of [_trace] and notifies listeners.
+  /// Sets the value of [_traceBinary] and notifies listeners.
   ///
-  /// Listeners will be notified event if [_trace] and [value] satisfy Object
-  /// equality. This is intentional, since the data contained in the [Trace]
-  /// object may be different.
-  set trace(Trace? value) {
-    _trace = value;
+  /// Listeners will be notified event if [_traceBinary] and [value] satisfy
+  /// Object equality. This is intentional, since the content in the [Uint8List]
+  /// may be different.
+  set trace(Uint8List? value) {
+    _traceBinary = value;
     notifyListeners();
   }
 }
diff --git a/packages/devtools_app/lib/src/screens/performance/panes/timeline_events/perfetto/perfetto_event_processor.dart b/packages/devtools_app/lib/src/screens/performance/panes/timeline_events/timeline_event_processor.dart
similarity index 95%
rename from packages/devtools_app/lib/src/screens/performance/panes/timeline_events/perfetto/perfetto_event_processor.dart
rename to packages/devtools_app/lib/src/screens/performance/panes/timeline_events/timeline_event_processor.dart
index eb98a2d..ec539f5 100644
--- a/packages/devtools_app/lib/src/screens/performance/panes/timeline_events/perfetto/perfetto_event_processor.dart
+++ b/packages/devtools_app/lib/src/screens/performance/panes/timeline_events/timeline_event_processor.dart
@@ -8,12 +8,12 @@
 import 'package:flutter/foundation.dart';
 import 'package:logging/logging.dart';
 
-import '../../../../../shared/development_helpers.dart';
-import '../../../../../shared/primitives/utils.dart';
-import '../../../performance_controller.dart';
-import '../../../performance_model.dart';
-import '../timeline_events_controller.dart';
-import 'tracing/model.dart';
+import '../../../../shared/development_helpers.dart';
+import '../../../../shared/primitives/utils.dart';
+import '../../performance_controller.dart';
+import '../../performance_model.dart';
+import 'perfetto/tracing/model.dart';
+import 'timeline_events_controller.dart';
 
 final _log = Logger('flutter_timeline_event_processor');
 
diff --git a/packages/devtools_app/lib/src/screens/performance/panes/timeline_events/timeline_events_controller.dart b/packages/devtools_app/lib/src/screens/performance/panes/timeline_events/timeline_events_controller.dart
index 8cd6377..2d1014d 100644
--- a/packages/devtools_app/lib/src/screens/performance/panes/timeline_events/timeline_events_controller.dart
+++ b/packages/devtools_app/lib/src/screens/performance/panes/timeline_events/timeline_events_controller.dart
@@ -19,6 +19,7 @@
 import '../../../../shared/development_helpers.dart';
 import '../../../../shared/future_work_tracker.dart';
 import '../../../../shared/globals.dart';
+import '../../../../shared/primitives/byte_utils.dart';
 import '../../../../shared/primitives/utils.dart';
 import '../../performance_controller.dart';
 import '../../performance_model.dart';
@@ -47,6 +48,7 @@
         _status.value = EventsControllerStatus.ready;
       }
     });
+    traceRingBuffer = Uint8ListRingBuffer(maxSizeBytes: _traceRingBufferSize);
   }
 
   static const uiThreadSuffix = '.ui';
@@ -60,10 +62,31 @@
 
   /// The complete Perfetto timeline that DevTools has received from the VM.
   ///
-  /// This value is built up by polling every [_timelinePollingInterval], and
-  /// fetching new Perfetto timeline data from the VM. New data is continually
-  /// merged with [fullPerfettoTrace] to keep this value up to date.
-  Trace? fullPerfettoTrace;
+  /// This returns the merged value of all the traces in [traceRingBuffer],
+  /// which is periodically trimmed to preserve memory in DevTools.
+  Uint8List get fullPerfettoTrace => traceRingBuffer.merged;
+
+  /// A ring buffer containing all the Perfetto trace binaries that we have
+  /// received from the VM.
+  ///
+  /// This ring buffer is built up by polling every [_timelinePollingInterval]
+  /// and fetching new Perfetto timeline data from the VM.
+  ///
+  /// We use a ring buffer for this data so that the earliest entries will be
+  /// removed when the total size of this queue exceeds [_traceRingBufferSize].
+  /// This prevents the Performance page from causing DevTools to OOM.
+  ///
+  /// The bytes contained in this ring buffer are stored until the Perfetto
+  /// viewer is refreshed, at which point [fullPerfettoTrace] will be called to
+  /// merge all of this data into a single trace binary for the Perfetto UI to
+  /// consume.
+  @visibleForTesting
+  late final Uint8ListRingBuffer traceRingBuffer;
+
+  /// Size limit in GB for [traceRingBuffer] that determines when traces should
+  /// be removed from the queue.
+  final _traceRingBufferSize =
+      convertBytes(1, from: ByteUnit.gb, to: ByteUnit.byte).round();
 
   /// Track events that we have received from the VM, but have not yet
   /// processed.
@@ -183,34 +206,16 @@
       () => traceBinary = base64Decode(rawPerfettoTimeline.trace!),
       debugName: 'base64Decode perfetto trace',
     );
+
     _updatePerfettoTrace(traceBinary!, logWarning: isInitialPull);
   }
 
   void _updatePerfettoTrace(Uint8List traceBinary, {bool logWarning = true}) {
-    final decodedTrace =
-        _prepareForTraceProcessing(traceBinary, logWarning: logWarning);
-
-    if (fullPerfettoTrace == null) {
-      debugTraceCallback(
-        () => _log.info(
-          '[_updatePerfettoTrace] setting initial perfetto trace',
-        ),
-      );
-      fullPerfettoTrace = decodedTrace ?? _traceFromBinary(traceBinary);
-    } else {
-      debugTraceCallback(
-        () => _log.info(
-          '[_updatePerfettoTrace] merging perfetto trace with new buffer',
-        ),
-      );
-      debugTimeSync(
-        () => fullPerfettoTrace!.mergeFromBuffer(traceBinary),
-        debugName: 'perfettoTrace.mergeFromBuffer',
-      );
-    }
+    _prepareForTraceProcessing(traceBinary, logWarning: logWarning);
+    traceRingBuffer.addData(traceBinary);
   }
 
-  Trace? _prepareForTraceProcessing(
+  void _prepareForTraceProcessing(
     Uint8List traceBinary, {
     bool logWarning = true,
   }) {
@@ -219,7 +224,7 @@
         () => _log
             .info('[_prepareTraceForProcessing] not a flutter app, returning.'),
       );
-      return null;
+      return;
     }
 
     final trace = _traceFromBinary(traceBinary);
@@ -239,7 +244,6 @@
       }
     }
     updateTrackIds(newTrackDescriptors, logWarning: logWarning);
-    return trace;
   }
 
   void updateTrackIds(
@@ -342,8 +346,7 @@
   }
 
   Future<void> loadPerfettoTrace() async {
-    debugTraceCallback(() => _log.info('[loadPerfettoTrace] updating viewer'));
-    await perfettoController.loadTrace(fullPerfettoTrace ?? Trace());
+    await perfettoController.loadTrace(fullPerfettoTrace);
   }
 
   @override
@@ -486,7 +489,7 @@
   @override
   Future<void> clearData() async {
     _unprocessedTrackEvents.clear();
-    fullPerfettoTrace = Trace();
+    traceRingBuffer.clear();
     _trackDescriptors.clear();
     _unassignedFlutterTimelineEvents.clear();
 
diff --git a/packages/devtools_app/lib/src/screens/performance/performance_controller.dart b/packages/devtools_app/lib/src/screens/performance/performance_controller.dart
index abb81ec..a3051c7 100644
--- a/packages/devtools_app/lib/src/screens/performance/performance_controller.dart
+++ b/packages/devtools_app/lib/src/screens/performance/performance_controller.dart
@@ -242,8 +242,7 @@
   OfflineScreenData screenDataForExport() => OfflineScreenData(
         screenId: PerformanceScreen.id,
         data: OfflinePerformanceData(
-          perfettoTraceBinary:
-              timelineEventsController.fullPerfettoTrace?.writeToBuffer(),
+          perfettoTraceBinary: timelineEventsController.fullPerfettoTrace,
           frames: flutterFramesController.flutterFrames.value,
           selectedFrame: flutterFramesController.selectedFrame.value,
           rasterStats: rasterStatsController.rasterStats.value,
diff --git a/packages/devtools_app/lib/src/shared/primitives/byte_utils.dart b/packages/devtools_app/lib/src/shared/primitives/byte_utils.dart
index b3c7c38..31a40bb 100644
--- a/packages/devtools_app/lib/src/shared/primitives/byte_utils.dart
+++ b/packages/devtools_app/lib/src/shared/primitives/byte_utils.dart
@@ -2,7 +2,13 @@
 // 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';
+import 'dart:typed_data';
+
+import 'package:meta/meta.dart';
+
+import '../development_helpers.dart';
 
 String? prettyPrintBytes(
   num? bytes, {
@@ -115,3 +121,51 @@
 
   String get display => _display ?? name.toUpperCase();
 }
+
+/// Stores a list of Uint8List objects in a ring buffer, keeping the total size
+/// at or below [maxSizeBytes].
+class Uint8ListRingBuffer {
+  Uint8ListRingBuffer({required this.maxSizeBytes});
+
+  /// The maximum size in bytes that [data] will contain.
+  final int maxSizeBytes;
+
+  /// Returns the size of the ring buffer in bytes.
+  ///
+  /// Since each element in [data] is a Uint8List, the size of each element in
+  /// bytes is the length of the Uint8List.
+  int get size => data.fold(0, (sum, e) => sum + e.length);
+
+  @visibleForTesting
+  final data = ListQueue<Uint8List>();
+
+  /// Stores [binaryData] in [data] and removes as many early elements as
+  /// necessary to keep the size of [data] smaller than [maxSizeBytes].
+  void addData(Uint8List binaryData) {
+    data.add(binaryData);
+
+    final exceeded = size - maxSizeBytes;
+    if (exceeded < 0) return;
+
+    var bytesRemoved = 0;
+    while (bytesRemoved < exceeded && data.length > 1) {
+      final removed = data.removeFirst();
+      bytesRemoved += removed.length;
+    }
+  }
+
+  /// Merges all the data in this ring buffer into a single [Uint8List] and
+  /// returns it.
+  Uint8List get merged {
+    final allBytes = BytesBuilder();
+    debugTimeSync(
+      () => data.forEach(allBytes.add),
+      debugName: 'Uint8ListRingBuffer.mergeAllData',
+    );
+    return allBytes.takeBytes();
+  }
+
+  void clear() {
+    data.clear();
+  }
+}
diff --git a/packages/devtools_app/lib/src/shared/routing.dart b/packages/devtools_app/lib/src/shared/routing.dart
index ed6fa4a..a61b389 100644
--- a/packages/devtools_app/lib/src/shared/routing.dart
+++ b/packages/devtools_app/lib/src/shared/routing.dart
@@ -123,8 +123,8 @@
   @override
   final GlobalKey<NavigatorState> navigatorKey;
 
-  static String get currentPage => _currentPage;
-  static late String _currentPage;
+  static String? get currentPage => _currentPage;
+  static String? _currentPage;
 
   final Page Function(
     BuildContext,
diff --git a/packages/devtools_app/test/performance/timeline_events/perfetto/tracing_model_test.dart b/packages/devtools_app/test/performance/timeline_events/perfetto/tracing_model_test.dart
index 0092eed..de0b9bd 100644
--- a/packages/devtools_app/test/performance/timeline_events/perfetto/tracing_model_test.dart
+++ b/packages/devtools_app/test/performance/timeline_events/perfetto/tracing_model_test.dart
@@ -3,6 +3,7 @@
 // found in the LICENSE file.
 
 import 'dart:convert';
+import 'dart:typed_data';
 
 import 'package:devtools_app/devtools_app.dart';
 import 'package:fixnum/fixnum.dart';
@@ -14,19 +15,19 @@
 void main() {
   group('$PerfettoTrace', () {
     test('setting trace with new trace object notifies listeners', () {
-      final startingTrace = Trace();
-      final perfettoTrace = PerfettoTrace(startingTrace);
-      final newTrace = Trace();
+      final startingBinary = Uint8List(0);
+      final perfettoTrace = PerfettoTrace(startingBinary);
+      final newBinary = Uint8List(0);
 
       bool notified = false;
       perfettoTrace.addListener(() => notified = true);
-      perfettoTrace.trace = newTrace;
+      perfettoTrace.trace = newBinary;
 
-      expect(perfettoTrace.trace, newTrace);
+      expect(perfettoTrace.traceBinary, newBinary);
       expect(notified, isTrue);
     });
     test('setting trace with identical object notifies listeners', () {
-      final trace = Trace();
+      final trace = Uint8List(0);
       final perfettoTrace = PerfettoTrace(trace);
 
       bool notified = false;
diff --git a/packages/devtools_app/test/performance/timeline_events/perfetto/perfetto_event_processor_test.dart b/packages/devtools_app/test/performance/timeline_events/timeline_event_processor_test.dart
similarity index 96%
rename from packages/devtools_app/test/performance/timeline_events/perfetto/perfetto_event_processor_test.dart
rename to packages/devtools_app/test/performance/timeline_events/timeline_event_processor_test.dart
index 3da5ba6..fc0f689 100644
--- a/packages/devtools_app/test/performance/timeline_events/perfetto/perfetto_event_processor_test.dart
+++ b/packages/devtools_app/test/performance/timeline_events/timeline_event_processor_test.dart
@@ -5,7 +5,7 @@
 import 'dart:convert';
 
 import 'package:devtools_app/devtools_app.dart';
-import 'package:devtools_app/src/screens/performance/panes/timeline_events/perfetto/perfetto_event_processor.dart';
+import 'package:devtools_app/src/screens/performance/panes/timeline_events/timeline_event_processor.dart';
 import 'package:devtools_app_shared/ui.dart';
 import 'package:devtools_app_shared/utils.dart';
 import 'package:devtools_test/devtools_test.dart';
@@ -13,7 +13,7 @@
 import 'package:mockito/mockito.dart';
 import 'package:vm_service_protos/vm_service_protos.dart';
 
-import '../../../test_infra/test_data/performance/sample_performance_data.dart';
+import '../../test_infra/test_data/performance/sample_performance_data.dart';
 
 void main() {
   final originalTrackEventPackets = List.of(allTrackEventPackets);
diff --git a/packages/devtools_app/test/performance/timeline_events/timeline_events_controller_test.dart b/packages/devtools_app/test/performance/timeline_events/timeline_events_controller_test.dart
index 68c59e3..90cb00b 100644
--- a/packages/devtools_app/test/performance/timeline_events/timeline_events_controller_test.dart
+++ b/packages/devtools_app/test/performance/timeline_events/timeline_events_controller_test.dart
@@ -51,7 +51,7 @@
 
     test('can setOfflineData', () async {
       // Ensure we are starting in an empty state.
-      expect(eventsController.fullPerfettoTrace, isNull);
+      expect(eventsController.fullPerfettoTrace, isEmpty);
       expect(eventsController.perfettoController.processor.uiTrackId, isNull);
       expect(
         eventsController.perfettoController.processor.rasterTrackId,
@@ -66,7 +66,7 @@
           .thenReturn(offlineData);
       await eventsController.setOfflineData(offlineData);
 
-      expect(eventsController.fullPerfettoTrace, isNotNull);
+      expect(eventsController.fullPerfettoTrace, isNotEmpty);
       expect(
         eventsController.perfettoController.processor.uiTrackId,
         equals(testUiTrackId),
diff --git a/packages/devtools_app/test/primitives/byte_utils_test.dart b/packages/devtools_app/test/primitives/byte_utils_test.dart
index 67d30a0..900f9c1 100644
--- a/packages/devtools_app/test/primitives/byte_utils_test.dart
+++ b/packages/devtools_app/test/primitives/byte_utils_test.dart
@@ -2,10 +2,93 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
+import 'dart:typed_data';
+
 import 'package:devtools_app/src/shared/primitives/byte_utils.dart';
 import 'package:flutter_test/flutter_test.dart';
 
 void main() {
+  group('$Uint8ListRingBuffer', () {
+    test('calculates size', () {
+      final list1 = Uint8List.fromList([1, 2, 3, 4]);
+      final list2 = Uint8List.fromList([5, 6, 7, 8]);
+      final list3 = Uint8List.fromList([9, 10, 11, 12]);
+
+      final buffer = Uint8ListRingBuffer(maxSizeBytes: 100);
+      expect(buffer.size, 0);
+      buffer.addData(list1);
+      expect(buffer.size, 4);
+      buffer.addData(list2);
+      expect(buffer.size, 8);
+      buffer.addData(list3);
+      expect(buffer.size, 12);
+    });
+
+    test('can add data', () {
+      final list1 = Uint8List.fromList([1, 2, 3, 4]);
+      final list2 = Uint8List.fromList([5, 6, 7, 8]);
+      final list3 = Uint8List.fromList([9, 10, 11, 12]);
+
+      final buffer = Uint8ListRingBuffer(maxSizeBytes: 10);
+      expect(buffer.data, isEmpty);
+
+      buffer.addData(list1);
+      expect(buffer.data.length, 1);
+      expect(buffer.size, 4);
+      expect(buffer.data, contains(list1));
+
+      buffer.addData(list2);
+      expect(buffer.data.length, 2);
+      expect(buffer.size, 8);
+      expect(buffer.data, contains(list1));
+      expect(buffer.data, contains(list2));
+
+      buffer.addData(list3);
+      expect(buffer.data.length, 2);
+      expect(buffer.size, 8);
+      expect(buffer.data, isNot(contains(list1)));
+      expect(buffer.data, contains(list2));
+      expect(buffer.data, contains(list3));
+    });
+
+    test('can merge data', () {
+      final list1 = Uint8List.fromList([1, 2, 3, 4]);
+      final list2 = Uint8List.fromList([5, 6, 7, 8]);
+
+      final buffer = Uint8ListRingBuffer(maxSizeBytes: 10);
+      expect(buffer.data, isEmpty);
+
+      buffer
+        ..addData(list1)
+        ..addData(list2);
+      expect(buffer.size, 8);
+
+      final merged = buffer.merged;
+      expect(merged.length, 8);
+      expect(merged, Uint8List.fromList([...list1, ...list2]));
+    });
+
+    test('can clear data', () {
+      final list1 = Uint8List.fromList([1, 2, 3, 4]);
+      final list2 = Uint8List.fromList([5, 6, 7, 8]);
+
+      final buffer = Uint8ListRingBuffer(maxSizeBytes: 10);
+      expect(buffer.data, isEmpty);
+
+      buffer
+        ..addData(list1)
+        ..addData(list2);
+      expect(buffer.data.length, 2);
+      expect(buffer.size, 8);
+      expect(buffer.data, contains(list1));
+      expect(buffer.data, contains(list2));
+
+      buffer.clear();
+      expect(buffer.data, isEmpty);
+      expect(buffer.size, 0);
+    });
+  });
+
   group('printBytes', () {
     test('${ByteUnit.kb}', () {
       const int kb = 1024;
diff --git a/packages/devtools_app/test/test_infra/scenes/standalone_ui/vs_code_mock_editor.dart b/packages/devtools_app/test/test_infra/scenes/standalone_ui/vs_code_mock_editor.dart
index e42cffd..5f3b09e 100644
--- a/packages/devtools_app/test/test_infra/scenes/standalone_ui/vs_code_mock_editor.dart
+++ b/packages/devtools_app/test/test_infra/scenes/standalone_ui/vs_code_mock_editor.dart
@@ -41,7 +41,7 @@
 
   /// The last [maxLogEvents] communication messages sent between the panel
   /// and the "host IDE".
-  final logRing = DoubleLinkedQueue<String>();
+  final logRing = ListQueue<String>();
 
   /// A stream that emits each time the log is updated to allow the log widget
   /// to be rebuilt.