Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Proposal for improving the performances of ChangeNotifier #71900

Closed
letsar opened this issue Dec 8, 2020 · 39 comments
Closed

Proposal for improving the performances of ChangeNotifier #71900

letsar opened this issue Dec 8, 2020 · 39 comments
Labels
customer: crowd Affects or could affect many people, though not necessarily a specific customer. framework flutter/packages/flutter repository. See also f: labels. P3 Priority 3 issue (the default for issues we're likely to work on after P0-P2 issues) passed first triage tests are present, the PR follows the PR template, no obvious coding errors perf: memory Performance issues related to memory perf: speed Performance issues related to (mostly rendering) speed proposal A detailed proposal for a change to Flutter severe: performance Relates to speed or footprint issues.

Comments

@letsar
Copy link
Contributor

letsar commented Dec 8, 2020

Hi 👋

We (@escamoteur, @knaeckeKami, and me, but also with the help of @mraleph and @Kavantix ) found a new implementation for ChangeNotifier which would improve its performances, especially when notifyListeners is called.

We created multiple benchmarks to see how our implementation behaves in multiple conditions.
These benchmarks are available here: https://github.com/knaeckeKami/changenotifier_benchmark

We measured the performances in terms of CPU time, but also the Memory Footprint, for 3 different implementations:

CPU Time

Our Benchmark

You will find below the results of our main benchmark:

┌───────────────────────────────────────────────────────────────────────────────┐
│                            ValueNotifier benchmark                            │
├─────────────┬─────────────────────┬─────────────────────┬─────────────────────┤
│             │       Initial       │       Current       │      Proposed       │
│  Listeners  ├─────────┬───────────┼─────────┬───────────┼─────────┬───────────┤
│             │ Updates │ Time [µs] │ Updates │ Time [µs] │ Updates │ Time [µs] │
├─────────────┼─────────┼───────────┼─────────┼───────────┼─────────┼───────────┤
│             │      10 │         2 │      10 │         1 │      10 │         0 │
│             ├─────────┼───────────┼─────────┼───────────┼─────────┼───────────┤
│             │     100 │       114 │     100 │         1 │     100 │         1 │
│           1 ├─────────┼───────────┼─────────┼───────────┼─────────┼───────────┤
│             │    1000 │        82 │    1000 │        14 │    1000 │        12 │
│             ├─────────┼───────────┼─────────┼───────────┼─────────┼───────────┤
│             │   10000 │      1194 │   10000 │       162 │   10000 │       225 │
├─────────────┼─────────┼───────────┼─────────┼───────────┼─────────┼───────────┤
│             │      10 │         6 │      10 │         6 │      10 │         1 │
│             ├─────────┼───────────┼─────────┼───────────┼─────────┼───────────┤
│             │     100 │        26 │     100 │        14 │     100 │         2 │
│           2 ├─────────┼───────────┼─────────┼───────────┼─────────┼───────────┤
│             │    1000 │       266 │    1000 │       146 │    1000 │        22 │
│             ├─────────┼───────────┼─────────┼───────────┼─────────┼───────────┤
│             │   10000 │      3346 │   10000 │      1459 │   10000 │       255 │
├─────────────┼─────────┼───────────┼─────────┼───────────┼─────────┼───────────┤
│             │      10 │        18 │      10 │         3 │      10 │         1 │
│             ├─────────┼───────────┼─────────┼───────────┼─────────┼───────────┤
│             │     100 │        68 │     100 │        22 │     100 │         3 │
│           4 ├─────────┼───────────┼─────────┼───────────┼─────────┼───────────┤
│             │    1000 │       743 │    1000 │       209 │    1000 │        28 │
│             ├─────────┼───────────┼─────────┼───────────┼─────────┼───────────┤
│             │   10000 │      7556 │   10000 │      2240 │   10000 │       309 │
├─────────────┼─────────┼───────────┼─────────┼───────────┼─────────┼───────────┤
│             │      10 │        22 │      10 │         5 │      10 │         1 │
│             ├─────────┼───────────┼─────────┼───────────┼─────────┼───────────┤
│             │     100 │       153 │     100 │        37 │     100 │        56 │
│           8 ├─────────┼───────────┼─────────┼───────────┼─────────┼───────────┤
│             │    1000 │      1557 │    1000 │       372 │    1000 │        58 │
│             ├─────────┼───────────┼─────────┼───────────┼─────────┼───────────┤
│             │   10000 │     15599 │   10000 │      4559 │   10000 │       532 │
├─────────────┼─────────┼───────────┼─────────┼───────────┼─────────┼───────────┤
│             │      10 │        81 │      10 │         8 │      10 │         1 │
│             ├─────────┼───────────┼─────────┼───────────┼─────────┼───────────┤
│             │     100 │       320 │     100 │        68 │     100 │         9 │
│          16 ├─────────┼───────────┼─────────┼───────────┼─────────┼───────────┤
│             │    1000 │      3238 │    1000 │       939 │    1000 │        90 │
│             ├─────────┼───────────┼─────────┼───────────┼─────────┼───────────┤
│             │   10000 │     31379 │   10000 │      8216 │   10000 │      1041 │
├─────────────┼─────────┴───────────┼─────────┴───────────┼─────────┴───────────┤
│ Total Time: │               65770 │               18481 │                2647 │
└─────────────┴─────────────────────┴─────────────────────┴─────────────────────┘

The code of this benchmark is the following, and you can find it here.

Future<int> runBenchmark({
  required ValueNotifierFactory creator,
  required final int updates,
  required final int listeners,
}) {
  final c = Completer<int>();
  final notifier = creator(0);
  final timer = Stopwatch()..start();

  for (var i = 0; i < listeners - 1; i++) {
    notifier.addListener(() {});
  }
  notifier.addListener(() {
    if (updates == notifier.value) {
      timer.stop();
      c.complete(timer.elapsedMicroseconds);
    }
  });

  for (var i = 0; i <= updates; i++) {
    notifier.value = i;
  }
  return c.future;
}

This benchmark has been run on my OnePlus 8 Pro (with Android 11) with the following command:

flutter run --release lib/benchmark.dart

As you can see, the Proposed implementation seems to be much faster than the previous ones.

Flutter microbenchmark

We also ran the flutter microbenchmark, for ChangeNotifier, available here in which we made some modifications to be able to test our 3 implementations, with 100,000 iterations for each one.

We normalized the results to see how much other implementations are slower than the fastest (the implementation with a score of 1).

Here are our results:
image

As you can see, the Proposed implementation is the fastest for almost all iterations in this benchmark as well.

Memory Footprint

We also wanted to compare the memory footprint of these different implementations.

We didn't find an automatic way to measure this so we took an approach based on Heap Snapshots with Dart DevTools.

We created a simple app which will instantiate 1000 notifiers with 1000 listeners for each one of them:

const name = 'Proposed';
const notifierCount = 1000;
const listenerCount = 1000;

main() => runApp(const MyApp());

class MyApp extends StatefulWidget {
  const MyApp({
    Key? key,
  }) : super(key: key);

  @override
  _MyAppState createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> {
  late final List<ValueNotifier<int>> notifiers;

  @override
  void initState() {
    super.initState();
    notifiers = List.generate(notifierCount, (index) {
      final notifier = factories[name]!(0);
      for (var i = 0; i < listenerCount; i++) {
        notifier.addListener(() {});
      }
      return notifier;
    });
  }

  @override
  Widget build(BuildContext context) {
    return const SizedBox();
  }
}

The protocol to measure the memory footprint can be reproduced by following these steps:

  1. Set the name constant to the implementation for which you want to measure the memory footprint (Initial, Current or Proposed).
  2. Launch the app with this command flutter run --profile lib/main.dart.
  3. Copy the Observatory Uri.
  4. Launch Dart DevTools and connect it to your app with the Observatory Uri you copied before.
  5. Go to the Memory tab.
  6. Wait a couple of seconds and click on GC.
  7. Wait a couple of seconds and click on Take Heap Snapshot
  8. Expand the Snapshot item and copy the Shallow value of External, Filtered, package and src into a spreadsheet
  9. Go to 1. and change the name.

We also measure the memory footprint of the same app with 0 notifiers to have a reference.

These are our results:

┌──────────┬─────────────┬─────────┬─────────┬──────────┐
│          │ 0 notifiers │ Initial │ Current │ Proposed │
├──────────┼─────────────┼─────────┼─────────┼──────────┤
│ External │       3.01K │   3.01K │   3.01K │    3.01K │
│ Filtered │       1,99M │   74,3M │    114M │    74,2M │
│ package  │          16 │     32K │      16 │      64K │
│ src      │       4,64M │   4,64M │   4,64M │    4,64M │
│ Total    │       6.63M │  78,98M │ 118,64M │   78,91M │
└──────────┴─────────────┴─────────┴─────────┴──────────┘

We can see that Initial and Proposed implementations have about the same memory footprint, but the Current implementation's footprint is higher.

By removing the total memory footprint of the 0 notifiers iteration, we can compute that the Proposed implementation consumes 1.55 times less than the Current one.

We also think that this proposed implementation allocates fewer temporary objects because it doesn't create a new list for each call to notifyListener.

Proposed implementation

You will find below the proposed implementation which lays on a custom growable list:

class ProposedChangeNotifier implements Listenable {
  int _length = 0;
  List<VoidCallback?>? _listeners = List<VoidCallback?>.filled(0, null);
  int _notificationCallStackDepth = 0;
  int _removedListeners = 0;

  bool _debugAssertNotDisposed() {
    assert(() {
      if (_listeners == null) {
        throw FlutterError('A $runtimeType was used after being disposed.\n'
            'Once you have called dispose() on a $runtimeType, it can no longer be used.');
      }
      return true;
    }());
    return true;
  }

  bool get hasListeners {
    assert(_debugAssertNotDisposed());
    return _length > 0;
  }

  void addListener(VoidCallback listener) {
    assert(_debugAssertNotDisposed());

    if (_length == _listeners!.length) {
      if (_length == 0) {
        _listeners = List<VoidCallback?>.filled(1, null);
      } else {
        final newListeners =
            List<VoidCallback?>.filled(_listeners!.length * 2, null);
        for (int i = 0; i < _length; i++) {
          newListeners[i] = _listeners![i];
        }
        _listeners = newListeners;
      }
    }
    _listeners![_length++] = listener;
  }

  void _removeAt(int index) {
    for (int i = index; i < _length - 1; i++) {
      _listeners![i] = _listeners![i + 1];
    }
    _length--;
  }

  void removeListener(VoidCallback listener) {
    assert(_debugAssertNotDisposed());

    for (int i = 0; i < _length; i++) {
      final _listener = _listeners![i];
      if (_listener == listener) {
        if (_notificationCallStackDepth > 0) {
          _listeners![i] = null;
          _removedListeners++;
        } else {
          _removeAt(i);
        }
        break;
      }
    }
  }

  void dispose() {
    assert(_debugAssertNotDisposed());
    _listeners = null;
  }

  void notifyListeners() {
    assert(_debugAssertNotDisposed());

    if (_length == 0) {
      return;
    }
    _notificationCallStackDepth++;

    final int end = _length;
    for (int i = 0; i < end; i++) {
      try {
        _listeners![i]?.call();
      } catch (exception, stack) {
        FlutterError.reportError(FlutterErrorDetails(
          exception: exception,
          stack: stack,
          library: 'foundation library',
          context: ErrorDescription(
              'while dispatching notifications for $runtimeType'),
          informationCollector: () sync* {
            yield DiagnosticsProperty<ProposedChangeNotifier>(
              'The $runtimeType sending notification was',
              this,
              style: DiagnosticsTreeStyle.errorProperty,
            );
          },
        ));
      }
    }

    _notificationCallStackDepth--;

    if (_notificationCallStackDepth == 0 && _removedListeners > 0) {
      // We really remove the listeners when all notifications are done.
      final newLength = _length - _removedListeners;
      final newListeners = List<VoidCallback?>.filled(newLength, null);

      int newIndex = 0;
      for (int i = 0; i < _length; i++) {
        final listener = _listeners![i];
        if (listener != null) {
          newListeners[newIndex++] = listener;
        }
      }

      _removedListeners = 0;
      _length = newLength;
      _listeners = newListeners;
    }
  }
}

This implementation passes all current tests.

The main drawback of this implementation can be the source code which is more complicated than the previous ones.

But the benchmark results show a lot of benefits and we think that it could be a good opportunity to change the current implementation to this one.

If you think that's a good idea, we can do a PR ourselves and also add some tests to ChangeNotifier.

@darshankawar darshankawar added framework flutter/packages/flutter repository. See also f: labels. perf: memory Performance issues related to memory perf: speed Performance issues related to (mostly rendering) speed proposal A detailed proposal for a change to Flutter severe: performance Relates to speed or footprint issues. passed first triage tests are present, the PR follows the PR template, no obvious coding errors labels Dec 8, 2020
@pedromassangocode pedromassangocode added the customer: crowd Affects or could affect many people, though not necessarily a specific customer. label Dec 8, 2020
@goderbauer
Copy link
Member

goderbauer commented Dec 8, 2020

Those numbers look impressive. I guess, adding listeners is the only place where the proposal doesn't outshine the other implementations?

Is it an oversight that the list only shrinks if a listener is removed during notifyListeners and never when it is removed outside of that?

/cc @Hixie @dnfield

@letsar
Copy link
Contributor Author

letsar commented Dec 8, 2020

For adding listeners we saw something different in our benchmark:


┌─────────────────────────────────────────────────┐
│              addListener benchmark              │
├─────────────┬───────────┬───────────┬───────────┤
│             │  Initial  │  Current  │ Proposed  │
│  Listeners  ├───────────┼───────────┼───────────┤
│             │ Time [µs] │ Time [µs] │ Time [µs] │
├─────────────┼───────────┼───────────┼───────────┤
│           1 │         0 │         0 │         0 │
├─────────────┼───────────┼───────────┼───────────┤
│           2 │         0 │         0 │         1 │
├─────────────┼───────────┼───────────┼───────────┤
│           4 │         0 │         8 │         0 │
├─────────────┼───────────┼───────────┼───────────┤
│           8 │         0 │         8 │         0 │
├─────────────┼───────────┼───────────┼───────────┤
│          16 │         0 │         3 │         0 │
├─────────────┼───────────┼───────────┼───────────┤
│ Total Time: │         0 │        19 │         1 │
└─────────────┴───────────┴───────────┴───────────┘

I don't really know why there is so much difference between our benchmark and the official one for now, but I can look into it.

Yes, good point about the removing of listeners. If an app creates a lot of listeners it could look like a memory leak.
Maybe we should shrink it as well when the _length is half the size of the list. @escamoteur , @mraleph , @knaeckeKami, what do you think?

@knaeckeKami
Copy link
Contributor

knaeckeKami commented Dec 8, 2020

True, we didn't think of that. I hope this does not cost of much of our gains.
Maybe something like:

void removeAt(int index){
  
  _length--;
  if(_length *2 < _listeners!.length){
    final newListeners  = List.filled(_length, null);
    newListeners.replaceRange(0, index -1, _listeners! );
    newListeners.replaceRange(index, _length -1, _listeners!.skip(index) );
    _listeners = newListeners;
    
  }else{
     for (int i = index; i < _length; i++) {
      _listeners![i] = _listeners![i + 1];
    }
  }
  
}

(did not actually check if that works, probably there are off-by-one errors, just an idea). It might also be faster just to use two for loops instead of replaceRange.

@dnfield
Copy link
Member

dnfield commented Dec 8, 2020

I wonder if you'd get some better performance out of making _listeners non-nullable and having a separate bool flag for disposal, and just calling clear on the list in disposal - it would avoid the need for a null check everytime the list is accessed. Probably pretty small though.

I think at some point you would need to shrink the list if it grew too big, but I'd expect that not to happen too much in a real usage - these are really designed to have a relatively small number of listeners, though listeners may be added and removed over time, I would not expect the actual list size to grow much beyond say 16, which shouldn't normally be a problem.

I'd suggest adding the benchmark to our microbenchmarks suite as a starting point. But this seems like a reasonable approach to me overall.

@dnfield
Copy link
Member

dnfield commented Dec 8, 2020

It'd also be interesting to see if it has any impact on, e.g., the transitions perf benchmarks for gallery/new gallery.

@escamoteur
Copy link
Contributor

@letsar @knaeckeKami we already use the removeAt() function if not during a notify. So I don't see the problem with removeListener but maybe I'm just overlooking something. what could be that if our List grew before and then a lot of listeneres where removed the array could still be too big, so we could add a check in removeAt and allocate a smaller List again.

@dnfield If we want to use a ´bool´ flag we can't store the handler at the same place so we would need an aditional node object, which was how my first implementation lookked like. @mraleph then recommended using only the list without addional objects which indeed was faster, so I don't expect that null safe with objects is any faster.

@letsar
Copy link
Contributor Author

letsar commented Dec 8, 2020

@escamoteur the removeAt actually never shrinks the list, it only shift things so that all null are at the end of the list. But yes, we have to make sure that in the case a lot of listeners had been added, we shrink the list when it makes sense.
I add some code to do it and update the benchmarks to see where we go.

For the bool flag, I think there is a misunderstanding. It would be only for making the list not nullable. I did something like that in a previous version but I can compare the two approaches.

@knaeckeKami
Copy link
Contributor

I think @dnfield meant that instead of

  List<VoidCallback?>? _listeners = List<VoidCallback?>.filled(0, null);

we use

  List<VoidCallback?> _listeners = List<VoidCallback?>.filled(0, null);
  bool _isDisposed = false; 

and change dispose() to

void dispose() {
    assert(_debugAssertNotDisposed());
    _listeners.clear();
    _isDisposed = true;
  }

and also change _debugAssertNotDisposed() accordingly and change all the !. accesses to . accesses.

@escamoteur
Copy link
Contributor

@knaeckeKami I thought we had that already in?

@letsar
Copy link
Contributor Author

letsar commented Dec 8, 2020

At a moment, yes, but I thought it could be an unnecessary object. I have to bench it.

@mraleph
Copy link
Member

mraleph commented Dec 8, 2020

Yeah, I would definitely recommend avoiding making listeners nullable and instead use a separate boolean field for assertion purposes. In release mode this field should be removed by the AOT compiler, because it is write-only (you only write to it but never read from it) so it comes at 0 cost for release.

Regarding grow-shrink strategy: this is a problem which does not have a solution which works for all usage patterns, different grow/shrink strategies have different performance trade offs. I'd recommend to try collecting size histograms from real applications to see how ChangeNotifier lists grow and shrink - this might inform your decisions.

@letsar
Copy link
Contributor Author

letsar commented Dec 8, 2020

I ran the benchmarks with this new _removeAt method, and the performances are still ok.

void _removeAt(int index) {
  _length--;
  if (_length * 2 <= _listeners!.length) {
    final List<VoidCallback?> newListeners =
        List<VoidCallback?>.filled(_length, null);

    for (int i = 0; i < index; i++) {
      newListeners[i] = _listeners![i];
    }
    for (int i = index; i < _length; i++) {
      newListeners[i] = _listeners![i + 1];
    }

    _listeners = newListeners;
  } else {
    for (int i = index; i < _length; i++) _listeners![i] = _listeners![i + 1];
  }
}

Concerning the _disposed field proposed by @dnfield, I didn't see noticeable changes in our benchmarks, but I can keep it because it makes the code it little more readable.

@dnfield where can I find the benchmarks for Gallery you mentioned?

@Hixie
Copy link
Member

Hixie commented Dec 9, 2020

If there are benchmarks that you would like to make sure we don't regress when refactoring this code in the future, please make sure to contribute those also (ideally in a PR before landing the improvement, so that we can verify that the PR does improve matters in the first place). There's already some benchmarks around this as you have noticed.

@letsar
Copy link
Contributor Author

letsar commented Dec 9, 2020

@Hixie I made this PR (#71986) for benchmarking removeListener during notifyListener.

@dnfield
Copy link
Member

dnfield commented Dec 9, 2020

@letsar - something like this should do it

cd dev/devicelab
dart bin/run.dart -t flutter_gallery__transition_perf

You can also try new_gallery__transition_perf.

@dnfield
Copy link
Member

dnfield commented Dec 9, 2020

(you'll need an attached android device for those)

@letsar
Copy link
Contributor Author

letsar commented Dec 9, 2020

@dnfield I found the tests, but I'm unable to launch them.
After I saw 'Device chosen: XXXXXX' I have a this error:

Unhandled exception:
JSON-RPC error -32000: Server error
package:json_rpc_2/src/client.dart 123:62              Client.sendRequest
package:json_rpc_2/src/peer.dart 98:15                 Peer.sendRequest
package:vm_service_client/src/scope.dart 64:23         Scope.sendRequestRaw
package:vm_service_client/src/isolate.dart 361:19      VMIsolateRef.invokeExtension
package:flutter_devicelab/framework/runner.dart 83:63  runTask

@dnfield
Copy link
Member

dnfield commented Dec 9, 2020

If there are any other logs that may be helpful.

Is your device unlocked?

@letsar
Copy link
Contributor Author

letsar commented Dec 9, 2020

The device is unlocked, and I'm able to run flutter apps on it.

Here the full trace:

$ dart bin/run.dart -t new_gallery__transition_perf

════════════╡ ••• Running task "new_gallery__transition_perf" ••• ╞═════════════

Executing: /Users/xxxxxx/Dev/flutter_fork/flutter/bin/cache/dart-sdk/bin/dart --disable-dart-dev --enable-vm-service=0 --no-pause-isolates-on-exit bin/tasks/new_gallery__transition_perf.dart in /Users/xxxxxx/Dev/flutter_fork/flutter/dev/devicelab with environment {}
[new_gallery__transition_perf] [STDOUT] Observatory listening on http://127.0.0.1:56712/kHW2NxpVbZw=/
[new_gallery__transition_perf] [STDOUT] Running task with a timeout of null.
[new_gallery__transition_perf] [STDOUT]
[new_gallery__transition_perf] [STDOUT]
[new_gallery__transition_perf] [STDOUT] ══════════════════╡ ••• Checking running Dart processes ••• ╞═══════════════════
[new_gallery__transition_perf] [STDOUT]
[new_gallery__transition_perf] [STDOUT] Checking for reboot
[new_gallery__transition_perf] [STDOUT]
[new_gallery__transition_perf] [STDOUT] Executing: /Users/xxxxxx/Library/Android/sdk/platform-tools/adb devices -l in /Users/xxxxxx/Dev/flutter_fork/flutter/dev/devicelab
[new_gallery__transition_perf] [STDOUT] stdout: List of devices attached
[new_gallery__transition_perf] [STDOUT] stdout: XXXXXX device usb:YYYYYY product:OnePlus8Pro_EEA model:IN2023 device:OnePlus8Pro transport_id:13
[new_gallery__transition_perf] [STDOUT] stdout:
[new_gallery__transition_perf] [STDOUT] "/Users/xxxxxx/Library/Android/sdk/platform-tools/adb" exit code: 0
[new_gallery__transition_perf] [STDOUT]
[new_gallery__transition_perf] [STDOUT] Executing: /Users/xxxxxx/Library/Android/sdk/platform-tools/adb -s XXXXXX shell getprop ro.bootimage.build.fingerprint ; getprop ro.build.version.release ; getprop ro.build.version.sdk in /Users/xxxxxx/Dev/flutter_fork/flutter/dev/devicelab
[new_gallery__transition_perf] [STDOUT] Device chosen: XXXXXX
Unhandled exception:
JSON-RPC error -32000: Server error
package:json_rpc_2/src/client.dart 123:62 Client.sendRequest
package:json_rpc_2/src/peer.dart 98:15 Peer.sendRequest
package:vm_service_client/src/scope.dart 64:23 Scope.sendRequestRaw
package:vm_service_client/src/isolate.dart 361:19 VMIsolateRef.invokeExtension
package:flutter_devicelab/framework/runner.dart 83:63 runTask
===== asynchronous gap ===========================
bin/run.dart 111:31 _runTasks
===== asynchronous gap ===========================
bin/run.dart 104:5

@dnfield
Copy link
Member

dnfield commented Dec 9, 2020

Hmm. That seems strange. There's a patch coming to remove vm_client_service which may help this, maybe once that lands we can try again.

@knaeckeKami
Copy link
Contributor

knaeckeKami commented Dec 9, 2020

I was able to run in on 1.24.0-10.2.pre

flutter_gallery__transition_perf :

Old Value notifier:

    "average_frame_build_time_millis": 1.174159528907923,
    "90th_percentile_frame_build_time_millis": 1.698,
    "99th_percentile_frame_build_time_millis": 9.582,
    "worst_frame_build_time_millis": 22.488,
    "missed_frame_build_budget_count": 3,
    "average_frame_rasterizer_time_millis": 5.107235042735051,
    "90th_percentile_frame_rasterizer_time_millis": 5.786,
    "99th_percentile_frame_rasterizer_time_millis": 23.925,
    "worst_frame_rasterizer_time_millis": 76.047,
    "missed_frame_rasterizer_budget_count": 14,
    "frame_count": 934,
    "frame_rasterizer_count": 936,

New value notifier:

    "average_frame_build_time_millis": 1.14836712913554,
    "90th_percentile_frame_build_time_millis": 1.6,
    "99th_percentile_frame_build_time_millis": 9.42,
    "worst_frame_build_time_millis": 19.687,
    "missed_frame_build_budget_count": 2,
    "average_frame_rasterizer_time_millis": 5.00333049040511,
    "90th_percentile_frame_rasterizer_time_millis": 5.702,
    "99th_percentile_frame_rasterizer_time_millis": 22.119,
    "worst_frame_rasterizer_time_millis": 77.254,
    "missed_frame_rasterizer_budget_count": 14,
    "frame_count": 937,
    "frame_rasterizer_count": 938,

new_gallery__transition_perf :

Old value notifier:

    "average_frame_build_time_millis": 1.5316753926701585,
    "90th_percentile_frame_build_time_millis": 1.982,
    "99th_percentile_frame_build_time_millis": 12.621,
    "worst_frame_build_time_millis": 51.118,
    "missed_frame_build_budget_count": 11,
    "average_frame_rasterizer_time_millis": 6.200340091563104,
    "90th_percentile_frame_rasterizer_time_millis": 6.624,
    "99th_percentile_frame_rasterizer_time_millis": 27.202,
    "worst_frame_rasterizer_time_millis": 321.044,
    "missed_frame_rasterizer_budget_count": 23,
    "frame_count": 1528,
    "frame_rasterizer_count": 1529,

New value notifier:

   "average_frame_build_time_millis": 1.5151169170476826,
    "90th_percentile_frame_build_time_millis": 1.954,
    "99th_percentile_frame_build_time_millis": 11.552,
    "worst_frame_build_time_millis": 56.567,
    "missed_frame_build_budget_count": 10,
    "average_frame_rasterizer_time_millis": 6.144109660574416,
    "90th_percentile_frame_rasterizer_time_millis": 6.665,
    "99th_percentile_frame_rasterizer_time_millis": 32.115,
    "worst_frame_rasterizer_time_millis": 157.345,
    "missed_frame_rasterizer_budget_count": 25,
    "frame_count": 1531,
    "frame_rasterizer_count": 1532,

Seems a bit faster I don't know if it's significant though. I ran it only once.
Full results: https://gist.github.com/knaeckeKami/c5cfd986d68f4a8c433705c6afa6fdc2

@dnfield
Copy link
Member

dnfield commented Dec 9, 2020

Nice. If you want you might try running it a few more times to check for noise, but this does seem like it improves some numbers there signfiicantly percentage wise, although there are a few in there that are slightly worse (I'd wonder if that's just noise though).

At any rate, this gives some more confidence that the improvements are meaningful for apps and not just something that's helping a narrow microbenchmark :)

@knaeckeKami
Copy link
Contributor

I ran both benchmarks 5 times with both implementations respectively.
According to my measurements, it seems to improve the performance slightly, but actually much more than I would have anticipated for an app like flutter_gallery just for tuning ValueNotifier.

Here are the results:
https://docs.google.com/spreadsheets/d/1L3YjtpE84tYnchWmLwoZ6eB1XFq19gFON5szlV4hZzo/edit?usp=sharing

I'm neither a microbenchmark guru nor a statistician, and there seems to be quite some noise, especially in the worst_time and 99 percentile measurements - we would probably need more measurements or filter some of the outliers (see flutter_gallery__transition_perf - new value notifier - missed_frame_rasterizer_budget_count - Run 2).

@escamoteur
Copy link
Contributor

@knaeckeKami If we can see this in the application test so strongly, we should instrument the notifier and get numbers of add, remove, notifiy (with number of listeners), add/remove while notify, number of array reallocations.
I made it while we made the tests. I have to check if I still have it on my laptop that at is occupied by my wife at the moment.

@knaeckeKami
Copy link
Contributor

Yeah, would be interesting to see. Also maybe interesting for the grow/shrink strategies.

@colinpoirier
Copy link

In notifyListeners() , is it worth checking if (newLength > 0) before the for loop? Or is newLength == 0 too low a probability?

@escamoteur
Copy link
Contributor

I'll create an instrumentated version then we see more. But I guess the code for a look won't be so different than having an addtional if around.
it will be something like
´´´
while(not end of list)
...
´´´

@escamoteur
Copy link
Contributor

I created this instrumented version here https://gist.github.com/escamoteur/6fd70cc9a4895e133a4b6dede6e4235f

Possibly this path for the logfile has to be changed:
grafik

It creates a ton of data on everey function that is called on a changenotifier. I try to create sort of UID for every change notifier so we could also observer how one behaves over a livetime.

@knaeckeKami could you run this one with the performacetest of the GaleryApp? Or is there any other app that we could run with this?

I hope someone of you is able afterwards to draw some conclusions on the _listener size and resizing strategy from the created data.

@knaeckeKami
Copy link
Contributor

Look interesting. I'll look into it tomorrow or the day after tomorrow.

@colinpoirier
Copy link

I believe any differences in the benchmark performances are due to AOT optimizations.
When testing the official benchmark on a 2016 Pixel with Flutter 1.24.0-10.2.pre, a growable version of this is more performant and has a 300 byte smaller APK.
For the OP benchmark, the fixed length is more performant and is 700 bytes smaller.
Which benchmark is more indicative of in-app usage?

@escamoteur
Copy link
Contributor

@colinpoirier Sorry but I'm not able to follow what you want to say. All versions get AOT optimization. Which is the growable version? What is the OP benchmark?

@knaeckeKami
Copy link
Contributor

I now ran the instrumented version @escamoteur .
Results:
flutter_gallery__transition_perf
https://gist.github.com/knaeckeKami/45b66471650ced9b35842a936650f50f
new_gallery__transition_perf
https://gist.github.com/knaeckeKami/f6ce6a3103e657551cbc96b101b32c9f

@escamoteur
Copy link
Contributor

Now we only need someone who can make the most sense of this data :-)

@kf6gpe
Copy link
Contributor

kf6gpe commented Jan 11, 2021

Appreciate the conversation on this thread. I'm tentatively assigning this P5, our usual priority for proposals and feature requests. Feel free to escalate the priority if we actively start working on this.

@kf6gpe kf6gpe added the P5 Priority 5 issue (default for new feature requests; things we'd like to work on) label Jan 11, 2021
@escamoteur
Copy link
Contributor

@kf6gpe there is actuall already a PR that is widely aceepted in the pipeline :-)

@kf6gpe
Copy link
Contributor

kf6gpe commented Jan 12, 2021

Sweet! Thanks, @escamoteur . Back to P3, since we're working on it! :)

@kf6gpe kf6gpe added P3 Priority 3 issue (the default for issues we're likely to work on after P0-P2 issues) and removed P5 Priority 5 issue (default for new feature requests; things we'd like to work on) labels Jan 12, 2021
@colinpoirier
Copy link

Apologies, @escamoteur hopefully this helps clear things up.
Growable version means a version of this proposed ChangeNotifier that uses a growable list instead of a fixed length list.
The OP benchmark is the one in the original post that outputs the nicely formatted table under "Our Benchmark."

My previous comment proposed a theory with supporting data in an attempt to explain the difference in addListener performance between the screenshot of the Flutter benchmark in the original post and the table of the addlistener benchmark posted in another comment.

And that lead to my question in the comment about which benchmark is more representative of in-app performance/optimization.

I think I was able to answer that question.
Assuming there is a relationship between performance and APK size (in this narrow context), as seen in my previous comment, I compiled the Flutter Gallery app using the LinkedList ChangeNotifier as a baseline. Switching to this proposed implementation shrank the APK by over 10 kilobytes. A growable list version of this proposal only shrank the APK by 4 kilobytes.

I look forward to the PR landing and hopefully in the future this implementation making it's way to Animations and other Listenables.

With this being the accepted implementation, should a member of the Flutter team address this older and similar proposal? #61619

@goderbauer
Copy link
Member

Closing this one as that PR is submitted. I posted some graphs on the PR showing the impact that this had on the microbenchmarks: #71947 (comment)

@github-actions
Copy link

github-actions bot commented Aug 2, 2021

This thread has been automatically locked since there has not been any recent activity after it was closed. If you are still experiencing a similar issue, please open a new bug, including the output of flutter doctor -v and a minimal reproduction of the issue.

@github-actions github-actions bot locked as resolved and limited conversation to collaborators Aug 2, 2021
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
customer: crowd Affects or could affect many people, though not necessarily a specific customer. framework flutter/packages/flutter repository. See also f: labels. P3 Priority 3 issue (the default for issues we're likely to work on after P0-P2 issues) passed first triage tests are present, the PR follows the PR template, no obvious coding errors perf: memory Performance issues related to memory perf: speed Performance issues related to (mostly rendering) speed proposal A detailed proposal for a change to Flutter severe: performance Relates to speed or footprint issues.
Projects
None yet
Development

No branches or pull requests