Skip to content

[google_maps_flutter_web] API performance bottleneck in custom tile overlays #116132

Open

Description

@AsturaPhoenix

Custom tile overlays on web aren't supported yet, but there may be a significant performance bottleneck in the API where tile content is expected to be in encoded bytes (Tile.data). If tile preprocessing is required on the Flutter app side, this incurs an expensive Image.toByteData call, which blocks the UI thread. On Android, we hit this bottleneck as well but it's less of an issue as it seems to happen off the UI thread, so the most that happens is tiles take longer to load than ideal.

To some degree, this can be mitigated by caching in the web tile overlay implementation, as my implementation does not cache on the web side yet. However, there would still be jank in the cache miss case, which would happen pretty often.

Ideally, I'd like it if the web implementation could insert flutter widgets as tiles so that we could avoid the extra encoding/decoding cycle. However, though I haven't looked too deeply into the Android/iOS APIs, I suspect that might be problematic there. Perhaps a middle ground could be to accept either raw bytes or Image objects and convert on the plugin side. I might try prototyping that on my fork. Please let me know whether such an API change is a direction you'd be open to. A related use case is placeholder images while tiles are loading from a network fetch, which is probably not possible with the current formulation (I'm using a styled base map for this purpose even though my previous web implementation was compositing from available nearby mipmaps).

I might also look at ways to offload the tile processing to a web worker (e.g. isolated_worker?), though I do recall possibly seeing that canvas operations needed to happen on the UI thread (and as with the Android case, while that could remove the jank, tiles would still take longer to load than ideal).

Also in the meantime, using image to encode as a bmp significantly improves performance over the stock png due to cheaper encoding, but it's still far from smooth. (trip_planner_aquamarine/819e9c)

Related: #98596

Details

Live example:

  1. http://34.83.198.158/trip_planner_aquamarine_slow
  2. Zoom into/out of the map and scroll around
  3. Observe frame drops during associated animations
Reduced example
chromedriver --port=4444 &
flutter drive -d chrome --driver test_driver/integration_test.dart --target integration_test/tile_overlay_performance_test.dart --profile

With the custom overlay commented out, observe a frame ratio of ~1 (no dropped frames). With the custom overlay, observe a significantly reduced frame ratio (~.25 on my system, 75% dropped frames).

Dependencies:

dependencies:
  ...
  equatable: ^2.0.5
  google_maps_flutter: ^2.2.1
  google_maps_flutter_web: ^0.4.0+3
  http: ^0.13.5
  image: ^3.2.2

dependency_overrides:
  google_maps_flutter_web:
    path: ../flutter/plugins/packages/google_maps_flutter/google_maps_flutter_web

Dependency overrides:
https://github.com/AsturaPhoenix/flutter-plugins/tree/dev (be sure to clone the dev branch)

integration_test/tile_overlay_performance_test.dart

import 'dart:async';
// ignore: avoid_web_libraries_in_flutter
import 'dart:html';
import 'dart:math';
import 'dart:typed_data';
import 'dart:ui' as ui;

import 'package:equatable/equatable.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:google_maps_flutter/google_maps_flutter.dart';
import 'package:image/image.dart' as img;
import 'package:integration_test/integration_test.dart';
import 'package:http/http.dart' as http;

// import 'package:trip_planner_aquamarine/persistence/blob_cache.dart';
// import 'package:trip_planner_aquamarine/providers/wms_tile_provider.dart';

class FakeWmsClient extends Fake implements http.Client {
  final random = Random();

  int tilesGenerated = 0;

  Future<ui.Image> generateTile(Uri url) async {
    ++tilesGenerated;

    final width = int.parse(url.queryParameters['width']!);
    final height = int.parse(url.queryParameters['height']!);
    final buffer = await ui.ImmutableBuffer.fromUint8List(
      Uint8List.fromList([
        for (int y = 0; y < height; ++y)
          for (int x = 0; x < width; ++x) ...[
            random.nextInt(0x100),
            random.nextInt(0x100),
            random.nextInt(0x100),
            0xFF
          ]
      ]),
    );
    final imageDescriptor = ui.ImageDescriptor.raw(
      buffer,
      width: width,
      height: height,
      pixelFormat: ui.PixelFormat.rgba8888,
    );

    try {
      final codec = await imageDescriptor.instantiateCodec();
      try {
        return (await codec.getNextFrame()).image;
      } finally {
        codec.dispose();
      }
    } finally {
      imageDescriptor.dispose();
    }
  }

  @override
  Future<http.Response> get(Uri url, {Map<String, String>? headers}) =>
      generateTile(url)
          .then((image) => image.toByteData(format: ui.ImageByteFormat.png))
          .then(
            (data) =>
                http.Response.bytes(data!.buffer.asUint8List(), HttpStatus.ok),
          );

  @override
  void close() {}
}

class FakeBlobCache extends Fake implements BlobCache {
  final data = <String, Uint8List>{};

  @override
  Uint8List? operator [](String key) => data[key];
  @override
  void operator []=(String key, Uint8List value) => data[key] = value;
  @override
  void close() {}
}

Future<void> watchTiles(int minCount) {
  final completer = Completer<void>();
  final watched = <ImageElement>{}, loading = <ImageElement>{};

  void awaitLoad(ImageElement img) {
    loading.add(img);
    img.decode().whenComplete(() {
      loading.remove(img);

      if (watched.length >= minCount && loading.isEmpty) {
        completer.complete();
      }
    });
  }

  void scan() {
    watched.removeWhere((img) => !img.isConnected!);
    loading.removeWhere((img) => !img.isConnected!);

    for (final ImageElement img
        in document.querySelectorAll('img[src^="blob:"]')) {
      if (watched.add(img)) {
        awaitLoad(img);
      }
    }
  }

  final observer = MutationObserver((_, __) => scan())
    ..observe(
      document,
      childList: true,
      subtree: true,
      attributes: true,
      attributeFilter: const ['src'],
    );

  scan();

  return completer.future.whenComplete(() => observer.disconnect());
}

/// Uses timeouts to probe whether web animations are happening smoothly.
/// (Integration test timelines don't work on web because they need isolate
/// support.)
class WebPerformanceMonitor {
  WebPerformanceMonitor({required this.fps}) : t0 = DateTime.now() {
    timer = Timer.periodic(
      const Duration(seconds: 1) * (1 / fps),
      (_) => ++_frames,
    );
  }
  final DateTime t0;
  final num fps;
  late final Timer timer;
  int _frames = 0;
  // The ratio of frames recorded : frames expected.
  double get frameRatio =>
      _frames * 1000 / (fps * DateTime.now().difference(t0).inMilliseconds);

  void close() => timer.cancel();
}

///  ```
/// chromedriver --port=4444 &
/// flutter drive -d chrome --driver test_driver/integration_test.dart --target integration_test/tile_overlay_performance_test.dart --profile
/// ```
void main() {
  IntegrationTestWidgetsFlutterBinding.ensureInitialized();

  testWidgets('Tile overlay performance test', (tester) async {
    final mapControllerCompleter = Completer<GoogleMapController>();
    final onCameraIdleController = StreamController<void>.broadcast();

    final wmsClient = FakeWmsClient();

    await tester.pumpWidget(
      MaterialApp(
        home: Scaffold(
          body: Center(
            child: SizedBox.fromSize(
              size: const Size.square(0x300),
              child: GoogleMap(
                initialCameraPosition: const CameraPosition(
                  target: LatLng(37.8331, -122.4165),
                  zoom: 12,
                ),
                tileOverlays: {
                  TileOverlay(
                    tileOverlayId: const TileOverlayId('test'),
                    tileProvider: WmsTileProvider(
                      client: wmsClient,
                      cache: FakeBlobCache(),
                      tileType: 'test',
                      url: Uri(path: 'test'),
                      fetchLod: 2,
                      levelOfDetail: 14,
                      params: WmsParams(),
                    ),
                  )
                },
                onCameraIdle: () => onCameraIdleController.add(null),
                onMapCreated: mapControllerCompleter.complete,
              ),
            ),
          ),
        ),
      ),
    );

    // This is needed to kick-off the rendering of the JS Map flutter widget
    await tester.pump();
    final controller = await mapControllerCompleter.future;
    final onCameraIdle = onCameraIdleController.stream;

    await watchTiles(9);
    // Zoom in from 12 with target LOD 14 and max oversample 2 should not
    // require new tiles
    final tilesGenerated = wmsClient.tilesGenerated;

    final perf = WebPerformanceMonitor(fps: 60);
    const iterations = 16;
    for (int i = 0; i < iterations; ++i) {
      controller.animateCamera(CameraUpdate.zoomIn());
      await onCameraIdle.first;
      await watchTiles(9);

      controller.animateCamera(CameraUpdate.zoomOut());
      await onCameraIdle.first;
      await watchTiles(9);
    }

    tester.printToConsole('Frame ratio: ${perf.frameRatio}');
    expect(perf.frameRatio, greaterThan(.24));
    assert(wmsClient.tilesGenerated == tilesGenerated);
  });
}

// blob_cache.dart

abstract class BlobCache {
  void close();
  Uint8List? operator [](String key);
  void operator []=(String key, Uint8List value);
}

// wms_tile_provider.dart

class WmsParams {
  // unused
}

extension on Point<int> {
  ui.Offset toOffset() => ui.Offset(x.toDouble(), y.toDouble());
  Point<int> operator <<(int z) => Point(x << z, y << z);
  // On web, >> is limited to int32.
  // https://github.com/dart-lang/sdk/issues/15361
  // This is fine since the max zoom level for maps is 22.
  Point<int> operator >>(int z) =>
      Point((x >> z).toSigned(32), (y >> z).toSigned(32));
}

extension on ui.Image {
  ui.Size get size => ui.Size(width.toDouble(), height.toDouble());
}

class AreaLocator extends Equatable {
  const AreaLocator(this.zoom, this.coordinate);

  final int zoom;
  final Point<int> coordinate;

  @override
  get props => [zoom, coordinate];

  /// Derives a descendant [AreaLocator] in the quadtree. Descendants have
  /// coordinates in the top left.
  ///
  /// The `>>` symbol is chosen to represent shifting rightwards towards
  /// children in a left-to-right hierarchy representation.
  AreaLocator operator >>(int z) => AreaLocator(zoom + z, coordinate << z);

  /// Derives an ancestor of [AreaLocator] in the quadtree.
  ///
  /// The `<<` symbol is chosen to represent shifting leftwards towards
  /// ancestors in a left-to-right hierarchy representation.
  AreaLocator operator <<(int z) => AreaLocator(zoom - z, coordinate >> z);

  AreaLocator operator +(Point<int> offset) =>
      AreaLocator(zoom, coordinate + offset);

  TileLocator withLod(int lod) => TileLocator(zoom, coordinate, lod);
}

class TileLocator extends AreaLocator {
  /// The LOD delta from [zoom]. The effective zoom level of the referenced
  /// tiles is `zoom + lod`.
  final int lod;

  @override
  get props => [...super.props, lod];

  const TileLocator(super.zoom, super.coordinate, this.lod);
  @override
  String toString() => 'zoom: $zoom / coordinate: $coordinate / lod: $lod';

  @override
  TileLocator operator >>(int z) => (super >> z).withLod(lod - z);

  @override
  TileLocator operator <<(int z) => (super << z).withLod(lod + z);

  @override
  TileLocator operator +(Point<int> offset) => (super + offset).withLod(lod);
}

class TileKey extends TileLocator {
  const TileKey(this.type, super.zoom, super.coordinate, super.lod);
  TileKey.forLocator(TileLocator locator, String type)
      : this(type, locator.zoom, locator.coordinate, locator.lod);
  final String type;

  @override
  get props => [type, ...super.props];

  @override
  String toString() => '$type@$zoom:(${coordinate.x},${coordinate.y})+$lod';
}

// Derived from js-ogc
class WmsTileProvider implements TileProvider {
  /// This is determined by the Maps API, but doesn't seem to be a constant
  /// provided there.
  static const logicalTileSize = 256;

  static const _defaultWmsParams = {
    'request': 'GetMap',
    'service': 'WMS',
    'srs': 'EPSG:3857',
  };

  static const _epsg3857Extent = 20037508.34789244;
  static const _origin = ui.Offset(
    -_epsg3857Extent, // x starts from left
    _epsg3857Extent,
  ); // y starts from top

  static final _imagePaint = ui.Paint();

  static Future<ui.Image> decodeImage(Uint8List data) async {
    final buffer = await ui.ImmutableBuffer.fromUint8List(data);
    final ui.ImageDescriptor imageDescriptor;
    try {
      imageDescriptor = await ui.ImageDescriptor.encoded(buffer);
    } finally {
      buffer.dispose();
    }

    try {
      final codec = await imageDescriptor.instantiateCodec();
      try {
        return (await codec.getNextFrame()).image;
      } finally {
        codec.dispose();
      }
    } finally {
      imageDescriptor.dispose();
    }
  }

  /// Convert xyz tile coordinates to mercator bounds.
  static ui.Rect _xyzToBounds(AreaLocator xyz) {
    final wmsTileSize = _epsg3857Extent * 2 / (1 << xyz.zoom);
    return _origin + xyz.coordinate.toOffset().scale(1, -1) * wmsTileSize &
        ui.Size(wmsTileSize, -wmsTileSize);
  }

  WmsTileProvider({
    required this.client,
    required this.cache,
    required this.tileType,
    required this.url,
    this.levelOfDetail = 0,
    this.maxOversample = 2,
    this.fetchLod = 0,
    this.preferredTileSize = 256,
    required this.params,
  });

  http.Client client;
  BlobCache cache;
  String tileType;

  /// The base URL supplying tile data. For simplicity, any query paremeters
  /// also specified in [WmsParams] or [DEFAULT_WMS_PARAMS] are overwritten.
  Uri url;

  int levelOfDetail;
  int maxOversample;

  /// The level of detail above natural at which to fetch and cache. Higher
  /// values will fetch larger tiles from the server, by powers of 2, and window
  /// them into the expected logical tile size of 256 x 256. The web API allows
  /// for custom logical tile sizes, but mobile does not.
  final int fetchLod;

  /// The tile standard tile size multiplied by the device pixel ratio.
  int preferredTileSize;
  WmsParams params;

  void dispose() {
    cache.close();
    client.close();
  }

  Uri _getTileUrl(TileLocator locator) {
    final bbox = _xyzToBounds(locator);
    final tileSizeParam = (logicalTileSize << locator.lod).toString();

    return url.replace(
      queryParameters: {
        ..._defaultWmsParams,
        ...url.queryParametersAll,
        // ...params.toJson().map((key, value) => MapEntry(key, value.toString())),
        'bbox': '${bbox.left},${bbox.bottom},${bbox.right},${bbox.top}',
        'width': tileSizeParam,
        'height': tileSizeParam,
      },
    );
  }

  Future<Uint8List> fetchImageData(TileLocator locator) async {
    final url = _getTileUrl(locator);

    final response = await client.get(url);
    if (response.statusCode == HttpStatus.ok) {
      return response.bodyBytes;
    } else {
      throw response;
    }
  }

  final _activeDecodes = <TileLocator, Future<ui.Image>>{};

  Future<Uint8List> getImageData(TileLocator locator) async =>
      cache[TileKey.forLocator(locator, tileType).toString()] ??=
          await fetchImageData(locator);

  Future<ui.Image> getImage(TileLocator locator) => _activeDecodes[locator] ??=
          getImageData(locator).then(decodeImage).whenComplete(() {
        // Caution: This can't be a => because we must discard the return value
        // or else the async becomes circular and never completes.
        _activeDecodes.remove(locator);
      });

  /// Windows a tile from a larger tile at a higher LOD.
  Future<void> _tileFromLarger(ui.Canvas canvas, TileLocator locator) async {
    final levelsAbove = min(fetchLod - locator.lod, locator.zoom);
    final ancestor = locator << levelsAbove;
    final image = await getImage(ancestor);

    final size = image.size / (1 << levelsAbove).toDouble();
    final offset = (locator.coordinate - (ancestor.coordinate << levelsAbove))
        .toOffset()
        .scale(size.width, size.height);

    canvas.drawImageRect(
      image,
      offset & size,
      ui.Offset.zero & const ui.Size.square(1),
      _imagePaint,
    );
  }

  /// Assembles a tile from smaller tiles at a lower LOD.
  Future<void> _tileFromSmaller(ui.Canvas canvas, TileLocator locator) async {
    final levelsBelow = locator.lod - fetchLod;
    final descendantBasis = locator >> levelsBelow;
    final sideLength = 1 << levelsBelow;

    canvas
      ..save()
      ..scale(1 / sideLength);

    for (int j = 0; j < sideLength; ++j) {
      for (int i = 0; i < sideLength; ++i) {
        final offset = Point(i, j);
        final descendantLocator = descendantBasis + offset;

        final image = await getImage(descendantLocator);
        canvas.drawImageRect(
          image,
          ui.Offset.zero & image.size,
          offset.toOffset() & const ui.Size.square(1),
          _imagePaint,
        );
      }
    }

    canvas.restore();
  }

  /// Gets a tile image, composited from cached or fetched resources.
  ///
  /// The caller is responsible for calling `dispose` on the image.
  Future<ui.Image> getTileContent(TileLocator locator) async {
    final pictureRecorder = ui.PictureRecorder();
    final canvas = ui.Canvas(pictureRecorder);

    // Keep the natural resolution afforded by the LOD if we want to display at
    // a higher resolution, but don't waste bytes beyond the preferred size to
    // minimize encoding time.
    //
    // Also go ahead and normalize the tile size to 1.
    final tileSize = min(logicalTileSize << locator.lod, preferredTileSize);
    canvas.scale(tileSize.toDouble());

    if (locator.lod <= fetchLod) {
      await _tileFromLarger(canvas, locator);
    } else {
      await _tileFromSmaller(canvas, locator);
    }

    final picture = pictureRecorder.endRecording();
    try {
      return picture.toImage(tileSize, tileSize);
    } finally {
      picture.dispose();
    }
  }

  final _encoder = img.BmpEncoder();

  @override
  Future<Tile> getTile(int x, int y, int? zoom) async {
    final locator = TileLocator(
      zoom!,
      Point(x, y),
      max(min(levelOfDetail - zoom, maxOversample), 0),
    );
    final content = await getTileContent(locator);
    try {
      final data = await content.toByteData(
        format: ui.ImageByteFormat.rawStraightRgba,
      );
      final image = img.Image.fromBytes(
        content.width,
        content.height,
        data!.buffer.asUint8List(),
      );
      return Tile(
        image.width,
        image.height,
        Uint8List.fromList(_encoder.encodeImage(image)),
      );
    } finally {
      content.dispose();
    }
  }
}

test_driver/integration_test.dart

// https://docs.flutter.dev/testing/integration-tests#running-in-a-browser

import 'package:integration_test/integration_test_driver.dart';

Future<void> main() => integrationDriver();

`web/index.html'

<!DOCTYPE html>
<html>
<head>
  <base href="$FLUTTER_BASE_HREF">

  <meta charset="UTF-8">
  <meta content="IE=Edge" http-equiv="X-UA-Compatible">
  <meta name="description" content="Repro">

  <!-- iOS meta tags & icons -->
  <meta name="apple-mobile-web-app-capable" content="yes">
  <meta name="apple-mobile-web-app-status-bar-style" content="black">
  <meta name="apple-mobile-web-app-title" content="repro">
  <link rel="apple-touch-icon" href="icons/Icon-192.png">

  <!-- Favicon -->
  <link rel="icon" type="image/png" href="favicon.png"/>

  <title>Repro</title>
  <link rel="manifest" href="manifest.json">

  <script>
    // The value below is injected by flutter build, do not touch.
    var serviceWorkerVersion = null;
  </script>
  <!-- This script adds the flutter initialization JS code -->
  <script src="flutter.js" defer></script>
  <script src="https://maps.googleapis.com/maps/api/js?key="></script>
</head>
<body>
  <script>
    window.addEventListener('load', function(ev) {
      // Download main.dart.js
      _flutter.loader.loadEntrypoint({
        serviceWorker: {
          serviceWorkerVersion: serviceWorkerVersion,
        }
      }).then(function(engineInitializer) {
        return engineInitializer.initializeEngine();
      }).then(function(appRunner) {
        return appRunner.runApp();
      });
    });
  </script>
</body>
</html>

image

Profile-20221128T110059.json.txt

Target Platform: Web
Target OS version/browser: Chrome
Devices: LG gram / x64 / Windows 11 Home

Logs

Logs
Analyzing trip_planner_aquamarine...                                    
No issues found! (ran in 42.0s)
[√] Flutter (Channel master, 3.6.0-1.0.pre.3, on Microsoft Windows [Version 10.0.22621.674], locale en-US)
    • Flutter version 3.6.0-1.0.pre.3 on channel master at C:\Users\imagi\flutter
    • Upstream repository https://github.com/flutter/flutter.git
    • Framework revision 8e5439c89e (3 weeks ago), 2022-11-09 14:33:45 -0500
    • Engine revision c76035429c
    • Dart version 2.19.0 (build 2.19.0-377.0.dev)
    • DevTools version 2.19.0

[√] Windows Version (Installed version of Windows is version 10 or higher)

[√] Android toolchain - develop for Android devices (Android SDK version 33.0.0)
    • Android SDK at C:\Users\imagi\AppData\Local\Android\sdk
    • Platform android-33, build-tools 33.0.0
    • Java binary at: C:\Program Files\Android\Android Studio\jre\bin\java
    • Java version OpenJDK Runtime Environment (build 11.0.13+0-b1751.21-8125866)
    • All Android licenses accepted.

[√] Chrome - develop for the web
    • Chrome at C:\Program Files\Google\Chrome\Application\chrome.exe

[!] Visual Studio - develop for Windows (Visual Studio Community 2019 16.11.16)
    • Visual Studio at C:\Program Files (x86)\Microsoft Visual Studio\2019\Community
    • Visual Studio Community 2019 version 16.11.32602.291
    • Windows 10 SDK version 10.0.16299.0
    X Visual Studio is missing necessary components. Please re-run the Visual Studio installer for the "Desktop development with C++" workload, and include these components:
        MSVC v142 - VS 2019 C++ x64/x86 build tools
         - If there are multiple build tool versions available, install the latest
        C++ CMake tools for Windows
        Windows 10 SDK

[√] Android Studio (version 2021.3)
    • Android Studio at C:\Program Files\Android\Android Studio
    • Flutter plugin can be installed from:
       https://plugins.jetbrains.com/plugin/9212-flutter
    • Dart plugin can be installed from:
       https://plugins.jetbrains.com/plugin/6351-dart
    • Java version OpenJDK Runtime Environment (build 11.0.13+0-b1751.21-8125866)

[√] VS Code (version 1.73.1)
    • VS Code at C:\Users\imagi\AppData\Local\Programs\Microsoft VS Code
    • Flutter extension version 3.52.0

[√] Connected device (3 available)
    • Windows (desktop) • windows • windows-x64    • Microsoft Windows [Version 10.0.22621.674]
    • Chrome (web)      • chrome  • web-javascript • Google Chrome 107.0.5304.107
    • Edge (web)        • edge    • web-javascript • Microsoft Edge 107.0.1418.56

[√] HTTP Host Availability
    • All required HTTP hosts are available

! Doctor found issues in 1 category.

Activity

  1. AsturaPhoenix commented on Nov 29, 2022

    @AsturaPhoenix
    Author

    It looks like my example leaks image handles. After correcting this, the performance test frame ratio jumps from ~.25 to ~.45. Not quite jank-free, but a substantial difference.

  2. AsturaPhoenix commented on Nov 29, 2022

    @AsturaPhoenix
    Author

    I've hacked together a proof-of-concept that demonstrates the performance gain afforded by skipping the re-encoding step.

    With hack: http://34.83.198.158/trip_planner_aquamarine
    Without: http://34.83.198.158/trip_planner_aquamarine_slow

    Hack commits:
    Client: AsturaPhoenix/trip_planner_aquamarine@be0a9c6
    Plugin: AsturaPhoenix/flutter-plugins@ad7905a

  3. added
    in triagePresently being triaged by the triage team
    on Nov 29, 2022
  4. huycozy commented on Nov 29, 2022

    @huycozy
    Member

    Hi @AsturaPhoenix, thanks for filing a detailed issue. But I can't see tile_overlay_performance_test.dart file in integration_test directory. Can you double-check that you have committed it?

    Also, this issue occurs in both renderer modes (canvaskit and html), doesn't it?

  5. added
    waiting for responseThe Flutter team cannot make further progress on this issue until the original reporter responds
    on Nov 29, 2022
  6. AsturaPhoenix commented on Nov 29, 2022

    @AsturaPhoenix
    Author

    tile_overlay_performance_test.dart is part of the Flutter app, https://github.com/AsturaPhoenix/trip_planner_aquamarine/blob/master/integration_test/performance_test.dart ; sorry for the confusion.

    I don't think the canvas APIs in use here work with the html renderer; I'm not entirely clear on the failure mode (beyond the operations having "limited support"), but I can dig deeper if needed.

    Addendum 1
    It's worth noting that the master branch is currently ahead of this issue and uses the HTML5 canvas bypass; a commit with the version of the test that expects blob img elements is @75ece1.

    Addendum 2
    It would also seem I renamed the test at some point and the comment is stale. performance_test.dart is the correct file.

  7. removed
    waiting for responseThe Flutter team cannot make further progress on this issue until the original reporter responds
    on Nov 29, 2022
  8. huycozy commented on Nov 30, 2022

    @huycozy
    Member

    Hi @AsturaPhoenix, thanks for providing more info, but it is still missing joda_time in your trip_planner_aquamarine repository.

    Logs (flutter pub get error)
    Running "flutter pub get" in trip_planner_aquamarine...          2,607ms
    Resolving dependencies...
    Because trip_planner_aquamarine depends on joda from path which doesn't exist (could not find package joda at "../joda_time"), version solving failed.
    pub get failed
    command: "/Users/huynq/Documents/GitHub/flutter_master/bin/cache/dart-sdk/bin/dart __deprecated_pub --color --directory .
    get --example"
    pub env: {
      "FLUTTER_ROOT": "/Users/huynq/Documents/GitHub/flutter_master",
      "PUB_ENVIRONMENT": "flutter_cli:get",
      "PUB_CACHE": "/Users/huynq/.pub-cache",
    }
    exit code: 66
    last line of pub output: "Because trip_planner_aquamarine depends on joda from path which doesn't exist (could not find
    package joda at "../joda_time"), version solving failed."

    Also, I need to update dependency_overrides path as below, hope it's same as your side:

    dependency_overrides:
      google_maps_flutter:
        path: ../flutter-plugins/packages/google_maps_flutter/google_maps_flutter
      google_maps_flutter_android:
        path: ../flutter-plugins/packages/google_maps_flutter/google_maps_flutter_android
      google_maps_flutter_ios:
        path: ../flutter-plugins/packages/google_maps_flutter/google_maps_flutter_ios
      google_maps_flutter_platform_interface:
        path: ../flutter-plugins/packages/google_maps_flutter/google_maps_flutter_platform_interface
      google_maps_flutter_web:
        path: ../flutter-plugins/packages/google_maps_flutter/google_maps_flutter_web
    
    
  9. added
    waiting for responseThe Flutter team cannot make further progress on this issue until the original reporter responds
    on Nov 30, 2022
  10. AsturaPhoenix commented on Nov 30, 2022

    @AsturaPhoenix
    Author

    @huycozy joda_time is at https://github.com/AsturaPhoenix/joda_time, but I suspect it'll be easier to stand up the reduced integration/performance test which shouldn't depend on that than the full environment. Let me know if you want me to try setting it up in its own repo.

  11. removed
    waiting for responseThe Flutter team cannot make further progress on this issue until the original reporter responds
    on Nov 30, 2022
  12. huycozy commented on Nov 30, 2022

    @huycozy
    Member

    @AsturaPhoenix thanks, now I'm able to run the sample code. I can not observe the frame drops as your above experiment but it seems I can see it through visuals from below video (white area/border can be seen during animation zooms in and zooms out)

    To make the issue easier to reproduce with others, I summarize it as follows:

    Steps to reproduce

    1. Pull all these repositories into a directory
    1. Open trip_planner_aquamarine project in an IDE and update pubspec.yaml 's dependency_overrides path as below, hope it's same as your side:
    dependency_overrides
    dependency_overrides:
      google_maps_flutter:
        path: ../flutter-plugins/packages/google_maps_flutter/google_maps_flutter
      google_maps_flutter_android:
        path: ../flutter-plugins/packages/google_maps_flutter/google_maps_flutter_android
      google_maps_flutter_ios:
        path: ../flutter-plugins/packages/google_maps_flutter/google_maps_flutter_ios
      google_maps_flutter_platform_interface:
        path: ../flutter-plugins/packages/google_maps_flutter/google_maps_flutter_platform_interface
      google_maps_flutter_web:
        path: ../flutter-plugins/packages/google_maps_flutter/google_maps_flutter_web
    1. Run the test (Flutter master channel)
    chromedriver --port=4444 & flutter drive -d chrome --driver test_driver/integration_test.dart --target integration_test/performance_test.dart --profile
    

    Result

    Demo video
    Screen.Recording.2022-11-30.at.16.10.18.mp4
    Logs
    Launching integration_test/performance_test.dart on Chrome in profile mode...
    Compiling integration_test/performance_test.dart for the Web...        39.7s
    [1669799776.348][WARNING]: This version of ChromeDriver has not been tested with Chrome version 108.
    [1669799776.783][WARNING]: virtual void DevToolsClientImpl::AddListener(DevToolsEventListener *) subscribing a listener to the already connected DevToolsClient. Connection notification will not arrive.
    All tests passed.
    Application finished.
    
    flutter doctor -v (master)
    [!] Flutter (Channel master, 3.6.0-11.0.pre.25, on macOS 13.0.1 22A400 darwin-x64, locale en-VN)
        • Flutter version 3.6.0-11.0.pre.25 on channel master at /Users/huynq/Documents/GitHub/flutter_master
        ! Warning: `flutter` on your path resolves to /Users/huynq/Documents/GitHub/flutter/bin/flutter, which is not inside your current Flutter SDK checkout at /Users/huynq/Documents/GitHub/flutter_master. Consider adding /Users/huynq/Documents/GitHub/flutter_master/bin to the front of your path.
        ! Warning: `dart` on your path resolves to /Users/huynq/Documents/GitHub/flutter/bin/dart, which is not inside your current Flutter SDK checkout at /Users/huynq/Documents/GitHub/flutter_master. Consider adding /Users/huynq/Documents/GitHub/flutter_master/bin to the front of your path.
        • Upstream repository https://github.com/flutter/flutter.git
        • Framework revision 31ea315c3c (24 minutes ago), 2022-11-29 18:29:02 -0800
        • Engine revision 25a2ac85f6
        • Dart version 2.19.0 (build 2.19.0-430.0.dev)
        • DevTools version 2.19.0
        • If those were intentional, you can disregard the above warnings; however it is recommended to use "git" directly to perform update checks and upgrades.
    
    [✓] Android toolchain - develop for Android devices (Android SDK version 31.0.0)
        • Android SDK at /Users/huynq/Library/Android/sdk
        • Platform android-33, build-tools 31.0.0
        • ANDROID_HOME = /Users/huynq/Library/Android/sdk
        • Java binary at: /Applications/Android Studio.app/Contents/jre/Contents/Home/bin/java
        • Java version OpenJDK Runtime Environment (build 11.0.13+0-b1751.21-8125866)
        • All Android licenses accepted.
    
    [✓] Xcode - develop for iOS and macOS (Xcode 14.1)
        • Xcode at /Applications/Xcode.app/Contents/Developer
        • Build 14B47b
        • CocoaPods version 1.11.3
    
    [✓] Chrome - develop for the web
        • Chrome at /Applications/Google Chrome.app/Contents/MacOS/Google Chrome
    
    [✓] Android Studio (version 2021.3)
        • Android Studio at /Applications/Android Studio.app/Contents
        • Flutter plugin can be installed from:
          🔨 https://plugins.jetbrains.com/plugin/9212-flutter
        • Dart plugin can be installed from:
          🔨 https://plugins.jetbrains.com/plugin/6351-dart
        • Java version OpenJDK Runtime Environment (build 11.0.13+0-b1751.21-8125866)
    
    [✓] IntelliJ IDEA Community Edition (version 2022.1.1)
        • IntelliJ at /Users/huynq/Library/Application Support/JetBrains/Toolbox/apps/IDEA-C/ch-0/221.5591.52/IntelliJ IDEA CE.app
        • Flutter plugin can be installed from:
          🔨 https://plugins.jetbrains.com/plugin/9212-flutter
        • Dart plugin can be installed from:
          🔨 https://plugins.jetbrains.com/plugin/6351-dart
    
    [✓] VS Code (version 1.73.1)
        • VS Code at /Applications/Visual Studio Code.app/Contents
        • Flutter extension version 3.52.0
    
    [✓] Connected device (3 available)
        • SM T225 (mobile) • R9JT3004VRJ • android-arm64  • Android 12 (API 31)
        • macOS (desktop)  • macos       • darwin-x64     • macOS 13.0.1 22A400 darwin-x64
        • Chrome (web)     • chrome      • web-javascript • Google Chrome 107.0.5304.121
    
    [✓] HTTP Host Availability
        • All required HTTP hosts are available
    
    ! Doctor found issues in 1 category.
    

    Labeling this issue for further investigation from the team.

  13. removed
    in triagePresently being triaged by the triage team
    on Nov 30, 2022
  14. 11 remaining items

  15. added
    P3Issues that are less important to the Flutter project
    on Dec 1, 2022
  16. elitree commented on Mar 16, 2023

    @elitree

    @AsturaPhoenix I noticed that your PR was closed due to the plugins repo moving to flutter/packages. I'm hoping to make use of those changes from your PR, and have interest in seeing that PR progress further... I'm not sure whether you have time to migrate the PR to the new repo, but if it would help, I'd be glad to attempt migrating it to the new repo myself. Thanks!

  17. AsturaPhoenix commented on Mar 16, 2023

    @AsturaPhoenix
    Author

    @elitree Sure, please do. I was holding off until someone bugged me or I had to. Feel free to take ownership, or I can jump in if there are any review comments that need changes.

    Out of curiosity, is your use case also impacted by the API bottleneck on this issue? It does bother me a little that we haven't seen any technical/design discussion on this issue even if it's technically a nonissue until that PR implements initial support.

  18. elitree commented on Mar 17, 2023

    @elitree

    @AsturaPhoenix sounds good. I'll try to get a new PR out early next week, and will reach out with questions.

    Our use case is affected by the API bottleneck here. We've got a stable mobile app, and have been looking at enabling a web version, but performance is a deal breaker at the moment, specifically with the custom tile loading. We're looking into alternative solutions for loading in tile images on the web client (for example, using a raster image tile set instead of PNGs), but it would certainly be more ideal if we could use the same strategy on all platforms.

    I agree that it would be helpful and encouraging to see more discussion on this functionality from those who could steward it. Thanks again for your efforts so far getting this feature closer to being supported for the web.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    P3Issues that are less important to the Flutter projectc: new featureNothing broken; request for a new capabilityc: performanceRelates to speed or footprint issues (see "perf:" labels)found in release: 3.6Found to occur in 3.6has reproducible stepsThe issue has been confirmed reproducible and is ready to work onp: mapsGoogle Maps pluginpackageflutter/packages repository. See also p: labels.platform-webWeb applications specificallyteam-webOwned by Web platform teamtriaged-webTriaged by Web platform team

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions