From e47968a82cc069b8fd2798104098024e2cbda39a Mon Sep 17 00:00:00 2001 From: Reinis Sprogis Date: Fri, 23 Feb 2024 15:29:04 +0200 Subject: [PATCH 1/6] Add loading delay for tile layer updates fix [BUG] Bad performance in android web browser #1839 --- lib/src/layer/tile_layer/tile_layer.dart | 51 +++++++++++++++++++++++- 1 file changed, 49 insertions(+), 2 deletions(-) diff --git a/lib/src/layer/tile_layer/tile_layer.dart b/lib/src/layer/tile_layer/tile_layer.dart index 1f3986a75..a36f3e80a 100644 --- a/lib/src/layer/tile_layer/tile_layer.dart +++ b/lib/src/layer/tile_layer/tile_layer.dart @@ -207,6 +207,19 @@ class TileLayer extends StatefulWidget { /// no affect. final TileUpdateTransformer tileUpdateTransformer; + /// Defines the minimum time delay (in milliseconds) before map layers are allowed to rebuild. + /// + /// This delay acts as a debounce period to prevent frequent reloading of tile layers in response to rapid, successive events (e.g., zooming or panning). + /// + /// By setting this delay, we ensure that map layer updates are performed only after a period of inactivity, enhancing performance and user experience. + /// + /// - If multiple events occur within this delay period, only the last event triggers the tile layer update, reducing unnecessary processing and network requests. + /// + /// - This is particularly useful for events that are triggered often and rapidly, such as map movements or viewport changes. + /// + /// - The delay is measured from the last received event. Once the specified time has passed without any new events, the tile layers are updated to reflect the latest state. + final int loadingDelay; + /// Create a new [TileLayer] for the [FlutterMap] widget. TileLayer({ super.key, @@ -229,6 +242,7 @@ class TileLayer extends StatefulWidget { this.wmsOptions, this.tileDisplay = const TileDisplay.fadeIn(), + /// See [RetinaMode] for more information /// /// Defaults to `false` when `null`. @@ -238,6 +252,7 @@ class TileLayer extends StatefulWidget { this.evictErrorTileStrategy = EvictErrorTileStrategy.none, this.reset, this.tileBounds, + this.loadingDelay = 50, TileUpdateTransformer? tileUpdateTransformer, String userAgentPackageName = 'unknown', }) : assert( @@ -346,6 +361,38 @@ class _TileLayerState extends State with TickerProviderStateMixin { _loadAndPruneInVisibleBounds(MapCamera.of(context)); }); + + +Timer? _delayTimer; +int _lastUpdateTime = 0; + +void _loadingDelay(void Function() action) { + //execute immediately if delay is 0. + if(widget.loadingDelay == 0) { + action(); + return; + } + + final int now = DateTime.now().millisecondsSinceEpoch; + + // Cancel the existing timer if there's one + _delayTimer?.cancel(); + + // Calculate the time since the last update + final int timeSinceLastUpdate = now - _lastUpdateTime; + + // Reset the timer to wait for the debounce duration + _delayTimer = Timer(Duration(milliseconds: widget.loadingDelay), () { + _lastUpdateTime = DateTime.now().millisecondsSinceEpoch; + action(); + }); + + // Update the last update time if it's the first event or if the debounce duration has already passed + if (timeSinceLastUpdate >= widget.loadingDelay) { + _lastUpdateTime = now; + } +} + // This is called on every map movement so we should avoid expensive logic // where possible. @override @@ -362,7 +409,7 @@ class _TileLayerState extends State with TickerProviderStateMixin { _tileUpdateSubscription = mapController.mapEventStream .map((mapEvent) => TileUpdateEvent(mapEvent: mapEvent)) .transform(widget.tileUpdateTransformer) - .listen((event) => _onTileUpdateEvent(event)); + .listen((event) => _loadingDelay(() => _onTileUpdateEvent(event))); } var reloadTiles = false; @@ -457,7 +504,7 @@ class _TileLayerState extends State with TickerProviderStateMixin { _resetSub?.cancel(); _pruneLater?.cancel(); widget.tileProvider.dispose(); - + _delayTimer?.cancel(); super.dispose(); } From 2a3067797532a4a76c744e5e20a515049473bfc8 Mon Sep 17 00:00:00 2001 From: Reinis Sprogis Date: Fri, 23 Feb 2024 20:51:19 +0200 Subject: [PATCH 2/6] Refactor loading delay logic in tile layer --- lib/src/layer/tile_layer/tile_layer.dart | 21 +++++++-------------- 1 file changed, 7 insertions(+), 14 deletions(-) diff --git a/lib/src/layer/tile_layer/tile_layer.dart b/lib/src/layer/tile_layer/tile_layer.dart index a36f3e80a..9675b9e73 100644 --- a/lib/src/layer/tile_layer/tile_layer.dart +++ b/lib/src/layer/tile_layer/tile_layer.dart @@ -363,9 +363,11 @@ class _TileLayerState extends State with TickerProviderStateMixin { +// Delay Timer for loadingDelay Timer? _delayTimer; -int _lastUpdateTime = 0; +// This method is used to delay the execution of a function by the specified [loadingDelay]. +// This is useful to prevent frequent reloading of tile layers in response to rapid, successive events (e.g., zooming or panning). void _loadingDelay(void Function() action) { //execute immediately if delay is 0. if(widget.loadingDelay == 0) { @@ -373,24 +375,15 @@ void _loadingDelay(void Function() action) { return; } - final int now = DateTime.now().millisecondsSinceEpoch; - - // Cancel the existing timer if there's one - _delayTimer?.cancel(); - - // Calculate the time since the last update - final int timeSinceLastUpdate = now - _lastUpdateTime; + // Cancel the previous timer if it is still active + if(_delayTimer != null && _delayTimer!.isActive) { + _delayTimer!.cancel(); + } // Reset the timer to wait for the debounce duration _delayTimer = Timer(Duration(milliseconds: widget.loadingDelay), () { - _lastUpdateTime = DateTime.now().millisecondsSinceEpoch; action(); }); - - // Update the last update time if it's the first event or if the debounce duration has already passed - if (timeSinceLastUpdate >= widget.loadingDelay) { - _lastUpdateTime = now; - } } // This is called on every map movement so we should avoid expensive logic From d244eae6ed83d58e563235d64d7d1ab4ee36252b Mon Sep 17 00:00:00 2001 From: Joscha <34318751+josxha@users.noreply.github.com> Date: Sat, 24 Feb 2024 12:40:49 +0100 Subject: [PATCH 3/6] dart format --- lib/src/layer/tile_layer/tile_layer.dart | 35 +++++++++++------------- 1 file changed, 16 insertions(+), 19 deletions(-) diff --git a/lib/src/layer/tile_layer/tile_layer.dart b/lib/src/layer/tile_layer/tile_layer.dart index 9675b9e73..e40a10806 100644 --- a/lib/src/layer/tile_layer/tile_layer.dart +++ b/lib/src/layer/tile_layer/tile_layer.dart @@ -242,7 +242,6 @@ class TileLayer extends StatefulWidget { this.wmsOptions, this.tileDisplay = const TileDisplay.fadeIn(), - /// See [RetinaMode] for more information /// /// Defaults to `false` when `null`. @@ -361,30 +360,28 @@ class _TileLayerState extends State with TickerProviderStateMixin { _loadAndPruneInVisibleBounds(MapCamera.of(context)); }); - - // Delay Timer for loadingDelay -Timer? _delayTimer; + Timer? _delayTimer; // This method is used to delay the execution of a function by the specified [loadingDelay]. // This is useful to prevent frequent reloading of tile layers in response to rapid, successive events (e.g., zooming or panning). -void _loadingDelay(void Function() action) { - //execute immediately if delay is 0. - if(widget.loadingDelay == 0) { - action(); - return; - } + void _loadingDelay(void Function() action) { + //execute immediately if delay is 0. + if (widget.loadingDelay == 0) { + action(); + return; + } - // Cancel the previous timer if it is still active - if(_delayTimer != null && _delayTimer!.isActive) { - _delayTimer!.cancel(); - } + // Cancel the previous timer if it is still active + if (_delayTimer != null && _delayTimer!.isActive) { + _delayTimer!.cancel(); + } - // Reset the timer to wait for the debounce duration - _delayTimer = Timer(Duration(milliseconds: widget.loadingDelay), () { - action(); - }); -} + // Reset the timer to wait for the debounce duration + _delayTimer = Timer(Duration(milliseconds: widget.loadingDelay), () { + action(); + }); + } // This is called on every map movement so we should avoid expensive logic // where possible. From c357cbf0c55be24d2b955428e6aceca61cf99315 Mon Sep 17 00:00:00 2001 From: Reinis Sprogis Date: Sat, 24 Feb 2024 15:01:12 +0200 Subject: [PATCH 4/6] Replaced loadingDuration Type int -> Duration, improved documentation. --- lib/src/layer/tile_layer/tile_layer.dart | 54 ++++++++++++------------ 1 file changed, 27 insertions(+), 27 deletions(-) diff --git a/lib/src/layer/tile_layer/tile_layer.dart b/lib/src/layer/tile_layer/tile_layer.dart index 9675b9e73..7a736d1bb 100644 --- a/lib/src/layer/tile_layer/tile_layer.dart +++ b/lib/src/layer/tile_layer/tile_layer.dart @@ -207,18 +207,18 @@ class TileLayer extends StatefulWidget { /// no affect. final TileUpdateTransformer tileUpdateTransformer; - /// Defines the minimum time delay (in milliseconds) before map layers are allowed to rebuild. + /// Defines the minimum delay time from last map event before the tile layers are updated. + /// + /// 16ms could be a good starting point for most applications. This at 60fps this will wait one frame after the last event. /// /// This delay acts as a debounce period to prevent frequent reloading of tile layers in response to rapid, successive events (e.g., zooming or panning). /// - /// By setting this delay, we ensure that map layer updates are performed only after a period of inactivity, enhancing performance and user experience. + /// By setting this delay, we ensure that map layer updates are performed only after a period of inactivity, enhancing performance and user experience on lower performance devices. /// /// - If multiple events occur within this delay period, only the last event triggers the tile layer update, reducing unnecessary processing and network requests. - /// - /// - This is particularly useful for events that are triggered often and rapidly, such as map movements or viewport changes. - /// - /// - The delay is measured from the last received event. Once the specified time has passed without any new events, the tile layers are updated to reflect the latest state. - final int loadingDelay; + /// + /// - If the [loadingDelay] is `null`, the tile layers will update as soon as possible. + final Duration? loadingDelay; /// Create a new [TileLayer] for the [FlutterMap] widget. TileLayer({ @@ -252,7 +252,7 @@ class TileLayer extends StatefulWidget { this.evictErrorTileStrategy = EvictErrorTileStrategy.none, this.reset, this.tileBounds, - this.loadingDelay = 50, + this.loadingDelay, TileUpdateTransformer? tileUpdateTransformer, String userAgentPackageName = 'unknown', }) : assert( @@ -363,28 +363,28 @@ class _TileLayerState extends State with TickerProviderStateMixin { -// Delay Timer for loadingDelay -Timer? _delayTimer; +/// Delay Timer for loadingDelay + Timer? _delayTimer; -// This method is used to delay the execution of a function by the specified [loadingDelay]. -// This is useful to prevent frequent reloading of tile layers in response to rapid, successive events (e.g., zooming or panning). -void _loadingDelay(void Function() action) { - //execute immediately if delay is 0. - if(widget.loadingDelay == 0) { - action(); - return; - } + /// This method is used to delay the execution of a function by the specified [loadingDelay]. + /// This is useful to prevent frequent reloading of tile layers in response to rapid, successive events (e.g., zooming or panning). + void _loadingDelay(void Function() action) { + //execute immediately if delay is not provided. + if (widget.loadingDelay == null) { + action(); + return; + } - // Cancel the previous timer if it is still active - if(_delayTimer != null && _delayTimer!.isActive) { - _delayTimer!.cancel(); - } + // Cancel the previous timer if it is still active + if (_delayTimer?.isActive ?? false) { + _delayTimer!.cancel(); + } - // Reset the timer to wait for the debounce duration - _delayTimer = Timer(Duration(milliseconds: widget.loadingDelay), () { - action(); - }); -} + // Reset the timer to wait for the debounce duration + _delayTimer = Timer(widget.loadingDelay!, () { + action(); + }); + } // This is called on every map movement so we should avoid expensive logic // where possible. From 9fbc9aaa5dd8b3549334cb9c40691c68da234e94 Mon Sep 17 00:00:00 2001 From: Joscha <34318751+josxha@users.noreply.github.com> Date: Tue, 27 Feb 2024 00:27:48 +0100 Subject: [PATCH 5/6] format --- lib/src/layer/tile_layer/tile_layer.dart | 58 ++++++++++++------------ 1 file changed, 30 insertions(+), 28 deletions(-) diff --git a/lib/src/layer/tile_layer/tile_layer.dart b/lib/src/layer/tile_layer/tile_layer.dart index 50af608bb..e8edeaccb 100644 --- a/lib/src/layer/tile_layer/tile_layer.dart +++ b/lib/src/layer/tile_layer/tile_layer.dart @@ -208,18 +208,24 @@ class TileLayer extends StatefulWidget { /// no affect. final TileUpdateTransformer tileUpdateTransformer; - /// Defines the minimum delay time from last map event before the tile layers are updated. - /// - /// 16ms could be a good starting point for most applications. This at 60fps this will wait one frame after the last event. + /// Defines the minimum delay time from last map event before the tile layers + /// are updated. This delay acts as a debounce period to prevent frequent + /// reloading of tile layers in response to rapid, successive events + /// (e.g., zooming or panning). /// - /// This delay acts as a debounce period to prevent frequent reloading of tile layers in response to rapid, successive events (e.g., zooming or panning). + /// 16ms could be a good starting point for most applications. + /// This at 60fps this will wait one frame after the last event. /// - /// By setting this delay, we ensure that map layer updates are performed only after a period of inactivity, enhancing performance and user experience on lower performance devices. + /// By setting this delay, we ensure that map layer updates are performed + /// only after a period of inactivity, enhancing performance and user + /// experience on lower performance devices. /// - /// - If multiple events occur within this delay period, only the last event triggers the tile layer update, reducing unnecessary processing and network requests. - /// - /// - If the [loadingDelay] is `null`, the tile layers will update as soon as possible. - final Duration? loadingDelay; + /// - If multiple events occur within this delay period, only the last event + /// triggers the tile layer update, reducing unnecessary processing and + /// network requests. + /// - If the [loadingDelay] is `null`, the tile layers will update as soon + /// as possible. + final Duration loadingDelay; /// Create a new [TileLayer] for the [FlutterMap] widget. TileLayer({ @@ -252,7 +258,7 @@ class TileLayer extends StatefulWidget { this.evictErrorTileStrategy = EvictErrorTileStrategy.none, this.reset, this.tileBounds, - this.loadingDelay, + this.loadingDelay = Duration.zero, TileUpdateTransformer? tileUpdateTransformer, String userAgentPackageName = 'unknown', }) : assert( @@ -347,6 +353,9 @@ class _TileLayerState extends State with TickerProviderStateMixin { TileRangeCalculator(tileSize: widget.tileSize); late TileScaleCalculator _tileScaleCalculator; + /// Delay Timer for [TileLayer.loadingDelay] + Timer? _delayTimer; + // We have to hold on to the mapController hashCode to determine whether we // need to reinitialize the listeners. didChangeDependencies is called on // every map movement and if we unsubscribe and resubscribe every time we @@ -361,29 +370,22 @@ class _TileLayerState extends State with TickerProviderStateMixin { _loadAndPruneInVisibleBounds(MapCamera.of(context)); }); - - -/// Delay Timer for loadingDelay - Timer? _delayTimer; - - /// This method is used to delay the execution of a function by the specified [loadingDelay]. - /// This is useful to prevent frequent reloading of tile layers in response to rapid, successive events (e.g., zooming or panning). - void _loadingDelay(void Function() action) { - //execute immediately if delay is not provided. - if (widget.loadingDelay == null) { + /// This method is used to delay the execution of a function by the specified + /// [TileLayer.loadingDelay]. This is useful to prevent frequent reloading + /// of tile layers in response to rapid, successive events (e.g., zooming + /// or panning). + void _loadingDelay(VoidCallback action) { + //execute immediately if delay is zero. + if (widget.loadingDelay == Duration.zero) { action(); return; - } - - // Cancel the previous timer if it is still active - if (_delayTimer?.isActive ?? false) { - _delayTimer!.cancel(); } + // Cancel the previous timer if it is still active. + _delayTimer?.cancel(); + // Reset the timer to wait for the debounce duration - _delayTimer = Timer(widget.loadingDelay!, () { - action(); - }); + _delayTimer = Timer(widget.loadingDelay, action); } // This is called on every map movement so we should avoid expensive logic From f5034d53309706dfbeef6e842bb7059a9343e1b2 Mon Sep 17 00:00:00 2001 From: Joscha <34318751+josxha@users.noreply.github.com> Date: Tue, 27 Feb 2024 00:32:15 +0100 Subject: [PATCH 6/6] update docs --- lib/src/layer/tile_layer/tile_layer.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/src/layer/tile_layer/tile_layer.dart b/lib/src/layer/tile_layer/tile_layer.dart index e8edeaccb..db93df67c 100644 --- a/lib/src/layer/tile_layer/tile_layer.dart +++ b/lib/src/layer/tile_layer/tile_layer.dart @@ -223,8 +223,8 @@ class TileLayer extends StatefulWidget { /// - If multiple events occur within this delay period, only the last event /// triggers the tile layer update, reducing unnecessary processing and /// network requests. - /// - If the [loadingDelay] is `null`, the tile layers will update as soon - /// as possible. + /// - If the [loadingDelay] is `Duration.zero`, the delay is completely + /// disabled and the tile layer will update as soon as possible. final Duration loadingDelay; /// Create a new [TileLayer] for the [FlutterMap] widget.