Unverified Commit a3c3dc86 authored by Polina Cherkasova's avatar Polina Cherkasova Committed by GitHub

Create class MemoryAllocations. (#110230)

parent 745d2d44
...@@ -34,6 +34,7 @@ export 'src/foundation/isolates.dart'; ...@@ -34,6 +34,7 @@ export 'src/foundation/isolates.dart';
export 'src/foundation/key.dart'; export 'src/foundation/key.dart';
export 'src/foundation/licenses.dart'; export 'src/foundation/licenses.dart';
export 'src/foundation/math.dart'; export 'src/foundation/math.dart';
export 'src/foundation/memory_allocations.dart';
export 'src/foundation/node.dart'; export 'src/foundation/node.dart';
export 'src/foundation/object.dart'; export 'src/foundation/object.dart';
export 'src/foundation/observer_list.dart'; export 'src/foundation/observer_list.dart';
......
...@@ -8,6 +8,7 @@ import 'package:meta/meta.dart'; ...@@ -8,6 +8,7 @@ import 'package:meta/meta.dart';
import 'assertions.dart'; import 'assertions.dart';
import 'diagnostics.dart'; import 'diagnostics.dart';
import 'memory_allocations.dart';
export 'dart:ui' show VoidCallback; export 'dart:ui' show VoidCallback;
...@@ -95,6 +96,8 @@ abstract class ValueListenable<T> extends Listenable { ...@@ -95,6 +96,8 @@ abstract class ValueListenable<T> extends Listenable {
T get value; T get value;
} }
const String _flutterFoundationLibrary = 'package:flutter/foundation.dart';
/// A class that can be extended or mixed in that provides a change notification /// A class that can be extended or mixed in that provides a change notification
/// API using [VoidCallback] for notifications. /// API using [VoidCallback] for notifications.
/// ///
...@@ -122,6 +125,13 @@ class ChangeNotifier implements Listenable { ...@@ -122,6 +125,13 @@ class ChangeNotifier implements Listenable {
int _reentrantlyRemovedListeners = 0; int _reentrantlyRemovedListeners = 0;
bool _debugDisposed = false; bool _debugDisposed = false;
/// If true, the event [ObjectCreated] for this instance was dispatched to
/// [MemoryAllocations].
///
/// As [ChangedNotifier] is used as mixin, it does not have constructor,
/// so we use [addListener] to dispatch the event.
bool _creationDispatched = false;
/// Used by subclasses to assert that the [ChangeNotifier] has not yet been /// Used by subclasses to assert that the [ChangeNotifier] has not yet been
/// disposed. /// disposed.
/// ///
...@@ -204,6 +214,16 @@ class ChangeNotifier implements Listenable { ...@@ -204,6 +214,16 @@ class ChangeNotifier implements Listenable {
@override @override
void addListener(VoidCallback listener) { void addListener(VoidCallback listener) {
assert(ChangeNotifier.debugAssertNotDisposed(this)); assert(ChangeNotifier.debugAssertNotDisposed(this));
if (kFlutterMemoryAllocationsEnabled && !_creationDispatched) {
MemoryAllocations.instance.dispatchObjectEvent(() {
return ObjectCreated(
library: _flutterFoundationLibrary,
className: 'ChangeNotifier',
object: this,
);
});
_creationDispatched = true;
}
if (_count == _listeners.length) { if (_count == _listeners.length) {
if (_count == 0) { if (_count == 0) {
_listeners = List<VoidCallback?>.filled(1, null); _listeners = List<VoidCallback?>.filled(1, null);
...@@ -307,6 +327,9 @@ class ChangeNotifier implements Listenable { ...@@ -307,6 +327,9 @@ class ChangeNotifier implements Listenable {
_debugDisposed = true; _debugDisposed = true;
return true; return true;
}()); }());
if (kFlutterMemoryAllocationsEnabled && _creationDispatched) {
MemoryAllocations.instance.dispatchObjectEvent(() => ObjectDisposed(object: this));
}
_listeners = _emptyListeners; _listeners = _emptyListeners;
_count = 0; _count = 0;
} }
...@@ -441,7 +464,18 @@ class _MergingListenable extends Listenable { ...@@ -441,7 +464,18 @@ class _MergingListenable extends Listenable {
/// listeners. /// listeners.
class ValueNotifier<T> extends ChangeNotifier implements ValueListenable<T> { class ValueNotifier<T> extends ChangeNotifier implements ValueListenable<T> {
/// Creates a [ChangeNotifier] that wraps this value. /// Creates a [ChangeNotifier] that wraps this value.
ValueNotifier(this._value); ValueNotifier(this._value) {
if (kFlutterMemoryAllocationsEnabled) {
MemoryAllocations.instance.dispatchObjectEvent(() {
return ObjectCreated(
library: _flutterFoundationLibrary,
className: 'ValueNotifier',
object: this,
);
});
}
_creationDispatched = true;
}
/// The current value stored in this notifier. /// The current value stored in this notifier.
/// ///
......
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:ui' as ui;
import 'assertions.dart';
import 'constants.dart';
import 'diagnostics.dart';
const bool _kMemoryAllocations = bool.fromEnvironment('flutter.memory_allocations');
/// If true, Flutter objects dispatch the memory allocation events.
///
/// By default, the constant is true for profile and debug mode and false
/// for release mode, for app size optimization goals.
/// To enable the dispatching for release mode, pass the compilation flag
/// `--dart-define=flutter.memory_allocations=true`.
const bool kFlutterMemoryAllocationsEnabled = _kMemoryAllocations || kProfileMode || kDebugMode;
const String _dartUiLibrary = 'dart:ui';
class _FieldNames {
static const String eventType = 'eventType';
static const String libraryName = 'libraryName';
static const String className = 'className';
}
/// A lifecycle event of an object.
abstract class ObjectEvent{
/// Creates an instance of [ObjectEvent].
ObjectEvent({
required this.object,
});
/// Reference to the object.
///
/// The reference should not be stored in any
/// long living place as it will prevent garbage collection.
final Object object;
/// The representation of the event in a form, acceptible by a
/// pure dart library, that cannot depend on Flutter.
///
/// The method enables code like:
/// ```dart
/// void myDartMethod(Map<Object, Map<String, Object>> event) {}
/// MemoryAllocations.instance
/// .addListener((ObjectEvent event) => myDartMethod(event.toMap()));
/// ```
Map<Object, Map<String, Object>> toMap();
}
/// A listener of [ObjectEvent].
typedef ObjectEventListener = void Function(ObjectEvent);
/// A builder of [ObjectEvent].
typedef ObjectEventBuilder = ObjectEvent Function();
/// An event that describes creation of an object.
class ObjectCreated extends ObjectEvent {
/// Creates an instance of [ObjectCreated].
ObjectCreated({
required this.library,
required this.className,
required super.object,
});
/// Name of the instrumented library.
final String library;
/// Name of the instrumented class.
final String className;
@override
Map<Object, Map<String, Object>> toMap() {
return <Object, Map<String, Object>>{object: <String, Object>{
_FieldNames.libraryName: library,
_FieldNames.className: className,
_FieldNames.eventType: 'created',
}};
}
}
/// An event that describes disposal of an object.
class ObjectDisposed extends ObjectEvent {
/// Creates an instance of [ObjectDisposed].
ObjectDisposed({
required super.object,
});
@override
Map<Object, Map<String, Object>> toMap() {
return <Object, Map<String, Object>>{object: <String, Object>{
_FieldNames.eventType: 'disposed',
}};
}
}
/// An interface for listening to object lifecycle events.
///
/// If [kFlutterMemoryAllocationsEnabled] is true,
/// [MemoryAllocations] listens to creation and disposal events
/// for disposable objects in Flutter Framework.
/// To dispatch events for other objects, invoke
/// [MemoryAllocations.dispatchObjectEvent].
///
/// Use this class with condition `kFlutterMemoryAllocationsEnabled`,
/// to make sure not to increase size of the application by the code
/// of the class, if memory allocations are disabled.
///
/// The class is optimized for massive event flow and small number of
/// added or removed listeners.
class MemoryAllocations {
MemoryAllocations._();
/// The shared instance of [MemoryAllocations].
///
/// Only call this when [kFlutterMemoryAllocationsEnabled] is true.
static final MemoryAllocations instance = MemoryAllocations._();
/// List of listeners.
///
/// The elements are nullable, because the listeners should be removable
/// while iterating through the list.
List<ObjectEventListener?>? _listeners;
/// Register a listener that is called every time an object event is
/// dispatched.
///
/// Listeners can be removed with [removeListener].
///
/// Only call this when [kFlutterMemoryAllocationsEnabled] is true.
void addListener(ObjectEventListener listener){
if (!kFlutterMemoryAllocationsEnabled) {
return;
}
if (_listeners == null) {
_listeners = <ObjectEventListener?>[];
_subscribeToSdkObjects();
}
_listeners!.add(listener);
}
/// Number of active notification loops.
///
/// When equal to zero, we can delete listeners from the list,
/// otherwise should null them.
int _activeDispatchLoops = 0;
/// If true, listeners were nulled by [removeListener].
bool _listenersContainNulls = false;
/// Stop calling the given listener every time an object event is
/// dispatched.
///
/// Listeners can be added with [addListener].
///
/// Only call this when [kFlutterMemoryAllocationsEnabled] is true.
void removeListener(ObjectEventListener listener){
if (!kFlutterMemoryAllocationsEnabled) {
return;
}
final List<ObjectEventListener?>? listeners = _listeners;
if (listeners == null) {
return;
}
if (_activeDispatchLoops > 0) {
// If there are active dispatch loops, listeners.remove
// should not be invoked, as it will
// break the dispatch loops correctness.
for (int i = 0; i < listeners.length; i++) {
if (listeners[i] == listener) {
listeners[i] = null;
_listenersContainNulls = true;
}
}
} else {
listeners.removeWhere((ObjectEventListener? l) => l == listener);
_checkListenersForEmptiness();
}
}
void _tryDefragmentListeners() {
if (_activeDispatchLoops > 0 || !_listenersContainNulls) {
return;
}
_listeners?.removeWhere((ObjectEventListener? e) => e == null);
_listenersContainNulls = false;
_checkListenersForEmptiness();
}
void _checkListenersForEmptiness() {
if (_listeners?.isEmpty ?? false) {
_listeners = null;
_unSubscribeFromSdkObjects();
}
}
/// Return true if there are listeners.
///
/// If there is no listeners, the app can save on creating the event object.
///
/// Only call this when [kFlutterMemoryAllocationsEnabled] is true.
bool get hasListeners {
if (!kFlutterMemoryAllocationsEnabled) {
return false;
}
if (_listenersContainNulls) {
return _listeners?.firstWhere((ObjectEventListener? l) => l != null) != null;
}
return _listeners?.isNotEmpty ?? false;
}
/// Dispatch a new object event to listeners.
///
/// Exceptions thrown by listeners will be caught and reported using
/// [FlutterError.reportError].
///
/// Listeners added during an event dispatching, will start being invoked
/// for next events, but will be skipped for this event.
///
/// Listeners, removed during an event dispatching, will not be invoked
/// after the removal.
///
/// Only call this when [kFlutterMemoryAllocationsEnabled] is true.
void dispatchObjectEvent(ObjectEventBuilder objectEventBuilder) {
if (!kFlutterMemoryAllocationsEnabled) {
return;
}
final List<ObjectEventListener?>? listeners = _listeners;
if (listeners == null || listeners.isEmpty) {
return;
}
final ObjectEvent event = objectEventBuilder();
_activeDispatchLoops++;
final int end = listeners.length;
for (int i = 0; i < end; i++) {
try {
listeners[i]?.call(event);
} catch (exception, stack) {
final String type = event.object.runtimeType.toString();
FlutterError.reportError(FlutterErrorDetails(
exception: exception,
stack: stack,
library: 'foundation library',
context: ErrorDescription('MemoryAllocations while '
'dispatching notifications for $type'),
informationCollector: () => <DiagnosticsNode>[
DiagnosticsProperty<Object>(
'The $type sending notification was',
event.object,
style: DiagnosticsTreeStyle.errorProperty,
),
],
));
}
}
_activeDispatchLoops--;
_tryDefragmentListeners();
}
void _subscribeToSdkObjects() {
assert(ui.Image.onCreate == null);
assert(ui.Image.onDispose == null);
assert(ui.Picture.onCreate == null);
assert(ui.Picture.onDispose == null);
ui.Image.onCreate = _imageOnCreate;
ui.Image.onDispose = _imageOnDispose;
ui.Picture.onCreate = _pictureOnCreate;
ui.Picture.onDispose = _pictureOnDispose;
}
void _unSubscribeFromSdkObjects() {
assert(ui.Image.onCreate == _imageOnCreate);
assert(ui.Image.onDispose == _imageOnDispose);
assert(ui.Picture.onCreate == _pictureOnCreate);
assert(ui.Picture.onDispose == _pictureOnDispose);
ui.Image.onCreate = null;
ui.Image.onDispose = null;
ui.Picture.onCreate = null;
ui.Picture.onDispose = null;
}
void _imageOnCreate(ui.Image image) {
dispatchObjectEvent(() {
return ObjectCreated(
library: _dartUiLibrary,
className: 'Image',
object: image,
);
});
}
void _pictureOnCreate(ui.Picture picture) {
dispatchObjectEvent(() {
return ObjectCreated(
library: _dartUiLibrary,
className: 'Picture',
object: picture,
);
});
}
void _imageOnDispose(ui.Image image) {
dispatchObjectEvent(() {
return ObjectDisposed(
object: image,
);
});
}
void _pictureOnDispose(ui.Picture picture) {
dispatchObjectEvent(() {
return ObjectDisposed(
object: picture,
);
});
}
}
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:ui';
import 'package:flutter/foundation.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
final MemoryAllocations ma = MemoryAllocations.instance;
setUp(() {
assert(!ma.hasListeners);
_checkSdkHandlersNotSet();
});
test('addListener and removeListener add and remove listeners.', () {
final ObjectEvent event = ObjectDisposed(object: 'object');
ObjectEvent? recievedEvent;
void listener(ObjectEvent event) => recievedEvent = event;
expect(ma.hasListeners, isFalse);
ma.addListener(listener);
_checkSdkHandlersSet();
ma.dispatchObjectEvent(() => event);
expect(recievedEvent, equals(event));
expect(ma.hasListeners, isTrue);
recievedEvent = null;
ma.removeListener(listener);
ma.dispatchObjectEvent(() => event);
expect(recievedEvent, isNull);
expect(ma.hasListeners, isFalse);
_checkSdkHandlersNotSet();
});
testWidgets('dispatchObjectEvent handles bad listeners', (WidgetTester tester) async {
final ObjectEvent event = ObjectDisposed(object: 'object');
final List<String> log = <String>[];
void badListener1(ObjectEvent event) {
log.add('badListener1');
throw ArgumentError();
}
void listener1(ObjectEvent event) => log.add('listener1');
void badListener2(ObjectEvent event) {
log.add('badListener2');
throw ArgumentError();
}
void listener2(ObjectEvent event) => log.add('listener2');
ma.addListener(badListener1);
_checkSdkHandlersSet();
ma.addListener(listener1);
ma.addListener(badListener2);
ma.addListener(listener2);
ma.dispatchObjectEvent(() => event);
expect(log, <String>['badListener1', 'listener1', 'badListener2','listener2']);
expect(tester.takeException(), contains('Multiple exceptions (2)'));
ma.removeListener(badListener1);
_checkSdkHandlersSet();
ma.removeListener(listener1);
ma.removeListener(badListener2);
ma.removeListener(listener2);
_checkSdkHandlersNotSet();
log.clear();
expect(ma.hasListeners, isFalse);
ma.dispatchObjectEvent(() => event);
expect(log, <String>[]);
});
test('dispatchObjectEvent does not invoke concurrently added listeners', () {
final ObjectEvent event = ObjectDisposed(object: 'object');
final List<String> log = <String>[];
void listener2(ObjectEvent event) => log.add('listener2');
void listener1(ObjectEvent event) {
log.add('listener1');
ma.addListener(listener2);
}
ma.addListener(listener1);
_checkSdkHandlersSet();
ma.dispatchObjectEvent(() => event);
expect(log, <String>['listener1']);
log.clear();
ma.dispatchObjectEvent(() => event);
expect(log, <String>['listener1','listener2']);
log.clear();
ma.removeListener(listener1);
ma.removeListener(listener2);
_checkSdkHandlersNotSet();
expect(ma.hasListeners, isFalse);
ma.dispatchObjectEvent(() => event);
expect(log, <String>[]);
});
test('dispatchObjectEvent does not invoke concurrently removed listeners', () {
final ObjectEvent event = ObjectDisposed(object: 'object');
final List<String> log = <String>[];
void listener2(ObjectEvent event) => log.add('listener2');
void listener1(ObjectEvent event) {
log.add('listener1');
ma.removeListener(listener2);
expect(ma.hasListeners, isFalse);
}
ma.addListener(listener1);
ma.addListener(listener2);
ma.dispatchObjectEvent(() => event);
expect(log, <String>['listener1']);
log.clear();
ma.removeListener(listener1);
_checkSdkHandlersNotSet();
expect(ma.hasListeners, isFalse);
});
test('last removeListener unsubscribes from Flutter SDK events', () {
void listener1(ObjectEvent event) {}
void listener2(ObjectEvent event) {}
ma.addListener(listener1);
_checkSdkHandlersSet();
ma.addListener(listener2);
_checkSdkHandlersSet();
ma.removeListener(listener1);
_checkSdkHandlersSet();
ma.removeListener(listener2);
_checkSdkHandlersNotSet();
});
test('kFlutterMemoryAllocationsEnabled is true in debug mode.', () {
expect(kFlutterMemoryAllocationsEnabled, isTrue);
});
test('publishers in Flutter dispatch events in debug mode', () async {
int eventCount = 0;
void listener(ObjectEvent event) => eventCount++;
ma.addListener(listener);
final int expectedEventCount = await _activateFlutterObjectsAndReturnCountOfEvents();
expect(eventCount, expectedEventCount);
ma.removeListener(listener);
_checkSdkHandlersNotSet();
expect(ma.hasListeners, isFalse);
});
}
void _checkSdkHandlersSet() {
expect(Image.onCreate, isNotNull);
expect(Picture.onCreate, isNotNull);
expect(Image.onDispose, isNotNull);
expect(Picture.onDispose, isNotNull);
}
void _checkSdkHandlersNotSet() {
expect(Image.onCreate, isNull);
expect(Picture.onCreate, isNull);
expect(Image.onDispose, isNull);
expect(Picture.onDispose, isNull);
}
/// Create and dispose Flutter objects to fire memory allocation events.
Future<int> _activateFlutterObjectsAndReturnCountOfEvents() async {
int count = 0;
final ValueNotifier<bool> valueNotifier = ValueNotifier<bool>(true); count++;
final ChangeNotifier changeNotifier = ChangeNotifier()..addListener(() {}); count++;
final Picture picture = _createPicture(); count++;
valueNotifier.dispose(); count++;
changeNotifier.dispose(); count++;
picture.dispose(); count++;
// TODO(polina-c): Remove the condition after
// https://github.com/flutter/flutter/issues/110599 is fixed.
if (!kIsWeb) {
final Image image = await _createImage(); count++; count++; count++;
image.dispose(); count++;
}
return count;
}
Future<Image> _createImage() async {
final Picture picture = _createPicture();
final Image result = await picture.toImage(10, 10);
picture.dispose();
return result;
}
Picture _createPicture() {
final PictureRecorder recorder = PictureRecorder();
final Canvas canvas = Canvas(recorder);
const Rect rect = Rect.fromLTWH(0.0, 0.0, 100.0, 100.0);
canvas.clipRect(rect);
return recorder.endRecording();
}
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:ui';
import 'package:flutter/foundation.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
final MemoryAllocations ma = MemoryAllocations.instance;
setUp(() {
assert(!ma.hasListeners);
_checkSdkHandlersNotSet();
});
test('kFlutterMemoryAllocationsEnabled is false in release mode.', () {
expect(kFlutterMemoryAllocationsEnabled, isFalse);
});
test(
'$MemoryAllocations is noop when kFlutterMemoryAllocationsEnabled is false.',
() async {
ObjectEvent? recievedEvent;
ObjectEvent listener(ObjectEvent event) => recievedEvent = event;
ma.addListener(listener);
_checkSdkHandlersNotSet();
await _activateFlutterObjects();
_checkSdkHandlersNotSet();
expect(recievedEvent, isNull);
expect(ma.hasListeners, isFalse);
ma.removeListener(listener);
_checkSdkHandlersNotSet();
},
);
}
void _checkSdkHandlersNotSet() {
expect(Image.onCreate, isNull);
expect(Picture.onCreate, isNull);
expect(Image.onDispose, isNull);
expect(Picture.onDispose, isNull);
}
/// Create and dispose Flutter objects to fire memory allocation events.
Future<void> _activateFlutterObjects() async {
final ValueNotifier<bool> valueNotifier = ValueNotifier<bool>(true);
final ChangeNotifier changeNotifier = ChangeNotifier()..addListener(() {});
final Image image = await _createImage();
final Picture picture = _createPicture();
valueNotifier.dispose();
changeNotifier.dispose();
image.dispose();
picture.dispose();
}
Future<Image> _createImage() async {
final Picture picture = _createPicture();
final Image result = await picture.toImage(10, 10);
picture.dispose();
return result;
}
Picture _createPicture() {
final PictureRecorder recorder = PictureRecorder();
final Canvas canvas = Canvas(recorder);
const Rect rect = Rect.fromLTWH(0.0, 0.0, 100.0, 100.0);
canvas.clipRect(rect);
return recorder.endRecording();
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment