diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 5a4cc7b8..7902471a 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -1,5 +1,8 @@ name: CI +# TODO(polina-c): configure auto-update for diagrams +# https://github.com/dart-lang/leak_tracker/issues/104 + on: schedule: # “At 00:00 (UTC) on Sunday.” diff --git a/README.md b/README.md index fdeb7e7b..94751cf2 100644 --- a/README.md +++ b/README.md @@ -33,3 +33,11 @@ To temporary enable logs, add this line to `main`: ``` Logger.root.onRecord.listen((LogRecord record) => print(record.message)); ``` + +## How to regenerate diagrams + +To regenerate diagrams, run: + +``` +dart run layerlens +``` diff --git a/doc/TROUBLESHOOT.md b/doc/TROUBLESHOOT.md index 52ff760b..7d189821 100644 --- a/doc/TROUBLESHOOT.md +++ b/doc/TROUBLESHOOT.md @@ -47,14 +47,6 @@ For collecting debugging information in tests, temporarily pass an instance of ` }, leakTrackingTestConfig: LeakTrackingTestConfig.debug()); ``` -Or, you can temporarily set global flag, to make all tests collecting debug information: - -``` -setUpAll(() { - LeakTrackerGlobalFlags.collectDebugInformationForLeaks = true; -}); -``` - **Applications** For collecting debugging information in your running application, the options are: @@ -111,7 +103,7 @@ also become unreachable, and thus available for garbage collection. One of signs that some leaks still exist is fixing a leak by releasing link to child in the parent's `dispose`, because link to not needed parent should be released itself, together with its disposal. If -your fix for a leak is like this, you are defenitely hiding leaks of non-tracked objects: +your fix for a leak is like this, you are defenitely hiding leaks of non-tracked objects: ``` void dispose() { diff --git a/examples/minimal_flutter/lib/main.dart b/examples/minimal_flutter/lib/main.dart index a4aa27f2..343a201e 100644 --- a/examples/minimal_flutter/lib/main.dart +++ b/examples/minimal_flutter/lib/main.dart @@ -3,9 +3,10 @@ import 'package:flutter/material.dart'; import 'package:leak_tracker/leak_tracker.dart'; void main() { - enableLeakTracking(); - MemoryAllocations.instance - .addListener((ObjectEvent event) => dispatchObjectEvent(event.toMap())); + LeakTracking.start(); + MemoryAllocations.instance.addListener( + (ObjectEvent event) => LeakTracking.dispatchObjectEvent(event.toMap()), + ); runApp(const MyApp()); } diff --git a/pkgs/leak_tracker/CHANGELOG.md b/pkgs/leak_tracker/CHANGELOG.md index 32691e71..5dd29368 100644 --- a/pkgs/leak_tracker/CHANGELOG.md +++ b/pkgs/leak_tracker/CHANGELOG.md @@ -1,5 +1,8 @@ # 9.0.0 +* Remove global flag [collectDebugInformationForLeaks]. +* Rename `checkNonGCed` to `checkNotGCed`. +* Group global items related to leak tracking, in abstract class LeakTracking. * Rename `gcCountBuffer` to `numberOfGcCycles` and `disposalTimeBuffer` to `disposalTime`. # 8.0.3 diff --git a/pkgs/leak_tracker/analysis_options.yaml b/pkgs/leak_tracker/analysis_options.yaml index bdf4f4f2..b838293c 100644 --- a/pkgs/leak_tracker/analysis_options.yaml +++ b/pkgs/leak_tracker/analysis_options.yaml @@ -34,7 +34,7 @@ linter: # - avoid_bool_literals_in_conditional_expressions # not yet tested # - avoid_catches_without_on_clauses # we do this commonly # - avoid_catching_errors # we do this commonly - - avoid_classes_with_only_static_members + # - avoid_classes_with_only_static_members # - avoid_double_and_int_checks # only useful when targeting JS runtime - avoid_empty_else - avoid_field_initializers_in_const_classes diff --git a/pkgs/leak_tracker/example/main.dart b/pkgs/leak_tracker/example/main.dart index 9c95c7c7..fb4765e9 100644 --- a/pkgs/leak_tracker/example/main.dart +++ b/pkgs/leak_tracker/example/main.dart @@ -1,8 +1,8 @@ import 'package:leak_tracker/leak_tracker.dart'; void main(List arguments) { - enableLeakTracking(); + LeakTracking.start(); // ignore: avoid_print print('Hello, world!'); - disableLeakTracking(); + LeakTracking.stop(); } diff --git a/pkgs/leak_tracker/lib/DEPENDENCIES.md b/pkgs/leak_tracker/lib/DEPENDENCIES.md index cc8d4e49..8d2c05d5 100644 --- a/pkgs/leak_tracker/lib/DEPENDENCIES.md +++ b/pkgs/leak_tracker/lib/DEPENDENCIES.md @@ -7,6 +7,5 @@ Dependencies that create loop are markes with `!`. flowchart TD; devtools_integration.dart-->src; leak_tracker.dart-->src; -testing.dart-->src; ``` diff --git a/pkgs/leak_tracker/lib/leak_tracker.dart b/pkgs/leak_tracker/lib/leak_tracker.dart index 84609155..cba7a79f 100644 --- a/pkgs/leak_tracker/lib/leak_tracker.dart +++ b/pkgs/leak_tracker/lib/leak_tracker.dart @@ -2,8 +2,8 @@ // for details. All rights reserved. Use of this source code is governed by a // BSD-style license that can be found in the LICENSE file. -export 'src/leak_tracking/leak_tracker.dart'; -export 'src/leak_tracking/leak_tracker_model.dart'; +export 'src/leak_tracking/leak_tracking.dart'; +export 'src/leak_tracking/model.dart'; export 'src/leak_tracking/orchestration.dart'; export 'src/shared/shared_model.dart'; export 'src/usage_tracking/model.dart'; diff --git a/pkgs/leak_tracker/lib/src/DEPENDENCIES.md b/pkgs/leak_tracker/lib/src/DEPENDENCIES.md index b4c5149a..9ea699d7 100644 --- a/pkgs/leak_tracker/lib/src/DEPENDENCIES.md +++ b/pkgs/leak_tracker/lib/src/DEPENDENCIES.md @@ -8,7 +8,6 @@ flowchart TD; devtools_integration-->shared; leak_tracking-->devtools_integration; leak_tracking-->shared; -testing-->shared; usage_tracking-->shared; ``` diff --git a/pkgs/leak_tracker/lib/src/devtools_integration/_registration.dart b/pkgs/leak_tracker/lib/src/devtools_integration/_registration.dart index 7f984ead..dae305ee 100644 --- a/pkgs/leak_tracker/lib/src/devtools_integration/_registration.dart +++ b/pkgs/leak_tracker/lib/src/devtools_integration/_registration.dart @@ -27,8 +27,8 @@ bool registerLeakTrackingServiceExtension() => _registerServiceExtension( /// Registers service extension for DevTools integration. /// /// If the extension is already registered, returns false. -bool setupDevToolsIntegration( - ObjectRef leakProvider, +bool initializeDevToolsIntegration( + ObjectRef?> leakProvider, ) { Future handler( String method, @@ -37,7 +37,7 @@ bool setupDevToolsIntegration( try { assert(method == memoryLeakTrackingExtensionName); - final theLeakProvider = leakProvider.value; + final theLeakProvider = leakProvider.value?.target; if (theLeakProvider == null) { return ResponseFromApp(LeakTrackingTurnedOffError()) diff --git a/pkgs/leak_tracker/lib/src/devtools_integration/delivery.dart b/pkgs/leak_tracker/lib/src/devtools_integration/delivery.dart index de19af88..73306a90 100644 --- a/pkgs/leak_tracker/lib/src/devtools_integration/delivery.dart +++ b/pkgs/leak_tracker/lib/src/devtools_integration/delivery.dart @@ -7,7 +7,7 @@ import 'dart:developer'; import 'package:vm_service/vm_service.dart'; -import '../leak_tracking/_formatting.dart'; +import '../shared/_formatting.dart'; import '_protocol.dart'; import 'primitives.dart'; diff --git a/pkgs/leak_tracker/lib/src/leak_tracking/DEPENDENCIES.md b/pkgs/leak_tracker/lib/src/leak_tracking/DEPENDENCIES.md index 1fa58ae9..6cc2eab0 100644 --- a/pkgs/leak_tracker/lib/src/leak_tracking/DEPENDENCIES.md +++ b/pkgs/leak_tracker/lib/src/leak_tracking/DEPENDENCIES.md @@ -6,18 +6,21 @@ Dependencies that create loop are markes with `!`. ```mermaid flowchart TD; _dispatcher.dart-->_object_tracker.dart; -_leak_checker.dart-->leak_tracker_model.dart; +_leak_reporter.dart-->model.dart; +_leak_tracker.dart-->_leak_reporter.dart; +_leak_tracker.dart-->_object_tracker.dart; +_leak_tracker.dart-->model.dart; _object_record.dart-->_gc_counter.dart; +_object_tracker.dart-->_finalizer.dart; _object_tracker.dart-->_gc_counter.dart; _object_tracker.dart-->_object_record.dart; -_object_tracker.dart-->leak_tracker_model.dart; -_object_tracker.dart-->retaining_path; -leak_tracker.dart-->_dispatcher.dart; -leak_tracker.dart-->_leak_checker.dart; -leak_tracker.dart-->_object_tracker.dart; -leak_tracker.dart-->leak_tracker_model.dart; -orchestration.dart-->_gc_counter.dart; -orchestration.dart-->leak_tracker.dart; -orchestration.dart-->leak_tracker_model.dart; +_object_tracker.dart-->_retaining_path; +_object_tracker.dart-->model.dart; +leak_tracking.dart-->_dispatcher.dart; +leak_tracking.dart-->_leak_tracker.dart; +leak_tracking.dart-->model.dart; +orchestration.dart-->_retaining_path; +orchestration.dart-->leak_tracking.dart; +orchestration.dart-->model.dart; ``` diff --git a/pkgs/leak_tracker/lib/src/leak_tracking/_leak_checker.dart b/pkgs/leak_tracker/lib/src/leak_tracking/_leak_reporter.dart similarity index 96% rename from pkgs/leak_tracker/lib/src/leak_tracking/_leak_checker.dart rename to pkgs/leak_tracker/lib/src/leak_tracking/_leak_reporter.dart index d8156ebf..c24e58f1 100644 --- a/pkgs/leak_tracker/lib/src/leak_tracking/_leak_checker.dart +++ b/pkgs/leak_tracker/lib/src/leak_tracking/_leak_reporter.dart @@ -7,14 +7,14 @@ import 'dart:async'; import '../devtools_integration/delivery.dart'; import '../shared/_util.dart'; import '../shared/shared_model.dart'; -import 'leak_tracker_model.dart'; +import 'model.dart'; /// Checks [leakProvider] either by schedule or by request. /// /// If there are leaks, reports them to the enabled outputs: /// listener, console and DevTools. -class LeakChecker { - LeakChecker({ +class LeakReporter { + LeakReporter({ required this.leakProvider, required this.checkPeriod, required this.onLeaks, diff --git a/pkgs/leak_tracker/lib/src/leak_tracking/_leak_tracker.dart b/pkgs/leak_tracker/lib/src/leak_tracking/_leak_tracker.dart new file mode 100644 index 00000000..5d6909d0 --- /dev/null +++ b/pkgs/leak_tracker/lib/src/leak_tracking/_leak_tracker.dart @@ -0,0 +1,35 @@ +// Copyright (c) 2022, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import '_leak_reporter.dart'; +import '_object_tracker.dart'; +import 'model.dart'; + +class LeakTracker { + LeakTracker(LeakTrackingConfiguration config) { + objectTracker = ObjectTracker( + leakDiagnosticConfig: config.leakDiagnosticConfig, + disposalTime: config.disposalTime, + numberOfGcCycles: config.numberOfGcCycles, + maxRequestsForRetainingPath: config.maxRequestsForRetainingPath, + ); + + leakReporter = LeakReporter( + leakProvider: objectTracker, + checkPeriod: config.checkPeriod, + onLeaks: config.onLeaks, + stdoutSink: config.stdoutLeaks ? StdoutSummarySink() : null, + devToolsSink: config.notifyDevTools ? DevToolsSummarySink() : null, + ); + } + + late final ObjectTracker objectTracker; + + late final LeakReporter leakReporter; + + void dispose() { + objectTracker.dispose(); + leakReporter.dispose(); + } +} diff --git a/pkgs/leak_tracker/lib/src/leak_tracking/_object_tracker.dart b/pkgs/leak_tracker/lib/src/leak_tracking/_object_tracker.dart index 42035cf9..06fda7df 100644 --- a/pkgs/leak_tracker/lib/src/leak_tracking/_object_tracker.dart +++ b/pkgs/leak_tracker/lib/src/leak_tracking/_object_tracker.dart @@ -12,9 +12,9 @@ import '../shared/shared_model.dart'; import '_finalizer.dart'; import '_gc_counter.dart'; import '_object_record.dart'; -import 'leak_tracker_model.dart'; -import 'retaining_path/_connection.dart'; -import 'retaining_path/_retaining_path.dart'; +import '_retaining_path/_connection.dart'; +import '_retaining_path/_retaining_path.dart'; +import 'model.dart'; /// Keeps collection of object records until /// disposal and garbage gollection. @@ -27,6 +27,7 @@ class ObjectTracker implements LeakProvider { this.leakDiagnosticConfig = const LeakDiagnosticConfig(), required this.disposalTime, required this.numberOfGcCycles, + required this.maxRequestsForRetainingPath, FinalizerBuilder? finalizerBuilder, GcCounter? gcCounter, IdentityHashCoder? coder, @@ -54,6 +55,8 @@ class ObjectTracker implements LeakProvider { final int numberOfGcCycles; + final int? maxRequestsForRetainingPath; + void startTracking( Object object, { required Map? context, @@ -192,7 +195,7 @@ class ObjectTracker implements LeakProvider { await processIfNeeded( items: objectsToGetPath, - limit: LeakTrackerGlobalSettings.maxRequestsForRetainingPath, + limit: maxRequestsForRetainingPath, processor: _addRetainingPath, ); diff --git a/pkgs/leak_tracker/lib/src/leak_tracking/retaining_path/DEPENDENCIES.md b/pkgs/leak_tracker/lib/src/leak_tracking/_retaining_path/DEPENDENCIES.md similarity index 77% rename from pkgs/leak_tracker/lib/src/leak_tracking/retaining_path/DEPENDENCIES.md rename to pkgs/leak_tracker/lib/src/leak_tracking/_retaining_path/DEPENDENCIES.md index 4ca77646..30acac45 100644 --- a/pkgs/leak_tracker/lib/src/leak_tracking/retaining_path/DEPENDENCIES.md +++ b/pkgs/leak_tracker/lib/src/leak_tracking/_retaining_path/DEPENDENCIES.md @@ -5,6 +5,6 @@ Dependencies that create loop are markes with `!`. ```mermaid flowchart TD; -_retaining_path.dart-->_service.dart; +_retaining_path.dart-->_connection.dart; ``` diff --git a/pkgs/leak_tracker/lib/src/leak_tracking/retaining_path/_connection.dart b/pkgs/leak_tracker/lib/src/leak_tracking/_retaining_path/_connection.dart similarity index 100% rename from pkgs/leak_tracker/lib/src/leak_tracking/retaining_path/_connection.dart rename to pkgs/leak_tracker/lib/src/leak_tracking/_retaining_path/_connection.dart diff --git a/pkgs/leak_tracker/lib/src/leak_tracking/retaining_path/_retaining_path.dart b/pkgs/leak_tracker/lib/src/leak_tracking/_retaining_path/_retaining_path.dart similarity index 100% rename from pkgs/leak_tracker/lib/src/leak_tracking/retaining_path/_retaining_path.dart rename to pkgs/leak_tracker/lib/src/leak_tracking/_retaining_path/_retaining_path.dart diff --git a/pkgs/leak_tracker/lib/src/leak_tracking/leak_tracker.dart b/pkgs/leak_tracker/lib/src/leak_tracking/leak_tracker.dart deleted file mode 100644 index 59b9521a..00000000 --- a/pkgs/leak_tracker/lib/src/leak_tracking/leak_tracker.dart +++ /dev/null @@ -1,195 +0,0 @@ -// Copyright (c) 2022, the Dart project authors. Please see the AUTHORS file -// for details. All rights reserved. Use of this source code is governed by a -// BSD-style license that can be found in the LICENSE file. - -import '../devtools_integration/_registration.dart'; -import '../shared/_primitives.dart'; -import '../shared/shared_model.dart'; -import '_dispatcher.dart' as dispatcher; -import '_leak_checker.dart'; -import '_object_tracker.dart'; -import 'leak_tracker_model.dart'; - -final _objectTracker = ObjectRef(null); -LeakChecker? _leakChecker; - -ObjectTracker _theObjectTracker() { - // TODO(polina-c): return both tracker and checker when tuples get released. - final result = _objectTracker.value; - assert((result == null) == (_leakChecker == null)); - if (result == null) throw StateError('Leak tracking should be enabled.'); - return result; -} - -/// Enables leak tracking for the application. -/// -/// The leak tracking will function only for debug/profile/developer mode. -/// See usage guidance at https://github.com/dart-lang/leak_tracker. -/// -/// If [resetIfAlreadyEnabled] is true and leak tracking is already on, -/// the tracking will be reset with new configuration. -/// -/// If [resetIfAlreadyEnabled] is true and leak tracking is already on, -/// [StateError] will be thrown. -void enableLeakTracking({ - LeakTrackingConfiguration? config, - bool resetIfAlreadyEnabled = false, -}) { - assert(() { - final theConfig = config ??= const LeakTrackingConfiguration(); - if (_objectTracker.value != null) { - if (!resetIfAlreadyEnabled) { - throw StateError('Leak tracking is already enabled.'); - } - disableLeakTracking(); - } - - final newTracker = ObjectTracker( - leakDiagnosticConfig: theConfig.leakDiagnosticConfig, - disposalTime: theConfig.disposalTime, - numberOfGcCycles: theConfig.numberOfGcCycles, - ); - - _objectTracker.value = newTracker; - - _leakChecker = LeakChecker( - leakProvider: newTracker, - checkPeriod: theConfig.checkPeriod, - onLeaks: theConfig.onLeaks, - stdoutSink: theConfig.stdoutLeaks ? StdoutSummarySink() : null, - devToolsSink: theConfig.notifyDevTools ? DevToolsSummarySink() : null, - ); - - if (theConfig.notifyDevTools) { - setupDevToolsIntegration(_objectTracker); - } else { - registerLeakTrackingServiceExtension(); - } - return true; - }()); -} - -/// Disables leak tracking for the application. -/// -/// See usage guidance at https://github.com/dart-lang/leak_tracker. -void disableLeakTracking() { - assert(() { - _leakChecker?.dispose(); - _leakChecker = null; - _objectTracker.value?.dispose(); - _objectTracker.value = null; - - return true; - }()); -} - -/// Dispatches an object event to the leak tracker. -/// -/// Consumes the MemoryAllocations event format: -/// https://github.com/flutter/flutter/blob/a479718b02a818fb4ac8d4900bf08ca389cd8e7d/packages/flutter/lib/src/foundation/memory_allocations.dart#L51 -void dispatchObjectEvent(Map> event) { - assert(() { - dispatcher.dispatchObjectEvent(event, _objectTracker.value); - - return true; - }()); -} - -/// Dispatches object creation to the leak tracker. -/// -/// Use [context] to provide additional information, that may help in leek troubleshooting. -/// The value must be serializable. -void dispatchObjectCreated({ - required String library, - required String className, - required Object object, - Map? context, -}) { - assert(() { - final tracker = _objectTracker.value; - if (tracker == null) return true; - - tracker.startTracking( - object, - context: context, - trackedClass: fullClassName(library: library, shortClassName: className), - ); - - return true; - }()); -} - -/// Dispatches object disposal to the leak tracker. -/// -/// See [dispatchObjectCreated] for parameters documentation. -void dispatchObjectDisposed({ - required Object object, - Map? context, -}) { - assert(() { - final tracker = _objectTracker.value; - if (tracker == null) return true; - - tracker.dispatchDisposal(object, context: context); - return true; - }()); -} - -/// Dispatches additional context information to the leak tracker. -/// -/// See [dispatchObjectCreated] for parameters documentation. -void dispatchObjectTrace({ - required Object object, - Map? context, -}) { - assert(() { - _theObjectTracker().addContext(object, context: context); - return true; - }()); -} - -/// Checks for leaks and outputs [LeakSummary] as configured. -Future checkLeaks() async { - Future? result; - - assert(() { - // TODO(polina-c): get checker as result when tuples are released. - _theObjectTracker(); - result = _leakChecker!.checkLeaks(); - - return true; - }()); - - return await (result ?? Future.value(LeakSummary({}))); -} - -/// Returns details of the leaks collected since last invocation. -/// -/// The same object may be reported as leaked twice: first -/// as non GCed, and then as GCed late. -Future collectLeaks() async { - Future? result; - - assert(() { - result = _theObjectTracker().collectLeaks(); - return true; - }()); - - return await (result ?? Future.value(Leaks({}))); -} - -/// Checks for new not GCed leaks. -/// -/// Invoke this method to detect the leaks earlier, when -/// the leaked objects are not GCed yet, -/// to obtain retaining path. -Future checkNonGCed() async { - Future? result; - - assert(() { - result = _theObjectTracker().checkNonGCed(); - return true; - }()); - - await (result ?? Future.value()); -} diff --git a/pkgs/leak_tracker/lib/src/leak_tracking/leak_tracking.dart b/pkgs/leak_tracker/lib/src/leak_tracking/leak_tracking.dart new file mode 100644 index 00000000..ee451d0b --- /dev/null +++ b/pkgs/leak_tracker/lib/src/leak_tracking/leak_tracking.dart @@ -0,0 +1,178 @@ +// Copyright (c) 2022, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import '../devtools_integration/_registration.dart'; +import '../shared/_primitives.dart'; +import '../shared/shared_model.dart'; +import '_dispatcher.dart' as dispatcher; +import '_leak_tracker.dart'; +import 'model.dart'; + +/// Provides leak tracking functionality. +abstract class LeakTracking { + static LeakTracker? _leakTracker; + + /// Leak provider, used for integration with DevTools. + /// + /// It's value should be updated every time leak tracking is reconfigured. + static final _leakProvider = ObjectRef?>(null); + + /// Returns true if leak tracking is configured. + static bool get isStarted => _leakTracker != null; + + /// If true, a warning will be printed when leak tracking is + /// requested for a non-supported platform. + static bool warnForNonSupportedPlatforms = true; + + /// Configures leak tracking for the application. + /// + /// The leak tracking will function only for debug/profile/developer mode. + /// See usage guidance at https://github.com/dart-lang/leak_tracker. + /// + /// If [resetIfAlreadyStarted] is true and leak tracking is already on, + /// the tracking will be reset with new configuration. + /// + /// If [resetIfAlreadyStarted] is false and leak tracking is already on, + /// [StateError] will be thrown. + static void start({ + LeakTrackingConfiguration config = const LeakTrackingConfiguration(), + bool resetIfAlreadyStarted = false, + }) { + assert(() { + if (_leakTracker != null) { + if (!resetIfAlreadyStarted) { + throw StateError('Leak tracking is already enabled.'); + } + stop(); + } + + final leakTracker = _leakTracker = LeakTracker(config); + _leakProvider.value = WeakReference(leakTracker.objectTracker); + + if (config.notifyDevTools) { + // While [leakTracker] will push summary leak notifications to DevTools, + // DevTools may request leak details from the application via integration. + // That's why it needs [_leakProvider]. + initializeDevToolsIntegration(_leakProvider); + } else { + registerLeakTrackingServiceExtension(); + } + return true; + }()); + } + + /// Stops leak tracking for the application. + /// + /// See usage guidance at https://github.com/dart-lang/leak_tracker. + static void stop() { + assert(() { + _leakTracker?.dispose(); + _leakTracker = null; + return true; + }()); + } + + /// Dispatches an object event to the leak tracker. + /// + /// Consumes the MemoryAllocations event format: + /// https://github.com/flutter/flutter/blob/a479718b02a818fb4ac8d4900bf08ca389cd8e7d/packages/flutter/lib/src/foundation/memory_allocations.dart#L51 + static void dispatchObjectEvent(Map> event) { + assert(() { + dispatcher.dispatchObjectEvent(event, _leakTracker?.objectTracker); + + return true; + }()); + } + + /// Dispatches object creation to the leak tracker. + /// + /// Use [context] to provide additional information, that may help in leek troubleshooting. + /// The value must be serializable. + static void dispatchObjectCreated({ + required String library, + required String className, + required Object object, + Map? context, + }) { + assert(() { + _leakTracker?.objectTracker.startTracking( + object, + context: context, + trackedClass: + fullClassName(library: library, shortClassName: className), + ); + + return true; + }()); + } + + /// Dispatches object disposal to the leak tracker. + /// + /// See [dispatchObjectCreated] for parameters documentation. + static void dispatchObjectDisposed({ + required Object object, + Map? context, + }) { + assert(() { + _leakTracker?.objectTracker.dispatchDisposal(object, context: context); + return true; + }()); + } + + /// Dispatches additional context information to the leak tracker. + /// + /// See [dispatchObjectCreated] for parameters documentation. + static void dispatchObjectTrace({ + required Object object, + Map? context, + }) { + assert(() { + _leakTracker?.objectTracker.addContext(object, context: context); + return true; + }()); + } + + /// Checks for leaks and outputs [LeakSummary] as configured. + static Future checkLeaks() async { + Future? result; + + assert(() { + result = _leakTracker?.leakReporter.checkLeaks(); + return true; + }()); + + return await (result ?? Future.value(LeakSummary({}))); + } + + /// Returns details of the leaks collected since last invocation. + /// + /// The same object may be reported as leaked twice: first + /// as non GCed, and then as GCed late. + static Future collectLeaks() async { + Future? result; + + assert(() { + result = _leakTracker?.objectTracker.collectLeaks(); + return true; + }()); + + return await (result ?? Future.value(Leaks({}))); + } + + /// Checks for new not-GCed leaks. + /// + /// Invoke this method to detect the leaks earlier, when + /// the leaked objects are not GCed yet, + /// to obtain retaining path. + static Future checkNotGCed() async { + Future? result; + + assert(() { + result = _leakTracker?.objectTracker.checkNonGCed(); + return true; + }()); + + await (result ?? Future.value()); + } +} diff --git a/pkgs/leak_tracker/lib/src/leak_tracking/leak_tracker_model.dart b/pkgs/leak_tracker/lib/src/leak_tracking/model.dart similarity index 93% rename from pkgs/leak_tracker/lib/src/leak_tracking/leak_tracker_model.dart rename to pkgs/leak_tracker/lib/src/leak_tracking/model.dart index d8b678e9..4fb509a8 100644 --- a/pkgs/leak_tracker/lib/src/leak_tracking/leak_tracker_model.dart +++ b/pkgs/leak_tracker/lib/src/leak_tracking/model.dart @@ -4,24 +4,6 @@ import '../shared/shared_model.dart'; -// ignore: avoid_classes_with_only_static_members, as it is ok for enum-like classes. -/// Global settings for leak tracker. -class LeakTrackerGlobalSettings { - /// If true, the leak tracker will collect debug information for leaks. - static bool collectDebugInformationForLeaks = false; - - /// If true, a warning will be printed when leak tracking is - /// requested for a non-supported platform. - static bool warnForNonSupportedPlatforms = true; - - /// Limit for number of requests for retaining path per one round - /// of validation for leaks. - /// - /// If the number is too big, the performance may be seriously impacted. - /// If null, path will be requested without limit. - static int? maxRequestsForRetainingPath = 10; -} - /// Handler to collect leak summary. typedef LeakSummaryCallback = void Function(LeakSummary); @@ -87,6 +69,9 @@ class LeakDiagnosticConfig { /// if there is no activity in the application for ~5 minutes. const defaultNumberOfGcCycles = 3; +/// Leak tracking configuration. +/// +/// Contains settings that cannot be changed after leak tracking is started. class LeakTrackingConfiguration { const LeakTrackingConfiguration({ this.stdoutLeaks = true, @@ -96,6 +81,7 @@ class LeakTrackingConfiguration { this.disposalTime = const Duration(milliseconds: 100), this.leakDiagnosticConfig = const LeakDiagnosticConfig(), this.numberOfGcCycles = defaultNumberOfGcCycles, + this.maxRequestsForRetainingPath = 10, }); /// The leak tracker: @@ -106,6 +92,7 @@ class LeakTrackingConfiguration { LeakTrackingConfiguration.passive({ LeakDiagnosticConfig leakDiagnosticConfig = const LeakDiagnosticConfig(), int numberOfGcCycles = defaultNumberOfGcCycles, + int? maxRequestsForRetainingPath = 10, }) : this( stdoutLeaks: false, notifyDevTools: false, @@ -113,6 +100,7 @@ class LeakTrackingConfiguration { disposalTime: const Duration(), leakDiagnosticConfig: leakDiagnosticConfig, numberOfGcCycles: numberOfGcCycles, + maxRequestsForRetainingPath: maxRequestsForRetainingPath, ); /// Number of full GC cycles, enough for a non reachable object to be GCed. @@ -136,6 +124,13 @@ class LeakTrackingConfiguration { /// Time to allow the disposal invoker to release the reference to the object. final Duration disposalTime; + + /// Limit for number of requests for retaining path per one round + /// of validation for leaks. + /// + /// If the number is too big, the performance may be seriously impacted. + /// If null, the path will be srequested without limit. + final int? maxRequestsForRetainingPath; } /// Configuration for leak tracking in unit tests. diff --git a/pkgs/leak_tracker/lib/src/leak_tracking/orchestration.dart b/pkgs/leak_tracker/lib/src/leak_tracking/orchestration.dart index 8a87334b..9238304f 100644 --- a/pkgs/leak_tracker/lib/src/leak_tracking/orchestration.dart +++ b/pkgs/leak_tracker/lib/src/leak_tracking/orchestration.dart @@ -7,12 +7,13 @@ import 'dart:developer'; import 'package:logging/logging.dart'; +import '../shared/_formatting.dart'; import '../shared/shared_model.dart'; -import '_formatting.dart'; -import 'leak_tracker.dart'; -import 'leak_tracker_model.dart'; -import 'retaining_path/_connection.dart'; -import 'retaining_path/_retaining_path.dart'; +import '_retaining_path/_connection.dart'; +import '_retaining_path/_retaining_path.dart'; + +import 'leak_tracking.dart'; +import 'model.dart'; final _log = Logger('orchestration.dart'); @@ -88,6 +89,7 @@ Future withLeakTracking( LeakDiagnosticConfig leakDiagnosticConfig = const LeakDiagnosticConfig(), AsyncCodeRunner? asyncCodeRunner, int numberOfGcCycles = defaultNumberOfGcCycles, + int? maxRequestsForRetainingPath = 10, }) async { if (numberOfGcCycles <= 0) { throw ArgumentError.value( @@ -99,11 +101,12 @@ Future withLeakTracking( if (callback == null) return Leaks({}); - enableLeakTracking( - resetIfAlreadyEnabled: true, + LeakTracking.start( + resetIfAlreadyStarted: true, config: LeakTrackingConfiguration.passive( leakDiagnosticConfig: leakDiagnosticConfig, numberOfGcCycles: numberOfGcCycles, + maxRequestsForRetainingPath: maxRequestsForRetainingPath, ), ); @@ -119,14 +122,14 @@ Future withLeakTracking( if (leakDiagnosticConfig.collectRetainingPathForNonGCed) { // This early check is needed to collect retaing paths before forced GC, // because paths are unavailable for GCed objects. - await checkNonGCed(); + await LeakTracking.checkNotGCed(); } await forceGC( fullGcCycles: numberOfGcCycles, timeout: timeoutForFinalGarbageCollection, ); - leaks = await collectLeaks(); + leaks = await LeakTracking.collectLeaks(); if ((leaks?.total ?? 0) > 0 && shouldThrowOnLeaks) { // `expect` should not be used here, because, when the method is used // from Flutter, the packages `test` and `flutter_test` conflict. @@ -139,7 +142,7 @@ Future withLeakTracking( if (leaks == null) throw StateError('Leaks collection failed.'); return leaks!; } finally { - disableLeakTracking(); + LeakTracking.stop(); } } diff --git a/pkgs/leak_tracker/lib/src/shared/DEPENDENCIES.md b/pkgs/leak_tracker/lib/src/shared/DEPENDENCIES.md index c1d00302..747750ef 100644 --- a/pkgs/leak_tracker/lib/src/shared/DEPENDENCIES.md +++ b/pkgs/leak_tracker/lib/src/shared/DEPENDENCIES.md @@ -5,6 +5,9 @@ Dependencies that create loop are markes with `!`. ```mermaid flowchart TD; +_formatting.dart-->_primitives.dart; +_formatting.dart-->_util.dart; +shared_model.dart-->_formatting.dart; shared_model.dart-->_primitives.dart; shared_model.dart-->_util.dart; ``` diff --git a/pkgs/leak_tracker/lib/src/leak_tracking/_formatting.dart b/pkgs/leak_tracker/lib/src/shared/_formatting.dart similarity index 98% rename from pkgs/leak_tracker/lib/src/leak_tracking/_formatting.dart rename to pkgs/leak_tracker/lib/src/shared/_formatting.dart index efb6e4ca..cc6dd5e0 100644 --- a/pkgs/leak_tracker/lib/src/leak_tracking/_formatting.dart +++ b/pkgs/leak_tracker/lib/src/shared/_formatting.dart @@ -4,8 +4,8 @@ import 'package:vm_service/vm_service.dart'; -import '../shared/_primitives.dart'; -import '../shared/_util.dart'; +import '_primitives.dart'; +import '_util.dart'; /// Converts item in leak tracking context to string. String contextToString(Object? object) { diff --git a/pkgs/leak_tracker/lib/src/shared/shared_model.dart b/pkgs/leak_tracker/lib/src/shared/shared_model.dart index 62725f7d..adad755d 100644 --- a/pkgs/leak_tracker/lib/src/shared/shared_model.dart +++ b/pkgs/leak_tracker/lib/src/shared/shared_model.dart @@ -4,7 +4,7 @@ import 'package:collection/collection.dart'; -import '../leak_tracking/_formatting.dart'; +import '_formatting.dart'; import '_primitives.dart'; import '_util.dart'; diff --git a/pkgs/leak_tracker/pubspec.yaml b/pkgs/leak_tracker/pubspec.yaml index 1de0306f..44270b5a 100644 --- a/pkgs/leak_tracker/pubspec.yaml +++ b/pkgs/leak_tracker/pubspec.yaml @@ -1,5 +1,5 @@ name: leak_tracker -version: 8.0.3 +version: 9.0.0-dev.1 description: A framework for memory leak tracking for Dart and Flutter applications. repository: https://github.com/dart-lang/leak_tracker/tree/main/pkgs/leak_tracker diff --git a/pkgs/leak_tracker/test/debug/leak_tracking/end_to_end_test.dart b/pkgs/leak_tracker/test/debug/leak_tracking/end_to_end_test.dart index 57687116..81601252 100644 --- a/pkgs/leak_tracker/test/debug/leak_tracking/end_to_end_test.dart +++ b/pkgs/leak_tracker/test/debug/leak_tracking/end_to_end_test.dart @@ -10,17 +10,12 @@ import '../../test_infra/data/dart_classes.dart'; /// Tests for non-mocked public API of leak tracker. void main() { - setUp(() { - LeakTrackerGlobalSettings.maxRequestsForRetainingPath = null; - }); - - tearDown(() => disableLeakTracking()); + tearDown(() => LeakTracking.stop()); for (var numberOfGcCycles in [1, defaultNumberOfGcCycles]) { test( 'Leak tracker respects maxRequestsForRetainingPath, $numberOfGcCycles.', () async { - LeakTrackerGlobalSettings.maxRequestsForRetainingPath = 2; final leaks = await withLeakTracking( () async { LeakingClass(); @@ -32,6 +27,7 @@ void main() { collectRetainingPathForNonGCed: true, ), numberOfGcCycles: numberOfGcCycles, + maxRequestsForRetainingPath: 2, ); const pathHeader = ' path: >'; diff --git a/pkgs/leak_tracker/test/debug/leak_tracking/retaining_path/_retaining_path_test.dart b/pkgs/leak_tracker/test/debug/leak_tracking/retaining_path/_retaining_path_test.dart index ac8f2458..d0059c61 100644 --- a/pkgs/leak_tracker/test/debug/leak_tracking/retaining_path/_retaining_path_test.dart +++ b/pkgs/leak_tracker/test/debug/leak_tracking/retaining_path/_retaining_path_test.dart @@ -4,8 +4,8 @@ import 'dart:async'; -import 'package:leak_tracker/src/leak_tracking/retaining_path/_connection.dart'; -import 'package:leak_tracker/src/leak_tracking/retaining_path/_retaining_path.dart'; +import 'package:leak_tracker/src/leak_tracking/_retaining_path/_connection.dart'; +import 'package:leak_tracker/src/leak_tracking/_retaining_path/_retaining_path.dart'; import 'package:logging/logging.dart'; import 'package:test/test.dart'; diff --git a/pkgs/leak_tracker/test/release/leak_tracking/_leak_checker_test.dart b/pkgs/leak_tracker/test/release/leak_tracking/_leak_checker_test.dart index 7c1074fd..943eb253 100644 --- a/pkgs/leak_tracker/test/release/leak_tracking/_leak_checker_test.dart +++ b/pkgs/leak_tracker/test/release/leak_tracking/_leak_checker_test.dart @@ -3,11 +3,9 @@ // BSD-style license that can be found in the LICENSE file. import 'package:leak_tracker/leak_tracker.dart'; -import 'package:leak_tracker/src/leak_tracking/_leak_checker.dart'; +import 'package:leak_tracker/src/leak_tracking/_leak_reporter.dart'; import 'package:test/test.dart'; -// Enum-like static classes are ok. -// ignore: avoid_classes_with_only_static_members class _SummaryValues { static final zero = LeakSummary({}); @@ -31,13 +29,13 @@ void main() { const period = Duration(milliseconds: 5); late final doublePeriod = Duration(microseconds: period.inMicroseconds); - LeakChecker leakChecker({ + LeakReporter leakChecker({ required bool checkPeriodically, required bool hasListener, required bool hasStdout, required bool hasDevtools, }) => - LeakChecker( + LeakReporter( leakProvider: leakProvider, checkPeriod: checkPeriodically ? period : null, onLeaks: hasListener ? (summary) => listened.store.add(summary) : null, @@ -45,7 +43,7 @@ void main() { devToolsSink: hasDevtools ? devtools : null, ); - LeakChecker defaultLeakChecker() => leakChecker( + LeakReporter defaultLeakChecker() => leakChecker( checkPeriodically: true, hasListener: false, hasStdout: true, diff --git a/pkgs/leak_tracker/test/release/leak_tracking/_object_tracker_test.dart b/pkgs/leak_tracker/test/release/leak_tracking/_object_tracker_test.dart index dc8541e8..b39884aa 100644 --- a/pkgs/leak_tracker/test/release/leak_tracking/_object_tracker_test.dart +++ b/pkgs/leak_tracker/test/release/leak_tracking/_object_tracker_test.dart @@ -81,6 +81,7 @@ void main() { disposalTime: _disposalTime, coder: mockCoder, numberOfGcCycles: defaultNumberOfGcCycles, + maxRequestsForRetainingPath: 0, ); }); @@ -147,6 +148,7 @@ void main() { gcCounter: gcCounter, disposalTime: _disposalTime, numberOfGcCycles: defaultNumberOfGcCycles, + maxRequestsForRetainingPath: 0, ); }); @@ -323,6 +325,7 @@ void main() { ), disposalTime: _disposalTime, numberOfGcCycles: defaultNumberOfGcCycles, + maxRequestsForRetainingPath: 0, ); }); diff --git a/pkgs/leak_tracker/test/release/leak_tracking/end_to_end_test.dart b/pkgs/leak_tracker/test/release/leak_tracking/end_to_end_test.dart index f13e1e54..6be08310 100644 --- a/pkgs/leak_tracker/test/release/leak_tracking/end_to_end_test.dart +++ b/pkgs/leak_tracker/test/release/leak_tracking/end_to_end_test.dart @@ -13,7 +13,7 @@ import '../../test_infra/data/dart_classes.dart'; /// Tests for non-mocked public API of leak tracker. void main() { tearDown(() { - disableLeakTracking(); + LeakTracking.stop(); }); for (var numberOfGcCycles in [1, defaultNumberOfGcCycles]) { diff --git a/pkgs/leak_tracker/test/test_infra/data/dart_classes.dart b/pkgs/leak_tracker/test/test_infra/data/dart_classes.dart index ddd54a9f..8dd3d750 100644 --- a/pkgs/leak_tracker/test/test_infra/data/dart_classes.dart +++ b/pkgs/leak_tracker/test/test_infra/data/dart_classes.dart @@ -2,11 +2,11 @@ // for details. All rights reserved. Use of this source code is governed by a // BSD-style license that can be found in the LICENSE file. -import 'package:leak_tracker/src/leak_tracking/leak_tracker.dart'; +import 'package:leak_tracker/leak_tracker.dart'; class LeakTrackedClass { LeakTrackedClass() { - dispatchObjectCreated( + LeakTracking.dispatchObjectCreated( library: library, className: '$LeakTrackedClass', object: this, @@ -16,7 +16,7 @@ class LeakTrackedClass { static const library = 'package:my_package/lib/src/my_lib.dart'; void dispose() { - dispatchObjectDisposed(object: this); + LeakTracking.dispatchObjectDisposed(object: this); } } diff --git a/pkgs/leak_tracker/test/test_infra/mocks/mock_object_tracker.dart b/pkgs/leak_tracker/test/test_infra/mocks/mock_object_tracker.dart index 797a8ac9..7738c162 100644 --- a/pkgs/leak_tracker/test/test_infra/mocks/mock_object_tracker.dart +++ b/pkgs/leak_tracker/test/test_infra/mocks/mock_object_tracker.dart @@ -25,6 +25,7 @@ class MockObjectTracker extends ObjectTracker { disposalTime: const Duration(milliseconds: 100), leakDiagnosticConfig: const LeakDiagnosticConfig(), numberOfGcCycles: defaultNumberOfGcCycles, + maxRequestsForRetainingPath: 0, ); final events = []; diff --git a/pkgs/leak_tracker_flutter_test/test/_formatting_test.dart b/pkgs/leak_tracker_flutter_test/test/_formatting_test.dart index 2c82ae1f..3e7d3ad9 100644 --- a/pkgs/leak_tracker_flutter_test/test/_formatting_test.dart +++ b/pkgs/leak_tracker_flutter_test/test/_formatting_test.dart @@ -3,7 +3,7 @@ // found in the LICENSE file. import 'package:flutter_test/flutter_test.dart'; -import 'package:leak_tracker/src/leak_tracking/_formatting.dart'; +import 'package:leak_tracker/src/shared/_formatting.dart'; const _jsonEmpty = {}; diff --git a/pkgs/leak_tracker_flutter_test/test/end_to_end_test.dart b/pkgs/leak_tracker_flutter_test/test/end_to_end_test.dart index fd6346c3..914e5a19 100644 --- a/pkgs/leak_tracker_flutter_test/test/end_to_end_test.dart +++ b/pkgs/leak_tracker_flutter_test/test/end_to_end_test.dart @@ -45,36 +45,6 @@ void main() { ); }); - group('Leak tracker respects flag collectDebugInformationForLeaks', () { - late Leaks leaks; - - setUp( - () => LeakTrackerGlobalSettings.collectDebugInformationForLeaks = true, - ); - - testWidgetsWithLeakTracking( - 'for $StatelessLeakingWidget', - (WidgetTester tester) async { - await tester.pumpWidget(StatelessLeakingWidget()); - }, - leakTrackingTestConfig: LeakTrackingTestConfig( - onLeaks: (Leaks theLeaks) { - leaks = theLeaks; - }, - failTestOnLeaks: false, - ), - ); - - tearDown( - () => _verifyLeaks( - leaks, - expectedNotDisposed: 1, - expectedNotGCed: 1, - shouldContainDebugInfo: false, - ), - ); - }); - group('Stack trace does not start with leak tracker calls.', () { late Leaks leaks; diff --git a/pkgs/leak_tracker_flutter_test/test/test_infra/dart_classes.dart b/pkgs/leak_tracker_flutter_test/test/test_infra/dart_classes.dart index 7e97b0c7..b182a5fb 100644 --- a/pkgs/leak_tracker_flutter_test/test/test_infra/dart_classes.dart +++ b/pkgs/leak_tracker_flutter_test/test/test_infra/dart_classes.dart @@ -2,11 +2,11 @@ // for details. All rights reserved. Use of this source code is governed by a // BSD-style license that can be found in the LICENSE file. -import 'package:leak_tracker/src/leak_tracking/leak_tracker.dart'; +import 'package:leak_tracker/leak_tracker.dart'; class LeakTrackedClass { LeakTrackedClass() { - dispatchObjectCreated( + LeakTracking.dispatchObjectCreated( library: library, className: '$LeakTrackedClass', object: this, @@ -16,7 +16,7 @@ class LeakTrackedClass { static const library = 'package:my_package/lib/src/my_lib.dart'; void dispose() { - dispatchObjectDisposed(object: this); + LeakTracking.dispatchObjectDisposed(object: this); } } diff --git a/pkgs/leak_tracker_flutter_test/test/test_infra/helpers.dart b/pkgs/leak_tracker_flutter_test/test/test_infra/helpers.dart index 8dfdd68e..67d9a078 100644 --- a/pkgs/leak_tracker_flutter_test/test/test_infra/helpers.dart +++ b/pkgs/leak_tracker_flutter_test/test/test_infra/helpers.dart @@ -32,18 +32,14 @@ void testWidgetsWithLeakTracking( bool semanticsEnabled = true, TestVariant variant = const DefaultTestVariant(), dynamic tags, - LeakTrackingTestConfig? leakTrackingTestConfig, + LeakTrackingTestConfig leakTrackingTestConfig = + const LeakTrackingTestConfig(), }) { - final config = leakTrackingTestConfig ?? - (LeakTrackerGlobalSettings.collectDebugInformationForLeaks - ? LeakTrackingTestConfig.debug() - : const LeakTrackingTestConfig()); - Future wrappedCallback(WidgetTester tester) async { await withFlutterLeakTracking( () async => callback(tester), tester, - config, + leakTrackingTestConfig, ); } @@ -82,12 +78,12 @@ Future withFlutterLeakTracking( ) async { // Leak tracker does not work for web platform. if (kIsWeb) { - final bool shouldPrintWarning = !_webWarningPrinted && - LeakTrackerGlobalSettings.warnForNonSupportedPlatforms; + final bool shouldPrintWarning = + !_webWarningPrinted && LeakTracking.warnForNonSupportedPlatforms; if (shouldPrintWarning) { _webWarningPrinted = true; debugPrint( - 'Leak tracking is not supported on web platform.\nTo turn off this message, set `LeakTrackerGlobalFlags.warnForNonSupportedPlatforms` to false.', + 'Leak tracking is not supported on web platform.\nTo turn off this message, set `LeakTracking.warnForNonSupportedPlatforms` to false.', ); } await callback(); @@ -95,7 +91,7 @@ Future withFlutterLeakTracking( } void flutterEventToLeakTracker(ObjectEvent event) { - return dispatchObjectEvent(event.toMap()); + return LeakTracking.dispatchObjectEvent(event.toMap()); } return TestAsyncUtils.guard(() async { diff --git a/pkgs/leak_tracker_flutter_test/test/test_infra/mock_object_tracker.dart b/pkgs/leak_tracker_flutter_test/test/test_infra/mock_object_tracker.dart index 1924667a..c37590a9 100644 --- a/pkgs/leak_tracker_flutter_test/test/test_infra/mock_object_tracker.dart +++ b/pkgs/leak_tracker_flutter_test/test/test_infra/mock_object_tracker.dart @@ -25,6 +25,7 @@ class MockObjectTracker extends ObjectTracker { disposalTime: const Duration(milliseconds: 100), leakDiagnosticConfig: const LeakDiagnosticConfig(), numberOfGcCycles: 3, + maxRequestsForRetainingPath: 10, ); final events = []; diff --git a/pkgs/leak_tracker_testing/pubspec.yaml b/pkgs/leak_tracker_testing/pubspec.yaml index 0c4f5351..3eaf961b 100644 --- a/pkgs/leak_tracker_testing/pubspec.yaml +++ b/pkgs/leak_tracker_testing/pubspec.yaml @@ -7,7 +7,7 @@ environment: sdk: '>=3.0.0 <4.0.0' dependencies: - leak_tracker: ^8.0.0 + leak_tracker: '>=8.0.0 <11.0.0' test: ^1.16.0 dev_dependencies: