From bd2566c5411dbcfecd63a8bd77ef716de58bfe11 Mon Sep 17 00:00:00 2001 From: Kenzie Davisson <43759233+kenzieschmoll@users.noreply.github.com> Date: Thu, 21 Mar 2024 14:54:52 -0700 Subject: [PATCH] Add a ring buffer for Perfetto timeline traces (#7403) --- packages/.vscode/launch.json | 7 ++ ...rformance_screen_event_recording_test.dart | 26 +++--- .../perfetto/_perfetto_controller_web.dart | 11 ++- .../perfetto/_perfetto_web.dart | 24 ++---- .../perfetto/perfetto_controller.dart | 7 +- .../perfetto/tracing/model.dart | 33 +++----- ...sor.dart => timeline_event_processor.dart} | 12 +-- .../timeline_events_controller.dart | 65 ++++++++------- .../performance/performance_controller.dart | 3 +- .../lib/src/shared/primitives/byte_utils.dart | 54 ++++++++++++ .../devtools_app/lib/src/shared/routing.dart | 4 +- .../perfetto/tracing_model_test.dart | 13 +-- ...art => timeline_event_processor_test.dart} | 4 +- .../timeline_events_controller_test.dart | 4 +- .../test/primitives/byte_utils_test.dart | 83 +++++++++++++++++++ .../standalone_ui/vs_code_mock_editor.dart | 2 +- 16 files changed, 243 insertions(+), 109 deletions(-) rename packages/devtools_app/lib/src/screens/performance/panes/timeline_events/{perfetto/perfetto_event_processor.dart => timeline_event_processor.dart} (95%) rename packages/devtools_app/test/performance/timeline_events/{perfetto/perfetto_event_processor_test.dart => timeline_event_processor_test.dart} (97%) diff --git a/packages/.vscode/launch.json b/packages/.vscode/launch.json index 3e537d88e76..a8ac621b269 100644 --- a/packages/.vscode/launch.json +++ b/packages/.vscode/launch.json @@ -32,6 +32,13 @@ "program": "devtools_app/lib/main.dart", "flutterMode": "profile", }, + { + "name": "devtools + release", + "request": "launch", + "type": "dart", + "program": "devtools_app/lib/main.dart", + "flutterMode": "release", + }, { "name": "devtools + profile + experiments", "request": "launch", 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 3f1edaaea57..835e9a4d986 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:devtools_test/integration_test.dart'; 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 @@ void main() { 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 @@ void main() { 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 78a05b27ab9..137b615e102 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:async'; 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 @@ class PerfettoControllerImpl extends PerfettoController { /// 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 @@ class PerfettoControllerImpl extends PerfettoController { } @override - Future loadTrace(Trace trace) async { + Future 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 @@ class PerfettoControllerImpl extends PerfettoController { @override Future 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 4fb9affda5d..29859b29079 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 'dart:typed_data'; 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 @@ class _PerfettoState extends State with AutoDisposeMixin { // 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 @@ class _PerfettoState extends State with AutoDisposeMixin { } 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 @@ class _PerfettoViewController extends DisposableController ); } - Future _loadPerfettoTrace(Trace trace) async { - late Uint8List buffer; - debugTimeSync( - () => buffer = trace.writeToBuffer(), - debugName: 'Trace.writeToBuffer', - ); - - if (buffer.isEmpty) { + Future _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 58052187481..d1e28b8e5e8 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 @@ abstract class PerfettoController extends DisposableController { void onBecomingActive() {} - Future loadTrace(Trace trace) async {} + Future 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 51edf7b8a3e..801bac51d83 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 'package:vm_service_protos/vm_service_protos.dart'; 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 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 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 eb98a2d21be..ec539f50daa 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:fixnum/fixnum.dart'; 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 8cd6377c91b..2d1014dadb4 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/analytics/metrics.dart'; 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 @@ class TimelineEventsController extends PerformanceFeatureController _status.value = EventsControllerStatus.ready; } }); + traceRingBuffer = Uint8ListRingBuffer(maxSizeBytes: _traceRingBufferSize); } static const uiThreadSuffix = '.ui'; @@ -60,10 +62,31 @@ class TimelineEventsController extends PerformanceFeatureController /// 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 @@ class TimelineEventsController extends PerformanceFeatureController () => 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 @@ class TimelineEventsController extends PerformanceFeatureController () => _log .info('[_prepareTraceForProcessing] not a flutter app, returning.'), ); - return null; + return; } final trace = _traceFromBinary(traceBinary); @@ -239,7 +244,6 @@ class TimelineEventsController extends PerformanceFeatureController } } updateTrackIds(newTrackDescriptors, logWarning: logWarning); - return trace; } void updateTrackIds( @@ -342,8 +346,7 @@ class TimelineEventsController extends PerformanceFeatureController } Future loadPerfettoTrace() async { - debugTraceCallback(() => _log.info('[loadPerfettoTrace] updating viewer')); - await perfettoController.loadTrace(fullPerfettoTrace ?? Trace()); + await perfettoController.loadTrace(fullPerfettoTrace); } @override @@ -486,7 +489,7 @@ class TimelineEventsController extends PerformanceFeatureController @override Future 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 abb81ec4111..a3051c70930 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 @@ class PerformanceController extends DisposableController 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 b3c7c3818e3..31a40bbff2c 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 @@ enum ByteUnit { 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(); + + /// 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 ed6fa4aa736..a61b3890a06 100644 --- a/packages/devtools_app/lib/src/shared/routing.dart +++ b/packages/devtools_app/lib/src/shared/routing.dart @@ -123,8 +123,8 @@ class DevToolsRouterDelegate extends RouterDelegate @override final GlobalKey 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 0092eed6103..de0b9bd1692 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 @@ import '../../../test_infra/test_data/performance/sample_performance_data.dart'; 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 97% 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 3da5ba61a72..fc0f6899b5b 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:flutter_test/flutter_test.dart'; 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 68c59e34d5a..90cb00bdf81 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 @@ void main() { 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 @@ void main() { .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 67d30a036be..900f9c11db9 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 e42cffdca29..5f3b09ef998 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 @@ class _VsCodeFlutterPanelMockEditorState /// The last [maxLogEvents] communication messages sent between the panel /// and the "host IDE". - final logRing = DoubleLinkedQueue(); + final logRing = ListQueue(); /// A stream that emits each time the log is updated to allow the log widget /// to be rebuilt.