Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

When zooming and panning complex paths, performance degrades #72718

Closed
ghost opened this issue Dec 21, 2020 · 13 comments
Closed

When zooming and panning complex paths, performance degrades #72718

ghost opened this issue Dec 21, 2020 · 13 comments
Labels
c: performance Relates to speed or footprint issues (see "perf:" labels) f: material design flutter/packages/flutter/material repository. found in release: 1.26 Found to occur in 1.26 framework flutter/packages/flutter repository. See also f: labels. has reproducible steps The issue has been confirmed reproducible and is ready to work on perf: speed Performance issues related to (mostly rendering) speed

Comments

@ghost
Copy link

ghost commented Dec 21, 2020

Details

Using CustomPainter/RenderBox, I rendered a complex path by converting coordinate points(get it from the JSON file) into pixel offsets. After that, I've added the InteractiveViewer to do zooming and panning. Here, I've faced enormous performance problems.
I suspect it maybe the reason of repainting every pixel update, so I have wrapped my widget inside the RepaintBoundary widget, but still I didn't get any improvements.

I have tested both Android and iOS.

JSON file: json_file.zip

Code
import 'dart:convert';
import 'dart:math';

import 'package:flutter/material.dart';
import 'package:flutter/services.dart';

void main() => runApp(HomePage());

class HomePage extends StatefulWidget {
  @override
  _HomePageState createState() => _HomePageState();
}

class _HomePageState extends State<HomePage> {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      theme: ThemeData(
        brightness: Brightness.light,
      ),
      home: Scaffold(
        appBar: AppBar(
          title: Text('Performance issue sample'),
        ),
        body: JSONZoomingPerformance(),
      ),
    );
  }
}

class JSONZoomingPerformance extends StatefulWidget {
  @override
  _JSONZoomingPerformanceState createState() => _JSONZoomingPerformanceState();
}

class _JSONZoomingPerformanceState extends State<JSONZoomingPerformance> {
  Future<Map<String, List<List<dynamic>>>> _collectMapCoordinatePoints(
      String filePath) async {
    Map<String, List<List<dynamic>>> source =
        Map<String, List<List<dynamic>>>();
    final String bundleData = await rootBundle.loadString(filePath);
    final Map<String, dynamic> data = jsonDecode(bundleData);
    final int jsonLength = data['features'].length;
    for (int i = 0; i < jsonLength; i++) {
      final dynamic features = data['features'][i];
      Map<String, dynamic> geometry = features['geometry'];
      Map<String, dynamic> properties = features['properties'];
      if (geometry['type'] == 'Polygon') {
        List<dynamic> coordinates = geometry['coordinates'][0];
        source.update(
          properties['name'],
          (List value) {
            value.add(coordinates);
            return value;
          },
          ifAbsent: () => <List<dynamic>>[coordinates],
        );
      } else {
        int multipolygonGeometryLength = geometry['coordinates'].length;
        for (int j = 0; j < multipolygonGeometryLength; j++) {
          List<dynamic> coordinates = geometry['coordinates'][j][0];
          source.update(
            properties['name'],
            (List value) {
              value.add(coordinates);
              return value;
            },
            ifAbsent: () => <List<dynamic>>[coordinates],
          );
        }
      }
    }

    return source;
  }

  @override
  Widget build(BuildContext context) {
    return FutureBuilder<Map<String, List<dynamic>>>(
      future: _collectMapCoordinatePoints('assets/world_map.json'),
      builder: (BuildContext context,
          AsyncSnapshot<Map<String, List<dynamic>>> snapshot) {
        return LayoutBuilder(
          builder: (BuildContext context, BoxConstraints constraints) {
            final Size size = Size(
                constraints.hasBoundedWidth ? constraints.maxWidth : 300,
                constraints.hasBoundedHeight ? constraints.maxHeight : 300);
            if (snapshot.hasData) {
              return Container(
                width: size.width,
                height: size.height,
                child: InteractiveViewer(
                  maxScale: 15.0,
                  child: Center(
                    child: Container(
                      width: size.shortestSide,
                      height: size.shortestSide,
                      child: RepaintBoundary(
                        child: CustomPaint(
                          size: size,
                          painter: MapPainter(snapshot.data),
                        ),
                      ),
                    ),
                  ),
                ),
              );
            } else {
              return Center(child: Text('Map is loading...'));
            }
          },
        );
      },
    );
  }
}

class MapPainter extends CustomPainter {
  MapPainter(this.data);

  final Map<String, List<dynamic>> data;

  Offset _coordinateToOffset(num latitude, num longitude, Size size) {
    final double x = (longitude + 180.0) / 360.0;
    final double sinLatitude = sin(latitude * pi / 180.0);
    final double y =
        0.5 - log((1.0 + sinLatitude) / (1.0 - sinLatitude)) / (4.0 * pi);
    final double mapSize = size.shortestSide;
    final double dx = _clip(x * mapSize + 0.5, 0.0, mapSize - 1);
    final double dy = _clip(y * mapSize + 0.5, 0.0, mapSize - 1);
    return Offset(dx, dy);
  }

  double _clip(double value, double minValue, double maxValue) {
    return min(max(value, minValue), maxValue);
  }

  @override
  void paint(Canvas canvas, Size size) {
    Path path = Path();
    Paint fillPaint = Paint()..color = Colors.blue[100];
    Paint strokePaint = Paint()
      ..color = Colors.blue
      ..style = PaintingStyle.stroke
      ..strokeWidth = 1;
    data?.forEach((String key, List value) {
      for (int i = 0; i < value.length; i++) {
        final coordinateList = value[i];
        for (int j = 0; j < coordinateList.length; j++) {
          final coordinate = coordinateList[j];
          final Offset offset =
              _coordinateToOffset(coordinate[1], coordinate[0], size);
          if (j == 0)
            path.moveTo(offset.dx, offset.dy);
          else
            path.lineTo(offset.dx, offset.dy);
        }
      }

      canvas.drawPath(path, fillPaint);
      canvas.drawPath(path, strokePaint);
      path.reset();
    });
  }

  @override
  bool shouldRepaint(covariant CustomPainter oldDelegate) {
    // Need to implement shouldRepaint.
    return false;
  }
}

flutter doctor
[✓] Flutter (Channel stable, 1.22.4, on Mac OS X 10.15.7 19H2 darwin-x64, locale
    en-IN)
    • Flutter version 1.22.4 at /Users/VJ/sdk/flutter
    • Framework revision 1aafb3a8b9 (5 weeks ago), 2020-11-13 09:59:28 -0800
    • Engine revision 2c956a31c0
    • Dart version 2.10.4

 
[✓] Android toolchain - develop for Android devices (Android SDK version 30.0.2)
    • Android SDK at /Users/VJ/sdk/android
    • Platform android-29, build-tools 30.0.2
    • ANDROID_SDK_ROOT = /Users/VJ/sdk/android
    • Java binary at: /Applications/Android Studio.app/Contents/jre/jdk/Contents/Home/bin/java
    • Java version OpenJDK Runtime Environment (build 1.8.0_242-release-1644-b3-6915495)
    • All Android licenses accepted.

[✓] Xcode - develop for iOS and macOS (Xcode 12.0)
    • Xcode at /Applications/Xcode.app/Contents/Developer
    • Xcode 12.0, Build version 12A8189h
    • CocoaPods version 1.9.3

[!] Android Studio (version 4.1)
    • Android Studio at /Applications/Android Studio.app/Contents
    ✗ Flutter plugin not installed; this adds Flutter specific functionality.
    ✗ Dart plugin not installed; this adds Dart specific functionality.
    • Java version OpenJDK Runtime Environment (build 1.8.0_242-release-1644-b3-6915495)

[✓] VS Code (version 1.52.1)
    • VS Code at /Applications/Visual Studio Code.app/Contents
    • Flutter extension version 3.17.0

[✓] Connected device (1 available)
    • ONEPLUS A3003 (mobile) • android-arm64 • Android 9 (API 28)

! Doctor found issues in 1 category.
@ghost ghost added the from: performance template Issues created via a performance issue template label Dec 21, 2020
@chunhtai chunhtai added framework flutter/packages/flutter repository. See also f: labels. perf: speed Performance issues related to (mostly rendering) speed labels Dec 21, 2020
@chunhtai
Copy link
Contributor

cc @goderbauer

@pedromassangocode
Copy link

I was able to notice some lag while zooming in and then trying move the map.

flutter doctor -v
[✓] Flutter (Channel master, 1.26.0-2.0.pre.103, on Mac OS X 10.15.7 19H2 darwin-x64, locale en)
    • Flutter version 1.26.0-2.0.pre.103 at /Users/pedromassango/dev/SDKs/flutter_master
    • Framework revision 32741c0ec8 (8 hours ago), 2020-12-21 18:34:02 -0600
    • Engine revision af6889a600
    • Dart version 2.12.0 (build 2.12.0-169.0.dev)

[✓] Android toolchain - develop for Android devices (Android SDK version 30.0.2)
    • Android SDK at /Users/pedromassango/Library/Android/sdk
    • Platform android-30, build-tools 30.0.2
    • Java binary at: /Applications/Android Studio.app/Contents/jre/jdk/Contents/Home/bin/java
    • Java version OpenJDK Runtime Environment (build 1.8.0_242-release-1644-b3-6915495)
    • All Android licenses accepted.

[!] Xcode - develop for iOS and macOS (Xcode 12.1)
    • Xcode at /Applications/Xcode.app/Contents/Developer
    • Xcode 12.1, Build version 12A7403
    ! CocoaPods 1.9.3 out of date (1.10.0 is recommended).
        CocoaPods is used to retrieve the iOS and macOS platform side's plugin code that responds to your plugin usage on the Dart side.
        Without CocoaPods, plugins will not work on iOS or macOS.
        For more info, see https://flutter.dev/platform-plugins
      To upgrade see https://guides.cocoapods.org/using/getting-started.html#installation for instructions.

[✓] Chrome - develop for the web
    • Chrome at /Applications/Google Chrome.app/Contents/MacOS/Google Chrome

[✓] Android Studio (version 4.1)
    • 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 1.8.0_242-release-1644-b3-6915495)

[✓] IntelliJ IDEA Community Edition (version 2020.3)
    • IntelliJ at /Applications/IntelliJ IDEA CE.app
    • Flutter plugin version 52.1.5
    • Dart plugin version 203.5981.152

[✓] VS Code (version 1.51.1)
    • VS Code at /Applications/Visual Studio Code.app/Contents
    • Flutter extension version 3.16.0

[✓] Connected device (3 available)
    • Redmi 5 Plus (mobile) • 0258ff700005 • android-arm64  • Android 8.1.0 (API 27)
    • macOS (desktop)       • macos        • darwin-x64     • Mac OS X 10.15.7 19H2 darwin-x64
    • Chrome (web)          • chrome       • web-javascript • Google Chrome 87.0.4280.88

! Doctor found issues in 1 category.
Process finished with exit code 0

@pedromassangocode pedromassangocode added f: material design flutter/packages/flutter/material repository. found in release: 1.26 Found to occur in 1.26 has reproducible steps The issue has been confirmed reproducible and is ready to work on c: performance Relates to speed or footprint issues (see "perf:" labels) and removed from: performance template Issues created via a performance issue template labels Dec 22, 2020
@ibrierley
Copy link

This could also be something I've been seeing (using flutter_map). I've been trying to draw vectors/paths using a custompainter (with a repaintboundary). My experience has been that panning is ok, but pinch-zooming is very slow, and the painter needs to repaint every time (frame) and there seems to be simply no way to avoid it (even with shouldrepaint false).

I had wondered about wrapping it in a valuenotifier, but I suspect the custompainter has to completely repaint every time there is a different zoom/transform or whatever.

In your case, I wonder if you could try creating a path with subpaths of those, and only call drawpath once (if you aren't changing the fills/strokes) ? That may help a little ?

@ghost
Copy link
Author

ghost commented Dec 24, 2020

@ibrierley Thanks for your inputs.

Previously I had redrawn all the paths every time while zooming and panning. As expected, I have faced lagging issues and hence I decided to wrap our widget inside the Transform widget with RepaintBoundary. Now, we set only the scale and translation value while interacting (I had used InteractiveViewer in the above sample but tried Transform widget already) and redrawn the paths only at the interaction end and not during interaction. So paint is not being called while interacting now.

@RomanJos
Copy link
Contributor

This is weird, the RepaintBoundary work as expected because the painter don't redraw everytimes, when zooming it take between 60ms and 30ms to draw a frame but when panning it only take 3ms so there is something to do with the zoom.

Transform.scale give the same performance so I don't believe its the fault of InteractiveViewer but something else that has to do with how flutter paint handle long path ?

@ghost
Copy link
Author

ghost commented Jan 18, 2021

@pedromassangocode Can you please update its progress?

@pedromassangocode
Copy link

@pedromassangocode Can you please update its progress?

No, you will get notification for every update on this issue (because you are the author)!

@whiplashoo
Copy link

I have faced similar issues with using CustomPaint for drawing many complex paths and then trying to pan and zoom. My open issue: #72066

Do you also see a difference between Android and iOS? In my case, on iOS devices the issue is almost disappearing; only older iPhones may show some jank when zooming. However, on Android, even high-end devices seem to struggle and get under 20fps.

@ghost
Copy link
Author

ghost commented Feb 19, 2021

@whiplashoo Yes, I can see the difference between Android and iOS. Facing more performance lag with android and iOS is better than that.

@modulovalue
Copy link

There is a shorter (~20 line) example of this issue with a video in #78543.

@tmaihoff
Copy link

I had the same problem and solved it by switching from path drawing to line drawing.
See my detailed answer here: #78543 (comment)

@Piinks
Copy link
Contributor

Piinks commented Jun 16, 2022

This looks like a duplicate of #78543. To focus conversation and work to resolve the issue, I am going to close this. Please follow up there. A lot of discussion has already occurred with potential solutions.

If you feel this is a separate issue, let us know and we'll take another look! Thanks!

@Piinks Piinks closed this as completed Jun 16, 2022
@github-actions
Copy link

github-actions bot commented Jul 1, 2022

This thread has been automatically locked since there has not been any recent activity after it was closed. If you are still experiencing a similar issue, please open a new bug, including the output of flutter doctor -v and a minimal reproduction of the issue.

@github-actions github-actions bot locked as resolved and limited conversation to collaborators Jul 1, 2022
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
c: performance Relates to speed or footprint issues (see "perf:" labels) f: material design flutter/packages/flutter/material repository. found in release: 1.26 Found to occur in 1.26 framework flutter/packages/flutter repository. See also f: labels. has reproducible steps The issue has been confirmed reproducible and is ready to work on perf: speed Performance issues related to (mostly rendering) speed
Projects
None yet
Development

No branches or pull requests

8 participants