escamoteur / watch_it

MIT License
103 stars 8 forks source link

Gradual Performance Degradation using watchPropertyValue on ChangeNotifier Member #32

Open JMare opened 3 weeks ago

JMare commented 3 weeks ago

I have been debugging a gradual performance slow down when passing frequent state updates through watchPropertyValue. It seems like calls to notifyListeners gradually take more and more time to return, if left for long enough it will begin to affect the responsiveness of the ui. Using the "watch" method rather than "watchPropertyValue" solves the problem. The more listeners there are, the faster the performance will degrade, but the issue still occurs with a single listener.

I don't know if this is a bug or my improper usage of the library.

This graph shows the time spent calling notifyListeners gradually increasing over time. The line of code used in the build method is: final double localVal = watchPropertyValue((TestModel d) => d.val); watch_it_watchpropertyvalue

For comparison, here is the same thing but using the following line: final double localVal = watch(di<TestModel>()).val; watch_it_watch Equivalent code using provider, much like the watch method, has no slow down over time.

Versions pubspec.yaml ``` name: watch_it_test description: "A new Flutter project." publish_to: 'none' version: 0.1.0 environment: sdk: '>=3.4.1 <4.0.0' dependencies: flutter: sdk: flutter path_provider: ^2.1.3 watch_it: ^1.4.2 dev_dependencies: flutter_test: sdk: flutter flutter_lints: ^3.0.0 flutter: uses-material-design: true ``` flutter doctor ``` watch_it_test flutter doctor Doctor summary (to see all details, run flutter doctor -v): [✓] Flutter (Channel stable, 3.22.1, on macOS 14.5 23F79 darwin-x64, locale en-AU) [✗] Android toolchain - develop for Android devices ✗ Unable to locate Android SDK. Install Android Studio from: https://developer.android.com/studio/index.html On first launch it will assist you in installing the Android SDK components. (or visit https://flutter.dev/docs/get-started/install/macos#android-setup for detailed instructions). If the Android SDK has been installed to a custom location, please use `flutter config --android-sdk` to update to that location. [✓] Xcode - develop for iOS and macOS (Xcode 15.4) [✓] Chrome - develop for the web [!] Android Studio (not installed) [✓] VS Code (version 1.90.0) [✓] Connected device (2 available) [✓] Network resources ! Doctor found issues in 2 categories. ```
Full testable code that generated these graphs ```dart import 'dart:async'; import 'dart:io'; import 'dart:math'; import 'package:flutter/material.dart'; import 'package:watch_it/watch_it.dart'; import 'package:path_provider/path_provider.dart'; class TestModel extends ChangeNotifier{ double _val = 0.0; get val => _val; int _setterCallCount = 0; int get setterCallCount => _setterCallCount; Duration _lastSetterDuration = Duration.zero; Duration get lastSetterDuration => _lastSetterDuration; set val(newVal){ if(_val != newVal) { final stopwatch = Stopwatch()..start(); _val = newVal; notifyListeners(); stopwatch.stop(); _lastSetterDuration = stopwatch.elapsed; _setterCallCount++; } } } void main() { di.registerSingleton(TestModel()); runApp(const MainApp()); _initializeCsvFile(); // Start background task Timer.periodic(Duration(microseconds: 1000), (timer) { var randomValue = Random().nextDouble(); di().val = randomValue; }); // Print and store stats every 100ms final startTime = DateTime.now(); Timer.periodic(Duration(milliseconds: 100), (timer) { var model = di(); final elapsed = DateTime.now().difference(startTime).inMilliseconds; final lastDuration = model.lastSetterDuration.inMicroseconds; final callCount = model.setterCallCount; print('Time since start: $elapsed ms, Setter call count: $callCount, Last setter duration: $lastDuration microseconds'); _writeToCsv(elapsed, callCount, lastDuration); }); } void _initializeCsvFile() async { final directory = await getApplicationDocumentsDirectory(); final path = '${directory.path}/watch_it_setter_data.csv'; final file = File(path); if (!await file.exists()) { await file.writeAsString('Time since start (ms),Function calls,Time spent in setter (µs)\n'); } } void _writeToCsv(int elapsed, int callCount, int lastDuration) async { final directory = await getApplicationDocumentsDirectory(); final path = '${directory.path}/watch_it_setter_data.csv'; final file = File(path); final data = '$elapsed,$callCount,$lastDuration\n'; await file.writeAsString(data, mode: FileMode.append); } class MainApp extends StatelessWidget { const MainApp({super.key}); @override Widget build(BuildContext context) { return const MaterialApp( home: Scaffold( body: Column( children: [ Text("Watch_it test"), DisplayWidget(), DisplayWidget(), DisplayWidget(), DisplayWidget(), DisplayWidget(), DisplayWidget(), DisplayWidget(), DisplayWidget(), DisplayWidget(), DisplayWidget(), DisplayWidget(), DisplayWidget(), ], ), ), ); } } class DisplayWidget extends WatchingWidget { const DisplayWidget({ super.key, }); @override Widget build(BuildContext context) { final double localVal = watchPropertyValue((TestModel d) => d.val); //final double localVal = watch(di()).val; return Center( child: Text('Val: ${localVal.toStringAsFixed(2)}'), ); } } ```
escamoteur commented 3 weeks ago

Hey, thanks for this analysis. I will look into it. Interesting that it only happens with watchPropertyValue. I mainly use watch or watchValue so I haven't observed this myself yet.

Cheers Thomas Am 9. Juni 2024, 12:08 +0100 schrieb James Mare @.***>:

I have been debugging a gradual performance slow down when passing frequent state updates through watchPropertyValue. It seems like calls to notifyListeners gradually take more and more time to return, if left for long enough it will begin to affect the responsiveness of the ui. Using the "watch" method rather than "watchPropertyValue" solves the problem. The more listeners there are, the faster the performance will degrade, but the issue still occurs with a single listener. I don't know if this is a bug or my improper usage of the library. This graph shows the time spent calling notifyListeners gradually increasing over time. The line of code used in the build method is: final double localVal = watchPropertyValue((TestModel d) => d.val); watch_it_watchpropertyvalue.png (view on web) For comparison, here is the same thing but using the following line: final double localVal = watch(di()).val; watch_it_watch.png (view on web) Equivalent code using provider, much like the watch method, has no slow down over time. Versions pubspec.yaml name: watch_it_test description: "A new Flutter project." publish_to: 'none' version: 0.1.0

environment: sdk: '>=3.4.1 <4.0.0'

dependencies: flutter: sdk: flutter path_provider: ^2.1.3 watch_it: ^1.4.2

dev_dependencies: flutter_test: sdk: flutter flutter_lints: ^3.0.0

flutter: uses-material-design: true

flutter doctor watch_it_test flutter doctor Doctor summary (to see all details, run flutter doctor -v): [✓] Flutter (Channel stable, 3.22.1, on macOS 14.5 23F79 darwin-x64, locale en-AU) [✗] Android toolchain - develop for Android devices ✗ Unable to locate Android SDK. Install Android Studio from: https://developer.android.com/studio/index.html On first launch it will assist you in installing the Android SDK components. (or visit https://flutter.dev/docs/get-started/install/macos#android-setup for detailed instructions). If the Android SDK has been installed to a custom location, please use flutter config --android-sdk to update to that location.

[✓] Xcode - develop for iOS and macOS (Xcode 15.4) [✓] Chrome - develop for the web [!] Android Studio (not installed) [✓] VS Code (version 1.90.0) [✓] Connected device (2 available) [✓] Network resources

! Doctor found issues in 2 categories. Full testable code that generated these graphs import 'dart:async'; import 'dart:io'; import 'dart:math'; import 'package:flutter/material.dart'; import 'package:watch_it/watch_it.dart'; import 'package:path_provider/path_provider.dart';

class TestModel extends ChangeNotifier{ double _val = 0.0; get val => _val;

int _setterCallCount = 0; int get setterCallCount => _setterCallCount;

Duration _lastSetterDuration = Duration.zero; Duration get lastSetterDuration => _lastSetterDuration;

set val(newVal){ if(_val != newVal) { final stopwatch = Stopwatch()..start(); _val = newVal; notifyListeners(); stopwatch.stop(); _lastSetterDuration = stopwatch.elapsed; _setterCallCount++; } } }

void main() { di.registerSingleton(TestModel()); runApp(const MainApp());

_initializeCsvFile();

// Start background task Timer.periodic(Duration(microseconds: 1000), (timer) { var randomValue = Random().nextDouble(); di().val = randomValue; });

// Print and store stats every 100ms final startTime = DateTime.now(); Timer.periodic(Duration(milliseconds: 100), (timer) { var model = di(); final elapsed = DateTime.now().difference(startTime).inMilliseconds; final lastDuration = model.lastSetterDuration.inMicroseconds; final callCount = model.setterCallCount;

print('Time since start: $elapsed ms, Setter call count: $callCount, Last setter duration: $lastDuration microseconds');

_writeToCsv(elapsed, callCount, lastDuration); }); } void _initializeCsvFile() async { final directory = await getApplicationDocumentsDirectory(); final path = '${directory.path}/watch_it_setter_data.csv'; final file = File(path);

if (!await file.exists()) { await file.writeAsString('Time since start (ms),Function calls,Time spent in setter (µs)\n'); } } void _writeToCsv(int elapsed, int callCount, int lastDuration) async { final directory = await getApplicationDocumentsDirectory(); final path = '${directory.path}/watch_it_setter_data.csv'; final file = File(path);

final data = '$elapsed,$callCount,$lastDuration\n'; await file.writeAsString(data, mode: FileMode.append); }

class MainApp extends StatelessWidget { const MainApp({super.key});

@override Widget build(BuildContext context) { return const MaterialApp( home: Scaffold( body: Column( children: [ Text("Watch_it test"), DisplayWidget(), DisplayWidget(), DisplayWidget(), DisplayWidget(), DisplayWidget(), DisplayWidget(), DisplayWidget(), DisplayWidget(), DisplayWidget(), DisplayWidget(), DisplayWidget(), DisplayWidget(), ], ), ), ); } }

class DisplayWidget extends WatchingWidget { const DisplayWidget({ super.key, });

@override Widget build(BuildContext context) { final double localVal = watchPropertyValue((TestModel d) => d.val); //final double localVal = watch(di()).val; return Center( child: Text('Val: ${localVal.toStringAsFixed(2)}'), ); } } — Reply to this email directly, view it on GitHub, or unsubscribe. You are receiving this because you are subscribed to this thread.Message ID: @.***>

escamoteur commented 3 weeks ago

I think I found the problem. Could you try with the latest version on github?

Cheers Thomas

Message ID: ***@***.***>
JMare commented 3 weeks ago

Yep, Tested git main and the issue is gone - now watchPropertyValue performs the same as watch. Thanks for the quick fix!

escamoteur commented 3 weeks ago

is it ok to use the git reference for some days till I push a new release? Cheers Thomas Nachricht von James Mare @.***> am 9. Juni 2024, 13:37 +0100:

Yep, Tested git main and the issue is gone - now watchPropertyValue performs the same as watch. Thanks for the quick fix! — Reply to this email directly, view it on GitHub, or unsubscribe. You are receiving this because you commented.Message ID: @.***>

JMare commented 3 weeks ago

Yep, no rush, I actually already switched my project to use the watch function before I posted this issue