Skip to content

Commit

Permalink
Add a ring buffer for Perfetto timeline traces (flutter#7403)
Browse files Browse the repository at this point in the history
  • Loading branch information
kenzieschmoll committed Mar 21, 2024
1 parent 6f92735 commit bd2566c
Show file tree
Hide file tree
Showing 16 changed files with 243 additions and 109 deletions.
7 changes: 7 additions & 0 deletions packages/.vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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.',
);
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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).
Expand Down Expand Up @@ -202,13 +201,13 @@ class PerfettoControllerImpl extends PerfettoController {
}

@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);
}

Expand All @@ -230,6 +229,6 @@ class PerfettoControllerImpl extends PerfettoController {
@override
Future<void> clear() async {
processor.clear();
await loadTrace(Trace());
await loadTrace(Uint8List(0));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -47,7 +45,7 @@ class _PerfettoState extends State<Perfetto> 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);
Expand All @@ -60,10 +58,10 @@ class _PerfettoState extends State<Perfetto> with AutoDisposeMixin {
}

void _loadActiveTrace() {
assert(_perfettoController.activeTrace.trace != null);
assert(_perfettoController.activeTrace.traceBinary != null);
unawaited(
_viewController._loadPerfettoTrace(
_perfettoController.activeTrace.trace!,
_perfettoController.activeTrace.traceBinary!,
),
);
}
Expand Down Expand Up @@ -161,26 +159,22 @@ class _PerfettoViewController extends DisposableController
);
}

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,
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -42,7 +43,7 @@ abstract class PerfettoController extends DisposableController {

void onBecomingActive() {}

Future<void> loadTrace(Trace trace) async {}
Future<void> loadTrace(Uint8List traceBinary) async {}

void scrollToTimeRange(TimeRange timeRange) {}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<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();
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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');

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -47,6 +48,7 @@ class TimelineEventsController extends PerformanceFeatureController
_status.value = EventsControllerStatus.ready;
}
});
traceRingBuffer = Uint8ListRingBuffer(maxSizeBytes: _traceRingBufferSize);
}

static const uiThreadSuffix = '.ui';
Expand All @@ -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.
Expand Down Expand Up @@ -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,
}) {
Expand All @@ -219,7 +224,7 @@ class TimelineEventsController extends PerformanceFeatureController
() => _log
.info('[_prepareTraceForProcessing] not a flutter app, returning.'),
);
return null;
return;
}

final trace = _traceFromBinary(traceBinary);
Expand All @@ -239,7 +244,6 @@ class TimelineEventsController extends PerformanceFeatureController
}
}
updateTrackIds(newTrackDescriptors, logWarning: logWarning);
return trace;
}

void updateTrackIds(
Expand Down Expand Up @@ -342,8 +346,7 @@ class TimelineEventsController extends PerformanceFeatureController
}

Future<void> loadPerfettoTrace() async {
debugTraceCallback(() => _log.info('[loadPerfettoTrace] updating viewer'));
await perfettoController.loadTrace(fullPerfettoTrace ?? Trace());
await perfettoController.loadTrace(fullPerfettoTrace);
}

@override
Expand Down Expand Up @@ -486,7 +489,7 @@ class TimelineEventsController extends PerformanceFeatureController
@override
Future<void> clearData() async {
_unprocessedTrackEvents.clear();
fullPerfettoTrace = Trace();
traceRingBuffer.clear();
_trackDescriptors.clear();
_unassignedFlutterTimelineEvents.clear();

Expand Down
Loading

0 comments on commit bd2566c

Please sign in to comment.