Unverified Commit 10d5ec87 authored by Romain Rastel's avatar Romain Rastel Committed by GitHub

Improve the performances of ChangeNotifier (#71947)

parent a219b86d
...@@ -2,8 +2,6 @@ ...@@ -2,8 +2,6 @@
// Use of this source code is governed by a BSD-style license that can be // Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. // found in the LICENSE file.
import 'dart:collection';
import 'package:meta/meta.dart'; import 'package:meta/meta.dart';
import 'assertions.dart'; import 'assertions.dart';
...@@ -94,11 +92,6 @@ abstract class ValueListenable<T> extends Listenable { ...@@ -94,11 +92,6 @@ abstract class ValueListenable<T> extends Listenable {
T get value; T get value;
} }
class _ListenerEntry extends LinkedListEntry<_ListenerEntry> {
_ListenerEntry(this.listener);
final VoidCallback listener;
}
/// 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.
/// ///
...@@ -109,11 +102,15 @@ class _ListenerEntry extends LinkedListEntry<_ListenerEntry> { ...@@ -109,11 +102,15 @@ class _ListenerEntry extends LinkedListEntry<_ListenerEntry> {
/// ///
/// * [ValueNotifier], which is a [ChangeNotifier] that wraps a single value. /// * [ValueNotifier], which is a [ChangeNotifier] that wraps a single value.
class ChangeNotifier implements Listenable { class ChangeNotifier implements Listenable {
LinkedList<_ListenerEntry>? _listeners = LinkedList<_ListenerEntry>(); int _count = 0;
List<VoidCallback?> _listeners = List<VoidCallback?>.filled(0, null);
int _notificationCallStackDepth = 0;
int _reentrantlyRemovedListeners = 0;
bool _debugDisposed = false;
bool _debugAssertNotDisposed() { bool _debugAssertNotDisposed() {
assert(() { assert(() {
if (_listeners == null) { if (_debugDisposed) {
throw FlutterError( throw FlutterError(
'A $runtimeType was used after being disposed.\n' 'A $runtimeType was used after being disposed.\n'
'Once you have called dispose() on a $runtimeType, it can no longer be used.', 'Once you have called dispose() on a $runtimeType, it can no longer be used.',
...@@ -142,7 +139,7 @@ class ChangeNotifier implements Listenable { ...@@ -142,7 +139,7 @@ class ChangeNotifier implements Listenable {
@protected @protected
bool get hasListeners { bool get hasListeners {
assert(_debugAssertNotDisposed()); assert(_debugAssertNotDisposed());
return _listeners!.isNotEmpty; return _count > 0;
} }
/// Register a closure to be called when the object changes. /// Register a closure to be called when the object changes.
...@@ -174,7 +171,48 @@ class ChangeNotifier implements Listenable { ...@@ -174,7 +171,48 @@ class ChangeNotifier implements Listenable {
@override @override
void addListener(VoidCallback listener) { void addListener(VoidCallback listener) {
assert(_debugAssertNotDisposed()); assert(_debugAssertNotDisposed());
_listeners!.add(_ListenerEntry(listener)); if (_count == _listeners.length) {
if (_count == 0) {
_listeners = List<VoidCallback?>.filled(1, null);
} else {
final List<VoidCallback?> newListeners =
List<VoidCallback?>.filled(_listeners.length * 2, null);
for (int i = 0; i < _count; i++) {
newListeners[i] = _listeners[i];
}
_listeners = newListeners;
}
}
_listeners[_count++] = listener;
}
void _removeAt(int index) {
// The list holding the listeners is not growable for performances reasons.
// We still want to shrink this list if a lot of listeners have been added
// and then removed outside a notifyListeners iteration.
// We do this only when the real number of listeners is half the length
// of our list.
_count -= 1;
if (_count * 2 <= _listeners.length) {
final List<VoidCallback?> newListeners = List<VoidCallback?>.filled(_count, null);
// Listeners before the index are at the same place.
for (int i = 0; i < index; i++)
newListeners[i] = _listeners[i];
// Listeners after the index move towards the start of the list.
for (int i = index; i < _count; i++)
newListeners[i] = _listeners[i + 1];
_listeners = newListeners;
} else {
// When there are more listeners than half the length of the list, we only
// shift our listeners, so that we avoid to reallocate memory for the
// whole list.
for (int i = index; i < _count; i++)
_listeners[i] = _listeners[i + 1];
_listeners[_count] = null;
}
} }
/// Remove a previously registered closure from the list of closures that are /// Remove a previously registered closure from the list of closures that are
...@@ -193,10 +231,22 @@ class ChangeNotifier implements Listenable { ...@@ -193,10 +231,22 @@ class ChangeNotifier implements Listenable {
@override @override
void removeListener(VoidCallback listener) { void removeListener(VoidCallback listener) {
assert(_debugAssertNotDisposed()); assert(_debugAssertNotDisposed());
for (final _ListenerEntry entry in _listeners!) { for (int i = 0; i < _count; i++) {
if (entry.listener == listener) { final VoidCallback? _listener = _listeners[i];
entry.unlink(); if (_listener == listener) {
return; if (_notificationCallStackDepth > 0) {
// We don't resize the list during notifyListeners iterations
// but we set to null, the listeners we want to remove. We will
// effectively resize the list at the end of all notifyListeners
// iterations.
_listeners[i] = null;
_reentrantlyRemovedListeners++;
} else {
// When we are outside the notifyListeners iterations we can
// effectively shrink the list.
_removeAt(i);
}
break;
} }
} }
} }
...@@ -210,7 +260,10 @@ class ChangeNotifier implements Listenable { ...@@ -210,7 +260,10 @@ class ChangeNotifier implements Listenable {
@mustCallSuper @mustCallSuper
void dispose() { void dispose() {
assert(_debugAssertNotDisposed()); assert(_debugAssertNotDisposed());
_listeners = null; assert(() {
_debugDisposed = true;
return true;
}());
} }
/// Call all the registered listeners. /// Call all the registered listeners.
...@@ -232,15 +285,26 @@ class ChangeNotifier implements Listenable { ...@@ -232,15 +285,26 @@ class ChangeNotifier implements Listenable {
@visibleForTesting @visibleForTesting
void notifyListeners() { void notifyListeners() {
assert(_debugAssertNotDisposed()); assert(_debugAssertNotDisposed());
if (_listeners!.isEmpty) if (_count == 0)
return; return;
final List<_ListenerEntry> localListeners = List<_ListenerEntry>.from(_listeners!); // To make sure that listeners removed during this iteration are not called,
// we set them to null, but we don't shrink the list right away.
// By doing this, we can continue to iterate on our list until it reaches
// the last listener added before the call to this method.
// To allow potential listeners to recursively call notifyListener, we track
// the number of times this method is called in _notificationCallStackDepth.
// Once every recursive iteration is finished (i.e. when _notificationCallStackDepth == 0),
// we can safely shrink our list so that it will only contain not null
// listeners.
for (final _ListenerEntry entry in localListeners) { _notificationCallStackDepth++;
final int end = _count;
for (int i = 0; i < end; i++) {
try { try {
if (entry.list != null) _listeners[i]?.call();
entry.listener();
} catch (exception, stack) { } catch (exception, stack) {
FlutterError.reportError(FlutterErrorDetails( FlutterError.reportError(FlutterErrorDetails(
exception: exception, exception: exception,
...@@ -257,6 +321,44 @@ class ChangeNotifier implements Listenable { ...@@ -257,6 +321,44 @@ class ChangeNotifier implements Listenable {
)); ));
} }
} }
_notificationCallStackDepth--;
if (_notificationCallStackDepth == 0 && _reentrantlyRemovedListeners > 0) {
// We really remove the listeners when all notifications are done.
final int newLength = _count - _reentrantlyRemovedListeners;
if (newLength * 2 <= _listeners.length) {
// As in _removeAt, we only shrink the list when the real number of
// listeners is half the length of our list.
final List<VoidCallback?> newListeners = List<VoidCallback?>.filled(newLength, null);
int newIndex = 0;
for (int i = 0; i < _count; i++) {
final VoidCallback? listener = _listeners[i];
if (listener != null) {
newListeners[newIndex++] = listener;
}
}
_listeners = newListeners;
} else {
// Otherwise we put all the null references at the end.
for (int i = 0; i < newLength; i += 1) {
if (_listeners[i] == null) {
// We swap this item with the next not null item.
int swapIndex = i + 1;
while(_listeners[swapIndex] == null){
swapIndex += 1;
}
_listeners[i] = _listeners[swapIndex];
_listeners[swapIndex] = null;
}
}
}
_reentrantlyRemovedListeners = 0;
_count = newLength;
}
} }
} }
......
...@@ -20,7 +20,9 @@ class HasListenersTester<T> extends ValueNotifier<T> { ...@@ -20,7 +20,9 @@ class HasListenersTester<T> extends ValueNotifier<T> {
class A { class A {
bool result = false; bool result = false;
void test() { result = true; } void test() {
result = true;
}
} }
class B extends A with ChangeNotifier { class B extends A with ChangeNotifier {
...@@ -31,12 +33,36 @@ class B extends A with ChangeNotifier { ...@@ -31,12 +33,36 @@ class B extends A with ChangeNotifier {
} }
} }
class Counter with ChangeNotifier {
int get value => _value;
int _value = 0;
set value(int value) {
if (_value != value) {
_value = value;
notifyListeners();
}
}
void notify() {
notifyListeners();
}
}
void main() { void main() {
testWidgets('ChangeNotifier', (WidgetTester tester) async { testWidgets('ChangeNotifier', (WidgetTester tester) async {
final List<String> log = <String>[]; final List<String> log = <String>[];
void listener() { log.add('listener'); } void listener() {
void listener1() { log.add('listener1'); } log.add('listener');
void listener2() { log.add('listener2'); } }
void listener1() {
log.add('listener1');
}
void listener2() {
log.add('listener2');
}
void badListener() { void badListener() {
log.add('badListener'); log.add('badListener');
throw ArgumentError(); throw ArgumentError();
...@@ -111,9 +137,18 @@ void main() { ...@@ -111,9 +137,18 @@ void main() {
final TestNotifier test = TestNotifier(); final TestNotifier test = TestNotifier();
final List<String> log = <String>[]; final List<String> log = <String>[];
void listener1() { log.add('listener1'); } void listener1() {
void listener3() { log.add('listener3'); } log.add('listener1');
void listener4() { log.add('listener4'); } }
void listener3() {
log.add('listener3');
}
void listener4() {
log.add('listener4');
}
void listener2() { void listener2() {
log.add('listener2'); log.add('listener2');
test.removeListener(listener1); test.removeListener(listener1);
...@@ -137,12 +172,19 @@ void main() { ...@@ -137,12 +172,19 @@ void main() {
log.clear(); log.clear();
}); });
test('During notifyListeners, a listener was added and removed immediately', () { test('During notifyListeners, a listener was added and removed immediately',
() {
final TestNotifier source = TestNotifier(); final TestNotifier source = TestNotifier();
final List<String> log = <String>[]; final List<String> log = <String>[];
void listener3() { log.add('listener3'); } void listener3() {
void listener2() { log.add('listener2'); } log.add('listener3');
}
void listener2() {
log.add('listener2');
}
void listener1() { void listener1() {
log.add('listener1'); log.add('listener1');
source.addListener(listener2); source.addListener(listener2);
...@@ -167,7 +209,10 @@ void main() { ...@@ -167,7 +209,10 @@ void main() {
log.add('selfRemovingListener'); log.add('selfRemovingListener');
source.removeListener(selfRemovingListener); source.removeListener(selfRemovingListener);
} }
void listener1() { log.add('listener1'); }
void listener1() {
log.add('listener1');
}
source.addListener(listener1); source.addListener(listener1);
source.addListener(selfRemovingListener); source.addListener(selfRemovingListener);
...@@ -178,7 +223,9 @@ void main() { ...@@ -178,7 +223,9 @@ void main() {
expect(log, <String>['listener1', 'selfRemovingListener', 'listener1']); expect(log, <String>['listener1', 'selfRemovingListener', 'listener1']);
}); });
test('If the first listener removes itself, notifyListeners still notify all listeners', () { test(
'If the first listener removes itself, notifyListeners still notify all listeners',
() {
final TestNotifier source = TestNotifier(); final TestNotifier source = TestNotifier();
final List<String> log = <String>[]; final List<String> log = <String>[];
...@@ -186,6 +233,7 @@ void main() { ...@@ -186,6 +233,7 @@ void main() {
log.add('selfRemovingListener'); log.add('selfRemovingListener');
source.removeListener(selfRemovingListener); source.removeListener(selfRemovingListener);
} }
void listener1() { void listener1() {
log.add('listener1'); log.add('listener1');
} }
...@@ -205,8 +253,13 @@ void main() { ...@@ -205,8 +253,13 @@ void main() {
final List<String> log = <String>[]; final List<String> log = <String>[];
final Listenable merged = Listenable.merge(<Listenable>[source1, source2]); final Listenable merged = Listenable.merge(<Listenable>[source1, source2]);
void listener1() { log.add('listener1'); } void listener1() {
void listener2() { log.add('listener2'); } log.add('listener1');
}
void listener2() {
log.add('listener2');
}
merged.addListener(listener1); merged.addListener(listener1);
source1.notify(); source1.notify();
...@@ -236,8 +289,11 @@ void main() { ...@@ -236,8 +289,11 @@ void main() {
final TestNotifier source2 = TestNotifier(); final TestNotifier source2 = TestNotifier();
final List<String> log = <String>[]; final List<String> log = <String>[];
final Listenable merged = Listenable.merge(<Listenable?>[null, source1, null, source2, null]); final Listenable merged =
void listener() { log.add('listener'); } Listenable.merge(<Listenable?>[null, source1, null, source2, null]);
void listener() {
log.add('listener');
}
merged.addListener(listener); merged.addListener(listener);
source1.notify(); source1.notify();
...@@ -252,7 +308,9 @@ void main() { ...@@ -252,7 +308,9 @@ void main() {
final List<String> log = <String>[]; final List<String> log = <String>[];
final Listenable merged = Listenable.merge(<Listenable>[source1, source2]); final Listenable merged = Listenable.merge(<Listenable>[source1, source2]);
void listener() { log.add('listener'); } void listener() {
log.add('listener');
}
merged.addListener(listener); merged.addListener(listener);
source1.notify(); source1.notify();
...@@ -269,22 +327,32 @@ void main() { ...@@ -269,22 +327,32 @@ void main() {
test('Cannot use a disposed ChangeNotifier', () { test('Cannot use a disposed ChangeNotifier', () {
final TestNotifier source = TestNotifier(); final TestNotifier source = TestNotifier();
source.dispose(); source.dispose();
expect(() { source.addListener(() { }); }, throwsFlutterError); expect(() {
expect(() { source.removeListener(() { }); }, throwsFlutterError); source.addListener(() {});
expect(() { source.dispose(); }, throwsFlutterError); }, throwsFlutterError);
expect(() { source.notify(); }, throwsFlutterError); expect(() {
source.removeListener(() {});
}, throwsFlutterError);
expect(() {
source.dispose();
}, throwsFlutterError);
expect(() {
source.notify();
}, throwsFlutterError);
}); });
test('Value notifier', () { test('Value notifier', () {
final ValueNotifier<double> notifier = ValueNotifier<double>(2.0); final ValueNotifier<double> notifier = ValueNotifier<double>(2.0);
final List<double> log = <double>[]; final List<double> log = <double>[];
void listener() { log.add(notifier.value); } void listener() {
log.add(notifier.value);
}
notifier.addListener(listener); notifier.addListener(listener);
notifier.value = 3.0; notifier.value = 3.0;
expect(log, equals(<double>[ 3.0 ])); expect(log, equals(<double>[3.0]));
log.clear(); log.clear();
notifier.value = 3.0; notifier.value = 3.0;
...@@ -325,9 +393,10 @@ void main() { ...@@ -325,9 +393,10 @@ void main() {
final TestNotifier source1 = TestNotifier(); final TestNotifier source1 = TestNotifier();
final TestNotifier source2 = TestNotifier(); final TestNotifier source2 = TestNotifier();
void fakeListener() { } void fakeListener() {}
final Listenable listenableUnderTest = Listenable.merge(<Listenable>[source1, source2]); final Listenable listenableUnderTest =
Listenable.merge(<Listenable>[source1, source2]);
expect(source1.isListenedTo, isFalse); expect(source1.isListenedTo, isFalse);
expect(source2.isListenedTo, isFalse); expect(source2.isListenedTo, isFalse);
listenableUnderTest.addListener(fakeListener); listenableUnderTest.addListener(fakeListener);
...@@ -339,12 +408,11 @@ void main() { ...@@ -339,12 +408,11 @@ void main() {
expect(source2.isListenedTo, isFalse); expect(source2.isListenedTo, isFalse);
}); });
test('hasListeners', () { test('hasListeners', () {
final HasListenersTester<bool> notifier = HasListenersTester<bool>(true); final HasListenersTester<bool> notifier = HasListenersTester<bool>(true);
expect(notifier.testHasListeners, isFalse); expect(notifier.testHasListeners, isFalse);
void test1() { } void test1() {}
void test2() { } void test2() {}
notifier.addListener(test1); notifier.addListener(test1);
expect(notifier.testHasListeners, isTrue); expect(notifier.testHasListeners, isTrue);
notifier.addListener(test1); notifier.addListener(test1);
...@@ -388,12 +456,94 @@ void main() { ...@@ -388,12 +456,94 @@ void main() {
} }
expect(error, isNotNull); expect(error, isNotNull);
expect(error!, isFlutterError); expect(error!, isFlutterError);
expect(error.toStringDeep(), equalsIgnoringHashCodes( expect(
'FlutterError\n' error.toStringDeep(),
' A TestNotifier was used after being disposed.\n' equalsIgnoringHashCodes('FlutterError\n'
' Once you have called dispose() on a TestNotifier, it can no\n' ' A TestNotifier was used after being disposed.\n'
' longer be used.\n' ' Once you have called dispose() on a TestNotifier, it can no\n'
)); ' longer be used.\n'));
});
test('notifyListener can be called recursively', () {
final Counter counter = Counter();
final List<String> log = <String>[];
void listener1() {
log.add('listener1');
if (counter.value < 0) {
counter.value = 0;
}
}
counter.addListener(listener1);
counter.notify();
expect(log, <String>['listener1']);
log.clear();
counter.value = 3;
expect(log, <String>['listener1']);
log.clear();
counter.value = -2;
expect(log, <String>['listener1', 'listener1']);
log.clear();
}); });
test('Remove Listeners while notifying on a list which will not resize', () {
final TestNotifier test = TestNotifier();
final List<String> log = <String>[];
final List<VoidCallback> listeners = <VoidCallback>[];
void autoRemove() {
// We remove 4 listeners.
// We will end up with (13-4 = 9) listeners.
test.removeListener(listeners[1]);
test.removeListener(listeners[3]);
test.removeListener(listeners[4]);
test.removeListener(autoRemove);
}
test.addListener(autoRemove);
// We add 12 more listeners.
for (int i = 0; i < 12; i++) {
void listener() {
log.add('listener$i');
}
listeners.add(listener);
test.addListener(listener);
}
final List<int> remainingListenerIndexes = <int>[
0,
2,
5,
6,
7,
8,
9,
10,
11
];
final List<String> expectedLog =
remainingListenerIndexes.map((int i) => 'listener$i').toList();
test.notify();
expect(log, expectedLog);
log.clear();
// We expect to have the same result after the removal of previous listeners.
test.notify();
expect(log, expectedLog);
// We remove all other listeners.
for (int i = 0; i < remainingListenerIndexes.length; i++) {
test.removeListener(listeners[remainingListenerIndexes[i]]);
}
log.clear();
test.notify();
expect(log, <String>[]);
});
} }
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