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.