flutter_map icon indicating copy to clipboard operation
flutter_map copied to clipboard

The render tile process is slow when there is a large number of tiles

Open SalsabeelaHasan opened this issue 1 year ago • 10 comments

What is the bug?

I have a problem with Tails Render, I have 4 layers, two of them, and each one of them has 48 Tails. Totally, there are 100 tiles "approximately". The problem is that when I enter the map page, it often takes 8 to 15 seconds for all the tiles to appear, in addition when I move and zoom on the tiles, there is a lot of slowness as well.

What is the expected behaviour?

I Tried several solutions from this link, but without a good result. https://docs.fleaflet.dev/faqs/performance-issues I need the render process to be completed within a few seconds.

How can we reproduce this issue?

Add a large number of layers and tiles and make the plugin render them at the same time. in my case, there is 4 layers and two of them have 48 tiles.

Do you have a potential solution?

No response

Can you provide any other information?

  FlutterMap(
                key: ValueKey(MediaQuery.of(context).orientation),
                 mapController: _mapController,
                options: MapOptions(
                  center: LatLng((lastLat < -90 ? -90 : lastLat > 90 ? 90 : lastLat),
                      (lastLng < -180 ? -180 : lastLng > 180 ? 180 : lastLng)),
                  zoom: lastZoom,
                  maxZoom: 8.0,
                  rotationThreshold: 0,
                  rotationWinGestures: MultiFingerGesture.pinchZoom,
                  pinchZoomWinGestures: MultiFingerGesture.all,
                  enableMultiFingerGestureRace: true,
                  rotation: 0,
                ),
              //  layers: getLightningTiles(widget.activeLayerId),
                children: <Widget>[
                  //base
                  TileLayerWidget(
                    options: TileLayerOptions(
                      retinaMode: false,
                      //keepBuffer:0,
                      urlTemplate: '${widget.snapshot.data.tilesSource}',
                      tileProvider: CachedNetworkTileProvider(),
                    ),
                  ),
                  // example layers
                  for (var i = 0; i < widget.snapshot.data.layersData[widget.activeLayerId].length; i++)
                    TileLayerWidget(
                        options: TileLayerOptions(
                          urlTemplate: widget.snapshot.data.layersData[widget.activeLayerId][i]['tiles'],
                          backgroundColor: Colors.transparent,
                          opacity: i == currentFrame ? tilesOpacity : 0,
                          maxZoom: 8,
                          maxNativeZoom: 6,
                          retinaMode: false,
                          updateInterval:150,
                          tileProvider: NetworkTileProvider(),
                          errorImage: CachedNetworkImageProvider(
                            'https://cdn.pixabay.com/photo/2017/12/13/00/23/christmas-3015776_960_720.jpg',
                          ),
                        )),
                            //lightning 
                        for (var i = 0; i < widget.snapshot.data.layersData[widget.activeLayerId].length; i++)
                         TileLayerWidget( options: TileLayerOptions(
                           urlTemplate: widget.snapshot.data.layersData['lightning'][i]['tiles'],
                           backgroundColor: Colors.transparent,
                           opacity: i==currentFrame?1:0,
                           maxZoom: 8,
                           maxNativeZoom: 6,
                         )),
                ]),

Platforms Affected

Android, iOS

Severity

Obtrusive: Prevents normal functioning but causes no errors in the console

Frequency

Consistently: Always occurs at the same time and location

Requirements

  • [X] I agree to follow this project's Code of Conduct
  • [X] My Flutter/Dart installation is unaltered, and flutter doctor finds no relevant issues
  • [X] I am using the latest stable version of this package
  • [X] I have checked the FAQs section on the documentation website
  • [X] I have checked for similar issues which may be duplicates

SalsabeelaHasan avatar Aug 07 '22 14:08 SalsabeelaHasan

Hi there @SalsabeelaHasan,

We are not aware of this issue, so the Performance Issues page won't help you here.

It is very unusual to be displaying 48 tiles at once, on any number of layers. Are you sure this is the amount a layer is showing all at once? However, this shouldn't be an issue, even if you were.

Can you provide one of your templateURLs, if there isn't any sensitive information in it? I would like to double check it isn't a slow server. Also please test the URL in the example application, and progressively add layers, until it becomes slow.

Also, if you don't need to show all layers at once, please use the ternary operator directly on the layer, not on the opacity property. Even if opacity is 0, all tiles must still be rendered/drawn.

Many thanks for the report!

JaffaKetchup avatar Aug 07 '22 15:08 JaffaKetchup

Hello, @JaffaKetchup. Your response helped me thank you. We do a pre-render for this large number of tiles simultaneously when the user enters the page because the user can review and read the data for 4 or 5 hours from the past, and the total of this data is about 100 tiles, this is why we use opacity property. (see video) And I allow him, through options (the slider, the play button and next and back buttons), to move quickly between these data without interruption or delay. Are there options I can have by showing default tiles, while the rest of the tiles are getting ready for rendering? or gradually/progressively adding tiles?

https://user-images.githubusercontent.com/62775384/183399457-4ba6bad6-4492-44f9-871f-7038262549c0.mp4

SalsabeelaHasan avatar Aug 08 '22 10:08 SalsabeelaHasan

Hi again,

Rendering is probably not the issue here, it's probably a slow server. Unfortunately, there is no 'default loading tile' option.

We do a pre-render for this large number of tiles simultaneously

How do you do this? Shouldn't this fix the issue about slow rendering if they are already rendered.

I think I'm struggling to understand the situation. Is the app actually as slow as that screen recording looks, or is that just a poor recording? If the app is that slow, there's probably too much going on, how many images are you simultaneously prerendering?

JaffaKetchup avatar Aug 08 '22 11:08 JaffaKetchup

Hi @JaffaKetchup ! There is no problem with the server or slowness in the application, just a poor recording issue. What I mean by prerendering, is that we give all tiles (100 tiles) to the plugin to make a Render for them when the user enters to map page. Through the opacity property, we only show the first tile (opacity =1) and the rest are hidden (opacity =0), so when the user starts moving through the options I mentioned in the previous reply.. we hide the tile[0] and show Another tile[1], then hide tile[1] and show tile[2] .. and so on.. until show all tiles. do you know how the Flip Book is work? almost like it! see this video [ https://www.youtube.com/watch?v=JVzf9rtgf9Y ] So the movement will be as shown in the video.

SalsabeelaHasan avatar Aug 08 '22 11:08 SalsabeelaHasan

Opacity is a slow widget to render and might be the cause of your performance issues. However a solution to preload tiles (from the server) would be convenient. Joining this discussion as I have interest in this feature.

mootw avatar Aug 11 '22 17:08 mootw

See #1337.

JaffaKetchup avatar Aug 11 '22 18:08 JaffaKetchup

Thank you @MooNag @JaffaKetchup.

SalsabeelaHasan avatar Aug 13 '22 17:08 SalsabeelaHasan

I don't really understand the need for opacity here at all (I'm assuming some of the tiles have partial transparent backgrounds, I may well be misunderstanding here). Just don't include the TileLayerWidgets that aren't selected at first ? That way you know the visible tiles are loaded first and quickest, then add them when required and setState ?

If you then want to try and preload some of the non visible tiles when selected later, maybe you do do some hackery like using a setTimeout with staggered times, loading a separate fluttermap widget with the other tile layer in an Offstage widget (so it doesn't display unless selected, but the tile url calls are made, so possibly caching the images?).

No idea if this would work, just thinking out loud....

pseudocode

Stack(children: [Offstage(child: FlutterMapWithVisibleTiles, offstage: false]

then after 0.1 second it becomes

Stack(children: [
Offstage(child: FlutterMapWithVisibleTileLayers, offstage: false,
Offstage(child: FlutterMapWithNonVisibleTilesLayer2, offstage: true]
])

then after 0.2 seconds add another non visible Offstage layer to the List.

And then when doing the flipBook thing, just alter the layers?

Also, for the lightning example, I'd be tempted to use Markers rather than tiles if you know their positions (whereas maybe tiles for the clouds...I'm not clear if these are all one tile that has clouds and lightning, or separate tiles for clouds, and separate ones for lightning or whatever).

Anyway, just some food for thought, that may be off target! Apologies if so.

ibrierley avatar Aug 13 '22 20:08 ibrierley

Sorry for the long post, figured I would share my experiences, since I've been working on rendering for the past week or so.

I don't really understand the need for opacity here at all (I'm assuming some of the tiles have partial transparent backgrounds, I may well be misunderstanding here).

My users can adjust the opacity of all layers on the map in the UI.

Opacity is a slow widget to render and might be the cause of your performance issues. However a solution to preload tiles (from the server) would be convenient. Joining this discussion as I have interest in this feature.

Yes, I've also noticed that there might be some room for rendering performance improvements, but I think this topic is different from much of the discussion in it. Preloading tiles has to do with network latency, not rendering performance.

Since I have several heavy and dynamically updating layers I've been going over best practises trying to optimize the rendering performance of my app (starting point here). My layers were already forked to begin with (except the tile layer), mainly because I work in the local projection above the layers as well, so it's been pretty simple for me to tinker on my own. I've improved the performance of my app quite significantly, mostly due to sloppy assumptions on my own part. Only one assumption caused a big performance impact, but many small sloppy decisions combined also contributed noticeably to a decline in performance.

  • For the TileLayer we are using the Opacity widget in addition to animating the opacity during fade-in. For the OverlayImageLayer we are using the recommended/performant way of adjusting image opacity (by setting color and BlendMode.modulate on the Image).

From the Opacity widget docs:

For values of opacity other than 0.0 and 1.0, this class is relatively expensive because it requires painting the child into an intermediate buffer.

If it's feasible, we would ideally have an opacity parameter exposed on each AnimatedTile. A fairly simple way of achieving this might be to use a FadeInImage rather than a RawImage and pass an Image with modulated blend mode and color set from the given opacity. This could achieve both shader-accelerated animation and configurability of opacity post-animation, but I haven't tried it out yet. I will try this out and submit a PR if it works out OK.

  • A recurring pattern I've seen is to use a LayoutBuilder, passing the size into the painter and then clipping the canvas using something like canvas.clipRect(Offset.zero & size).

In some cases, this is very inefficient, since the expensive operation is typically the rasterization, and the clipping is applied to the raster post-rendering. For closed geometries like very large circles, for example in a polar grid, my guess is that this results in huge rasters in the render buffer pre-clipping. Whatever happens during rendering, it's completely crippling the app even on a performant machine, at least on Metal.

What we want to do is to clip the vector before it is rasterized, this will yield the same memory savings as clipping the canvas (even more in fact, since we don't need to temporarily buffer unclipped rasters), but also save us the trouble of rendering things that will later be clipped away. This is why, for example, using ClipRRect solely to produce rounded corners in widgets is discouraged. Drawing a rounded border using a decoration/vector is faster than drawing a square border, rasterizing it and then clipping it (even more so in the case of antialiased clipping).

It's a bit of a hack for the lack of a better solution, but in my case I used ui.Path with its simple boolean vector operations (doesn't seem to support more than one subpath). This way I'm clipping the vector with a buffer of, say, +- the stroke width and then some around the viewport, using a rect path. With the buffer, a boundary of the "clipping" rect will be painted just outside the viewport at virtually no cost, but this can of course also be removed using canvas.clipRect if GPU memory is of great concern. Note that this efficient form of clipping, or rather this hack, only works for closed geometries, not lines for example. More generally speaking, there are efficient algorithms for boolean operations on vectors that are much more generic than those provided by Dart/Flutter, but for some reason no one has ported them to Dart yet.

Creating ui.Path objects can be a little expensive on the UI thread if they are created in the tens of thousands, but in my case I'm only creating a handful, and the cost of not having the vector clipping is blowing my frame budget many times over at higher zoom levels.

  • When one layer rebuilds, my other layers do not. They do repaint however, so I'm using RepaintBoundary to instruct Flutter/Skia to cache the rasterized (heavy) layers in the GPU.

This means that I only repaint the layer that updated on the map, plus the cost of compositing. It might not be as plausible for mobile if the GPU memory is tight, but I'm only targeting mid-end laptops, high-end tablets and above, and the raster caches have a hit rate of about 99.8%.

  • I haven't looked into it yet, but for larger images which are quite slow to load we might be able to benefit from ui.ImmutableBuffer.fromUint8List in Flutter 3.3.0+.

This would still entail copying the compressed Uint8List once, it might not be worth it unless it's significantly faster: https://medium.com/flutter/whats-new-in-flutter-3-3-893c7b9af1ff https://docs.flutter.dev/release/breaking-changes/image-provider-load-buffer

I'm still having issues with scene display lag on Metal in some edge cases, lots of time spent compositing. Open to input if any of you have run into it, doesn't seem to apply on other platforms. There is a slight chance it might be related to https://github.com/flutter/flutter/issues/104638.

JosefWN avatar Sep 03 '22 00:09 JosefWN

I feel like there's 2 separate issues mingled in there, one for tiles and rasters, and one for vectors ? I may have some ideas on the vector side (especially if you can do a minimal example), but want to keep that separate a separate discussion if you want to raise a separate ticket.

ibrierley avatar Sep 03 '22 05:09 ibrierley

Hi,

I would like to post some of my own experience here with displaying a lot of Tiles. My app will typically display 48 tiles per view port, and has animation of about 2-5 frames per second. So about 250 tiles per second.

I use a custom image provider that caches image data (will post code below). I have found that even on high end devices, the image decoding process is a huge bottleneck when skimming or animating the timeline (I am ignoring network stuff for now). So I would make sure that you have hashCode implemented. It can be as simple as this:

@override
  int get hashCode => url.hashCode;

This allows flutter to cache the image decode and it works really well. It is important that whatever image provider you use, it needs to be efficient loading from memory/disk/etc. Otherwise you will get lag regardless. This also might mean that you need to improve the speed of your tile server. Loading and skimming that many tiles from network or disk only is probably not going to be fast enough. You will want to cache in memory for performant skimming.

I don't believe that flutter map has any significant impact on the rendering of raster tiles that can't be avoided at the moment (using flutter_map: 3.0.0, flutter: 3.4).

However to get a smooth playback, a fast tile server is almost a requirement.

For preloading the animation, I use the Offstage widget to load the next 3 frames when the current one is visible. This works well and has minimal performance impact.

My raster image provider for example

SimpleCache networkImageCache =
    SimpleCache(database_id: 'network_image_cache', evictExpired: true);

Duration _defaultCacheDuration = const Duration(days: 2);

/// It will download the image from the url once, save it to cache
class CachedNetworkImageProvider
    extends ImageProvider<CachedNetworkImageProvider> {
  const CachedNetworkImageProvider({
    required this.url,
    required this.httpClient,
    this.debug = false,
  });

  final Uri url;
  final bool debug;
  final Client httpClient;

  @override
  Future<CachedNetworkImageProvider> obtainKey(
      ImageConfiguration configuration) {
    return SynchronousFuture<CachedNetworkImageProvider>(this);
  }

  @override
  ImageStreamCompleter loadBuffer(
      CachedNetworkImageProvider key, DecoderBufferCallback decode) {
    return OneFrameImageStreamCompleter(_load(key, decode),
        informationCollector: () sync* {
      yield ErrorDescription('Image provider: $this');
      yield ErrorDescription('Image key: $key');
    });
  }

  Future<ImageInfo> _load(
    CachedNetworkImageProvider key,
    DecoderBufferCallback decode,
  ) async {
    assert(key == this);

    Uint8List? bytes;

    //Calculate the url path md5 hash. This is fast and is unlikely
    //to have a collision which can cause issues.
    final String file = md5.convert(url.toString().codeUnits).toString();
    //File we want to get.

    final object = await networkImageCache.getObject(id: file);

    // Reads from the local file.
    if (object != null &&
        object.isExpired == false &&
        object.age < _defaultCacheDuration) {
      //Return cached data!
      bytes = object.bytes;
    } else {
      // Reads from the network and saves it to the local file.
      Response? response = await httpClient.get(url);

      if (response.statusCode != HttpStatus.ok) {
        throw NetworkImageLoadException(
            statusCode: response.statusCode, uri: url);
      }

      bytes = response.bodyBytes;
      if (bytes.lengthInBytes == 0) {
        throw Exception('NetworkImage is an empty file: $url');
      }

      //Uses provided cache control or default duration of 7 days
      await networkImageCache.writeObjectBytes(
          id: file,
          data: bytes,
          ttl: getCacheControl(response) ?? _defaultCacheDuration.inSeconds);
    }

    final codec = await decode(await ImmutableBuffer.fromUint8List(bytes));
    final image = (await codec.getNextFrame()).image;

    return ImageInfo(image: image);
  }

  @override
  bool operator ==(Object other) {
    if (other.runtimeType != runtimeType) return false;
    return other is CachedNetworkImageProvider && other.url == url;
  }

  @override
  int get hashCode => url.hashCode;

  @override
  String toString() => '$runtimeType("$url")';
}

mootw avatar Oct 03 '22 20:10 mootw

This issue is stale because it has been open 30 days with no activity. Remove stale label or comment or this will be closed in 5 days.

github-actions[bot] avatar Nov 03 '22 02:11 github-actions[bot]

@MooNag cool! How did the caching in RAM affect overall memory consumption? Did you notice an improvement with the new load buffers?

Increasing your tile size might help reduce the number of requests (per-request overhead), especially on retina displays I think the recommended tile size is 512x512px rather than 256x256, at least according to Mapbox.

JosefWN avatar Nov 04 '22 07:11 JosefWN

This issue is stale because it has been open 30 days with no activity. Remove stale label or comment or this will be closed in 5 days.

github-actions[bot] avatar Dec 05 '22 02:12 github-actions[bot]

This issue was closed because it has been stalled for 5 days with no activity.

github-actions[bot] avatar Dec 11 '22 02:12 github-actions[bot]

This issue is stale because it has been open 30 days with no activity. Remove stale label or comment or this will be closed in 5 days.

github-actions[bot] avatar Jan 11 '23 02:01 github-actions[bot]

This issue was closed because it has been stalled for 5 days with no activity.

github-actions[bot] avatar Jan 17 '23 01:01 github-actions[bot]