From 36e5a1c17ba8895fbfee35d1a6bf5f3b77eb5ce8 Mon Sep 17 00:00:00 2001 From: Ming Ming Date: Sun, 1 Aug 2021 01:28:08 +0800 Subject: [PATCH] Refactoring: extract throttling logic --- lib/bloc/scan_dir.dart | 51 ++++++++++++------------- lib/throttler.dart | 84 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 110 insertions(+), 25 deletions(-) create mode 100644 lib/throttler.dart diff --git a/lib/bloc/scan_dir.dart b/lib/bloc/scan_dir.dart index 16318fe9..21a37106 100644 --- a/lib/bloc/scan_dir.dart +++ b/lib/bloc/scan_dir.dart @@ -9,6 +9,7 @@ import 'package:nc_photos/entity/file.dart'; import 'package:nc_photos/entity/file/data_source.dart'; import 'package:nc_photos/event/event.dart'; import 'package:nc_photos/iterable_extension.dart'; +import 'package:nc_photos/throttler.dart'; import 'package:nc_photos/use_case/scan_dir.dart'; abstract class ScanDirBlocEvent { @@ -119,6 +120,13 @@ class ScanDirBloc extends Bloc { AppEventListener(_onFilePropertyUpdatedEvent); _fileRemovedEventListener.begin(); _filePropertyUpdatedEventListener.begin(); + + _refreshThrottler = Throttler( + onTriggered: (_) { + add(_ScanDirBlocExternalEvent()); + }, + logTag: "ScanDirBloc.refresh", + ); } static ScanDirBloc of(Account account) { @@ -163,7 +171,7 @@ class ScanDirBloc extends Bloc { close() { _fileRemovedEventListener.end(); _filePropertyUpdatedEventListener.end(); - _propertyUpdatedSubscription?.cancel(); + _refreshThrottler.clear(); return super.close(); } @@ -209,7 +217,10 @@ class ScanDirBloc extends Bloc { // no data in this bloc, ignore return; } - add(_ScanDirBlocExternalEvent()); + _refreshThrottler.trigger( + maxResponceTime: const Duration(seconds: 3), + maxPendingCount: 10, + ); } void _onFilePropertyUpdatedEvent(FilePropertyUpdatedEvent ev) { @@ -226,28 +237,19 @@ class ScanDirBloc extends Bloc { return; } - _successivePropertyUpdatedCount += 1; - _propertyUpdatedSubscription?.cancel(); - // only trigger the event on the 10th update or 10s after the last update - if (_successivePropertyUpdatedCount % 10 == 0) { - add(_ScanDirBlocExternalEvent()); + if (ev.hasAnyProperties([ + FilePropertyUpdatedEvent.propIsArchived, + FilePropertyUpdatedEvent.propOverrideDateTime, + ])) { + _refreshThrottler.trigger( + maxResponceTime: const Duration(seconds: 3), + maxPendingCount: 10, + ); } else { - if (ev.hasAnyProperties([ - FilePropertyUpdatedEvent.propIsArchived, - FilePropertyUpdatedEvent.propOverrideDateTime, - ])) { - _propertyUpdatedSubscription = - Future.delayed(const Duration(seconds: 2)).asStream().listen((_) { - add(_ScanDirBlocExternalEvent()); - _successivePropertyUpdatedCount = 0; - }); - } else { - _propertyUpdatedSubscription = - Future.delayed(const Duration(seconds: 10)).asStream().listen((_) { - add(_ScanDirBlocExternalEvent()); - _successivePropertyUpdatedCount = 0; - }); - } + _refreshThrottler.trigger( + maxResponceTime: const Duration(seconds: 10), + maxPendingCount: 10, + ); } } @@ -286,8 +288,7 @@ class ScanDirBloc extends Bloc { late AppEventListener _filePropertyUpdatedEventListener; - int _successivePropertyUpdatedCount = 0; - StreamSubscription? _propertyUpdatedSubscription; + late Throttler _refreshThrottler; bool _shouldCheckCache = true; diff --git a/lib/throttler.dart b/lib/throttler.dart new file mode 100644 index 00000000..07e333fa --- /dev/null +++ b/lib/throttler.dart @@ -0,0 +1,84 @@ +import 'dart:async'; +import 'dart:math' as math; + +import 'package:flutter/rendering.dart'; +import 'package:logging/logging.dart'; +import 'package:nc_photos/int_util.dart'; + +/// Throttle how many times an event could be triggered +/// +/// Events can be filtered by 2 ways: +/// 1. Time passed after the last event +/// 2. Number of events +class Throttler { + Throttler({ + required this.onTriggered, + this.logTag, + }); + + /// Post an event + /// + /// [data] can be used to provide optional data. When the stream of events + /// eventually trigger, a list of all data received will be passed to the + /// callback. Nulls are ignored + void trigger({ + Duration maxResponceTime = const Duration(seconds: 1), + int? maxPendingCount, + T? data, + }) { + _count += 1; + if (data != null) { + _data.add(data); + } + _subscription?.cancel(); + _subscription = null; + if (maxPendingCount != null) { + _maxCount = math.min(maxPendingCount, _maxCount ?? int32Max); + } + if (_maxCount != null && _count >= _maxCount!) { + _log.info("[trigger]$_logTag Triggered after $_count events"); + _doTrigger(); + } else { + final responseTime = _minDuration( + maxResponceTime, _currentResponseTime ?? Duration(days: 1)); + _subscription = Future.delayed(responseTime).asStream().listen((event) { + _log.info("[trigger]$_logTag Triggered after $responseTime"); + _doTrigger(); + }); + _currentResponseTime = responseTime; + } + } + + /// Drop all pending triggers, this may be useful in places like [dispose] + void clear() { + _subscription?.cancel(); + _subscription = null; + _currentResponseTime = null; + _count = 0; + _maxCount = null; + _data = []; + } + + void _doTrigger() { + onTriggered?.call(_data); + clear(); + } + + String get _logTag => logTag == null ? "" : "[$logTag]"; + + final ValueChanged>? onTriggered; + /// Extra tag printed with logs from this class + final String? logTag; + + StreamSubscription? _subscription; + Duration? _currentResponseTime; + int _count = 0; + int? _maxCount; + var _data = []; + + static final _log = Logger("throttler.Throttler"); +} + +Duration _minDuration(Duration a, Duration b) { + return a.compareTo(b) < 0 ? a : b; +}