Skip to content
This repository has been archived by the owner on Aug 8, 2023. It is now read-only.

Performance regression between iOS 0.5.2 and 0.5.3 #1975

Closed
tomtaylor opened this issue Jul 31, 2015 · 51 comments
Closed

Performance regression between iOS 0.5.2 and 0.5.3 #1975

tomtaylor opened this issue Jul 31, 2015 · 51 comments
Assignees
Labels
iOS Mapbox Maps SDK for iOS performance Speed, stability, CPU usage, memory usage, or power usage

Comments

@tomtaylor
Copy link

I've noticed a performance regression between iOS 0.5.2 and 0.5.3. I have a UI element which acts as a scrubber along a polyline (video here). I use a custom animation, driven by a CADisplayLink, to set the zoom and centre coordinate of the map every frame using a 3D spring behaviour.

In 0.5.2, the labels don't redraw until the animation stops, and during the animation the movement is smooth.

In 0.5.3, if the animation changes the zoom to the extent that the labels are due for refresh, it becomes jerky. If you pause the movement long enough for the labels to redraw, it becomes smooth again.

Let me know if you need any more information.

@friedbunny friedbunny added iOS Mapbox Maps SDK for iOS performance Speed, stability, CPU usage, memory usage, or power usage labels Aug 1, 2015
@friedbunny
Copy link
Contributor

This sounds like it would be related to @brunoabinader's label work in #1548.

/cc @incanus

@brunoabinader
Copy link
Member

This sounds like it would be related to @brunoabinader's label work in #1548.

We have a fix for that in #1928 (commit 5e2f3b1), which landed on version 0.5.4. @tomtaylor, is it possible to test it against version 0.5.4 instead of 0.5.3? Also, I'd love to reproduce the custom animation if you have it handy.

@tomtaylor
Copy link
Author

Thanks - I'm away from my machine at the moment, but I'm pretty confident I tested against 0.5.4 with the same results. I'll get you the animation code when I get back.

On 1 Aug 2015, at 09:03, Bruno de Oliveira Abinader notifications@github.com wrote:

This sounds like it would be related to @brunoabinader's label work in #1548.

We have a fix for that in #1928, which landed on version 0.5.4. @tomtaylor, is it possible to test it against version 0.5.4 instead of 0.5.3? Also, I'd love to reproduce the custom animation if you have it handy.


Reply to this email directly or view it on GitHub.

@tomtaylor
Copy link
Author

OK, here's the animation driver code: https://gist.github.com/tomtaylor/91d29f47f281b9d96fcd

And I can confirm the same behaviour on 0.5.4. Let me know if I can give you any more info.

@brunoabinader
Copy link
Member

@tomtaylor I just merged #1979 into master; could you please check?

@tomtaylor
Copy link
Author

@brunoabinader happy to, do you think you could make me a new podspec file, pointing to a precompiled .zip version?

@tomtaylor
Copy link
Author

@brunoabinader I just managed to try this out on 2.1.0-pre.1, and I don't think it's quite fixed yet. I recorded a video through Quicktime from my 5s, which shows what I'm experiencing. For context, I'm touching on the scrubber at the bottom to zoom in at that point on the route, driven by the animation code I shared previously. Let me know if I can give you any more information.

https://s3-eu-west-1.amazonaws.com/files.tomtaylor.co.uk/data/mapbox-lag.mov

(On the flip side, it seems to perform even better when all the labels have rendered!)

@tomtaylor
Copy link
Author

@brunoabinader @incanus should I open a new issue for this?

@incanus
Copy link
Contributor

incanus commented Sep 1, 2015

Nope, we'll re-investigate this here.

@incanus incanus reopened this Sep 1, 2015
@incanus incanus added this to the ios-v2.1.0 milestone Sep 1, 2015
@incanus incanus self-assigned this Sep 4, 2015
@incanus
Copy link
Contributor

incanus commented Sep 4, 2015

@tomtaylor You might be better served here by actually using the camera API in the upcoming ios-v2.1.0. It will let you concatenate animations between viewport changes without the need for a CADisplayLink-style syncing as you are doing. I can't quite replicate your whole setup for testing, but I will try to replicate the effect myself and see how it fares.

@incanus
Copy link
Contributor

incanus commented Sep 4, 2015

@tomtaylor e.g. check out this little demo I whipped up using the latest pre-release:

https://github.com/incanus/UISyncedMap

It uses a slider to trace along a polyline annotation, and not even using the camera API, but rather just normal center/zoom setting.

anigif-1441395874

It seems like an approach like this might work for your use case (tracking the elevation profile rather than a normal slider, though).

@tomtaylor
Copy link
Author

Thanks @incanus, using setCoordinate with animated: true without a CADisplayLoop is pretty close, but not quite as smooth, because it's starting a new animation each time the scrubber moves, which doesn't preserve the existing velocity.

For my use, I really want to to zoom in when the user touches the scrubber, so I need to use an animation when I set the position. This regression only exhibits when zooming in - my CADisplayLink approach is nice and smooth when the user is already at the correct zoom level and is just panning around.

My simplistic understanding is that if there's any outstanding label render tasks going on, those should be cancelled if a new setCoordinate request comes in, especially if it changes the zoom level. That would ensure the map waits until the zoom level has settled, before trying to rerender the labels, and the rerender process doesn't block any subsequent attempts to move the map. Does that make sense?

I tried to implement it using the camera API, to see if that behaves differently in this regard, but got stuck because of #2266, so I'll give it another go when that lands.

@tomtaylor
Copy link
Author

OK, using setCoordinate with animated: true is no longer possible on pre3. It looks like each animation is backing up behind previous ones, and with a scrub from left to right generating 10s of these, I get a big delay before the final one kicks in and the view actually moves.

@incanus
Copy link
Contributor

incanus commented Sep 9, 2015

Hmm, these aren't self-canceling, so that seems like it might happen @tomtaylor. I think this is also the case with MapKit. I think where we want to be is #1581 but also with cancelling handlers maybe.

@tomtaylor
Copy link
Author

That'd be better, but ideally I'd like MGLMapView to cancel any existing label redraws until the zoom level has settled down - then I'd be able to drive this from a CADisplayLoop using with setCenterCoordinate:animated with animations disabled. I appreciate this kind of usage might not be a priority though.

@incanus
Copy link
Contributor

incanus commented Sep 9, 2015

I see; it’s more about the interaction between viewport changes and label rendering holding things up (or vice-versa).

@tomtaylor
Copy link
Author

Yep, understood - let me know if I can help diagnose or test anything.

@incanus
Copy link
Contributor

incanus commented Sep 15, 2015

Moving from iOS milestone for now.

@brunoabinader
Copy link
Member

@adam-mapbox commit 0753aad checks for pending faded property animations to call for another render pass. This is need in cases where we zoom in/out via gestures and causes the labels to get in a semi-transparent state because the fade animation has not finished yet - related issue/pr is #1548.

After that patch, lots of changes related to render pass triggering happened. I wonder if the same issue happens on the latest version? If so, we might think about adding an option to ignore painter->needsAnimation() check if under a client-based animation case, as the extra render pass seems to be the root cause for this issue.

@jfirebaugh
Copy link
Contributor

Next step here is to determine why the addition of painter->needsAnimation() triggers jankiness.

@incanus
Copy link
Contributor

incanus commented Oct 28, 2015

From chat with @brunoabinader:

style->hasTransitions() and painter->needsAnimation() are now checked in separated if's - the former checks for style and/or class properties transitions (eg. from one style to another, or from a set (or absence) of applied style class(es) to another), while the latter checks the properties in which we use Faded<T> while parsing - and you are right about the zoom checks: the fade transition (from transparent to opaque and vice-versa) is triggered upon zoom level changes

this sounds like a good idea to add a brief comments in MapContext::renderSync

AFAIK, in Mapbox GL we have three different animations/transitions types: the two mentioned above plus the ones caused by transform changes (iterated via Transform::updateTransitions)

@adam-mapbox
Copy link
Contributor

@jfirebaugh @kkaefer @incanus

So, after a few hours of investigation, here's my preliminary thoughts. It looks like, when we enter this kind of use case, we're doing a sort of "double-render". Let me explain. Inside MapContext::renderSync, if we're currently updating, then we wind up calling asyncUpdate->send(), which ensures that we will later call another MapContext::renderSync. So we basically have a sort of loop driving our rendering here. But, when we call setZoomLevel, we end up changing the transform, which I believe also triggers some kind of loop (although I haven't determined the exact mechanism yet). So basically we end up with two loops driving rendering at the same time: the actual changes themselves, and then MapContext::renderSync, which, recognizing that things are in flux, is constantly asking for updates as well. The fix I recommended works because it disables that first loop from rendering. If we don't want to fix things that way, then either the setZoomLevel call can't actually trigger an update itself (but that doesn't work because when i tried that, it felt like the loop never got "primed" and I ended up with a black screen), or we can't have the first loop driving things (that doesn't work either for the reasons that @brunoabinader originally put this fix in for), or somehow the MapContext::renderSync loop has to recognize that it's already running updates and invalidate the update.

Someone who knows more about this please chime in, but is there some way to get update to recognize perhaps that other updates are already pending and not call invalidateSync, and if we did that, would it fix the problem?

@brunoabinader
Copy link
Member

@adam-mapbox I've been debugging this together with @tmpsantos and we found out the bottleneck isn't painter->needsAnimation() itself - that function is pretty fast. The bottleneck seems to be related with the extra asyncUpdate.send() caused by it. We're doing a custom animation in Qt (via QPropertyAnimation) to simulate the client-driven animations mechanism. I'm now thinking about ways to minimize the impact of that async update call - either avoiding it as much as possible or doing some refactors in MapContext::update to simplify the Repaint update type.

@jfirebaugh
Copy link
Contributor

@brunoabinader Do you know why exactly the async update introduces stuttering? My understanding is:

  • uv_async coalesces multiple calls during the same event loop iteration -- upshot is you can call asyncUpdate as often as you want during one turn of the event loop, and it still results in only one notification.
  • Even if we are somehow getting more async notifications than normal, all Update::Repaint does under the hood is ask the view to invalidate itself. It's assumed that view invalidation also has some form of deduping internally -- e.g. even if you invalidate the view twice before it gets a chance to repaint, it will only repaint once.
  • Even if both deduping mechanisms fail, I would expect the end result to be... an unnecessarily high frame rate. Not a reduced frame rate.

So I'm keen to have somebody pinpoint the exact issue here.

@adam-mapbox
Copy link
Contributor

@jfirebaugh If the results from XCode are to be believed, then it seems that the FPS is in fact higher when the async update is triggered...but the observable result is stuttering.

@brunoabinader
Copy link
Member

@adam-mapbox we had a similar approach today with Qt - rendering frames are always 60fps, but animation ticks (animation update calls from client) were raised from 39 per sec to ~60 with that asyncUpdate comment. We yet have to find out why.

@ljbade
Copy link
Contributor

ljbade commented Nov 2, 2015

Interesting. On Android I have had problems where the underlying MapView says it is rendering at 60fps, but only every second frame makes it to the screen (#2773). Probably not related to this however

@adam-mapbox
Copy link
Contributor

OK. I have a theory, or at least some interesting data. I wired myself up to -[EAGLContext presentRenderbuffer], and got some interesting results. When the updateSync is wired up, we drive the rendering at almost 60 FPS. When it isn't, we drive rendering at about 12 FPS. Why does 12 FPS feel better than 60? Perhaps it has to do with the standard deviation of the frame time, or our ability to maintain a consistent framerate. Here's a snapshot of the frame times with updateSync on:

17:25:21.679 Mapbox GL[9365:2274810] [EAGLContext presentRenderbuffer]: Time since last render:   0.0117 Average:   0.0167 StdDev:   0.0037
2015-11-02 17:25:21.698 Mapbox GL[9365:2274810] [EAGLContext presentRenderbuffer]: Time since last render:   0.0183 Average:   0.0166 StdDev:   0.0037
2015-11-02 17:25:21.714 Mapbox GL[9365:2274810] [EAGLContext presentRenderbuffer]: Time since last render:   0.0159 Average:   0.0167 StdDev:   0.0036
2015-11-02 17:25:21.730 Mapbox GL[9365:2274810] [EAGLContext presentRenderbuffer]: Time since last render:   0.0165 Average:   0.0167 StdDev:   0.0036
2015-11-02 17:25:21.748 Mapbox GL[9365:2274810] [EAGLContext presentRenderbuffer]: Time since last render:   0.0176 Average:   0.0167 StdDev:   0.0036
2015-11-02 17:25:21.765 Mapbox GL[9365:2274810] [EAGLContext presentRenderbuffer]: Time since last render:   0.0176 Average:   0.0167 StdDev:   0.0036
2015-11-02 17:25:21.782 Mapbox GL[9365:2274810] [EAGLContext presentRenderbuffer]: Time since last render:   0.0170 Average:   0.0167 StdDev:   0.0036
2015-11-02 17:25:21.802 Mapbox GL[9365:2274810] [EAGLContext presentRenderbuffer]: Time since last render:   0.0201 Average:   0.0167 StdDev:   0.0036
2015-11-02 17:25:21.812 Mapbox GL[9365:2274810] [EAGLContext presentRenderbuffer]: Time since last render:   0.0102 Average:   0.0167 StdDev:   0.0037
2015-11-02 17:25:21.829 Mapbox GL[9365:2274810] [EAGLContext presentRenderbuffer]: Time since last render:   0.0165 Average:   0.0167 StdDev:   0.0037
2015-11-02 17:25:21.860 Mapbox GL[9365:2274810] [EAGLContext presentRenderbuffer]: Time since last render:   0.0315 Average:   0.0168 StdDev:   0.0040
2015-11-02 17:25:21.867 Mapbox GL[9365:2274810] [EAGLContext presentRenderbuffer]: Time since last render:   0.0065 Average:   0.0167 StdDev:   0.0041
2015-11-02 17:25:21.879 Mapbox GL[9365:2274810] [EAGLContext presentRenderbuffer]: Time since last render:   0.0121 Average:   0.0167 StdDev:   0.0041
2015-11-02 17:25:21.896 Mapbox GL[9365:2274810] [EAGLContext presentRenderbuffer]: Time since last render:   0.0171 Average:   0.0167 StdDev:   0.0041
2015-11-02 17:25:21.913 Mapbox GL[9365:2274810] [EAGLContext presentRenderbuffer]: Time since last render:   0.0170 Average:   0.0167 StdDev:   0.0041
2015-11-02 17:25:21.936 Mapbox GL[9365:2274810] [EAGLContext presentRenderbuffer]: Time since last render:   0.0228 Average:   0.0167 StdDev:   0.0041
2015-11-02 17:25:21.953 Mapbox GL[9365:2274810] [EAGLContext presentRenderbuffer]: Time since last render:   0.0167 Average:   0.0167 StdDev:   0.0041
2015-11-02 17:25:21.965 Mapbox GL[9365:2274810] [EAGLContext presentRenderbuffer]: Time since last render:   0.0121 Average:   0.0167 StdDev:   0.0041
2015-11-02 17:25:21.979 Mapbox GL[9365:2274810] [EAGLContext presentRenderbuffer]: Time since last render:   0.0144 Average:   0.0167 StdDev:   0.0041
2015-11-02 17:25:21.997 Mapbox GL[9365:2274810] [EAGLContext presentRenderbuffer]: Time since last render:   0.0177 Average:   0.0167 StdDev:   0.0041
2015-11-02 17:25:22.013 Mapbox GL[9365:2274810] [EAGLContext presentRenderbuffer]: Time since last render:   0.0160 Average:   0.0167 StdDev:   0.0041
2015-11-02 17:25:22.041 Mapbox GL[9365:2274810] [EAGLContext presentRenderbuffer]: Time since last render:   0.0277 Average:   0.0167 StdDev:   0.0042
2015-11-02 17:25:22.062 Mapbox GL[9365:2274810] [EAGLContext presentRenderbuffer]: Time since last render:   0.0216 Average:   0.0168 StdDev:   0.0041
2015-11-02 17:25:22.068 Mapbox GL[9365:2274810] [EAGLContext presentRenderbuffer]: Time since last render:   0.0058 Average:   0.0167 StdDev:   0.0043

Note the frametime of .0315 followed immediately by .0065. The human eye perceives that poorly. Now look at the frametimes with updateSync off:

2015-11-02 17:33:14.558 Mapbox GL[9379:2278223] [EAGLContext presentRenderbuffer]: Time since last render:   0.0883 Average:   0.0793 StdDev:   0.0182
2015-11-02 17:33:14.637 Mapbox GL[9379:2278223] [EAGLContext presentRenderbuffer]: Time since last render:   0.0788 Average:   0.0793 StdDev:   0.0182
2015-11-02 17:33:14.720 Mapbox GL[9379:2278223] [EAGLContext presentRenderbuffer]: Time since last render:   0.0831 Average:   0.0793 StdDev:   0.0182
2015-11-02 17:33:14.809 Mapbox GL[9379:2278223] [EAGLContext presentRenderbuffer]: Time since last render:   0.0889 Average:   0.0794 StdDev:   0.0183
2015-11-02 17:33:14.886 Mapbox GL[9379:2278223] [EAGLContext presentRenderbuffer]: Time since last render:   0.0774 Average:   0.0792 StdDev:   0.0182
2015-11-02 17:33:14.971 Mapbox GL[9379:2278223] [EAGLContext presentRenderbuffer]: Time since last render:   0.0846 Average:   0.0792 StdDev:   0.0182
2015-11-02 17:33:15.062 Mapbox GL[9379:2278223] [EAGLContext presentRenderbuffer]: Time since last render:   0.0910 Average:   0.0794 StdDev:   0.0182
2015-11-02 17:33:15.134 Mapbox GL[9379:2278223] [EAGLContext presentRenderbuffer]: Time since last render:   0.0724 Average:   0.0793 StdDev:   0.0182
2015-11-02 17:33:15.220 Mapbox GL[9379:2278223] [EAGLContext presentRenderbuffer]: Time since last render:   0.0862 Average:   0.0793 StdDev:   0.0182
2015-11-02 17:33:15.308 Mapbox GL[9379:2278223] [EAGLContext presentRenderbuffer]: Time since last render:   0.0874 Average:   0.0794 StdDev:   0.0182
2015-11-02 17:33:15.390 Mapbox GL[9379:2278223] [EAGLContext presentRenderbuffer]: Time since last render:   0.0823 Average:   0.0793 StdDev:   0.0182
2015-11-02 17:33:15.475 Mapbox GL[9379:2278223] [EAGLContext presentRenderbuffer]: Time since last render:   0.0849 Average:   0.0794 StdDev:   0.0182
2015-11-02 17:33:15.560 Mapbox GL[9379:2278223] [EAGLContext presentRenderbuffer]: Time since last render:   0.0850 Average:   0.0794 StdDev:   0.0182
2015-11-02 17:33:15.644 Mapbox GL[9379:2278223] [EAGLContext presentRenderbuffer]: Time since last render:   0.0835 Average:   0.0794 StdDev:   0.0182
2015-11-02 17:33:15.728 Mapbox GL[9379:2278223] [EAGLContext presentRenderbuffer]: Time since last render:   0.0846 Average:   0.0794 StdDev:   0.0182
2015-11-02 17:33:15.807 Mapbox GL[9379:2278223] [EAGLContext presentRenderbuffer]: Time since last render:   0.0788 Average:   0.0793 StdDev:   0.0182
2015-11-02 17:33:15.888 Mapbox GL[9379:2278223] [EAGLContext presentRenderbuffer]: Time since last render:   0.0808 Average:   0.0793 StdDev:   0.0182
2015-11-02 17:33:15.970 Mapbox GL[9379:2278223] [EAGLContext presentRenderbuffer]: Time since last render:   0.0821 Average:   0.0791 StdDev:   0.0181
2015-11-02 17:33:16.059 Mapbox GL[9379:2278223] [EAGLContext presentRenderbuffer]: Time since last render:   0.0893 Average:   0.0794 StdDev:   0.0180
2015-11-02 17:33:16.141 Mapbox GL[9379:2278223] [EAGLContext presentRenderbuffer]: Time since last render:   0.0815 Average:   0.0794 StdDev:   0.0180
2015-11-02 17:33:16.223 Mapbox GL[9379:2278223] [EAGLContext presentRenderbuffer]: Time since last render:   0.0820 Average:   0.0794 StdDev:   0.0180
2015-11-02 17:33:16.310 Mapbox GL[9379:2278223] [EAGLContext presentRenderbuffer]: Time since last render:   0.0876 Average:   0.0794 StdDev:   0.0180
2015-11-02 17:33:16.390 Mapbox GL[9379:2278223] [EAGLContext presentRenderbuffer]: Time since last render:   0.0799 Average:   0.0793 StdDev:   0.0180
2015-11-02 17:33:16.474 Mapbox GL[9379:2278223] [EAGLContext presentRenderbuffer]: Time since last render:   0.0837 Average:   0.0793 StdDev:   0.0180

Yes, the frametime is longer, but it's also more consistent. Basically, when we drive the loop too hard, we can't keep it up, and we get choppy. So it isn't even so much about the second loop, it's more about frame-limiting, or putting a cap on our frame time. I'll look into how things change if I artificially frame-limit the app next.

@incanus
Copy link
Contributor

incanus commented Nov 3, 2015 via email

@mourner mourner assigned adam-mapbox and unassigned incanus Nov 4, 2015
@adam-mapbox
Copy link
Contributor

Hallelujah! The promised land arrives. I have a fix. It's simple, it's correct, and it fixes the problem, I do believe. Here it is: #2922. Enjoy.

@incanus @jfirebaugh @tomtaylor @brunoabinader @kkaefer @1ec5

For those who want to know more: the first clue was @jfirebaugh 's comment about de-duplicating updates. Then the second clue was the erratic way that the GL view was being driven when we were doing updates, as shown by my investigation into presentRenderbuffer. I started thinking: why are we trying to draw at 60 FPS? Then I went to rate-limit the app, and I realized: we weren't obeying any kind of smoothing or rate limiting at all. We were literally just rendering as fast (or as slow) as we could. Anytime anyone called [MGLMapView invalidate], we just did a setNeedsDisplay and the system responded by drawing as fast as it could. That is a super bad idea for a number of reasons, not least of which is that it causes uneven frame rate and stuttering - which is exactly what we were seeing! I honestly thought we were being driven by a GLKViewController, but of course, I understand why we're not, now; it's obvious - we want to stop rendering when nothing is happening. So we need to roll our own GLKViewController using CADisplayLink, but wire it up so that it only tries to render iff a full 1/30 of a second has passed and we've asked for a render. Add in a little thread-safety and that's the whole change.

Et voila, the app stops rendering when we don't need to, and renders smooth as butter when we want it to.

There's only one final detail to think about, and that's our desired frame rate. Right now I capped it at 30. That works pretty well on almost all devices and in 95% of all cases. We may have select reasons to want to go up or down, though, in specific situations. For example on an older iPod touch or a crappy old iPhone, we may want it to be 20. And on a very fast machine under certain circumstances we may want to shoot for 60. I would also be in favor of making this a user-controllable setting, if we think that's wise.

It's worth mentioning that, in my limited testing on an iPod Touch 6G, this change seems to improve performance across the board; at least, perceived performance. Pans and zooms are much smoother to the eye.

@ljbade
Copy link
Contributor

ljbade commented Nov 4, 2015

@adam-mapbox - interesting so part of the problem is that we are too slow for 60fps? perhaps I should rethink #2773 and aim for capped 30fps on Android too

@ljbade
Copy link
Contributor

ljbade commented Nov 4, 2015

Android used to be happily 60fps so perhaps things in core GL have gone backwards in the last year as far as performance goes. Either that or out GL styles have become to intensive.

@incanus
Copy link
Contributor

incanus commented Nov 4, 2015

Proposed solution discussion over in #2922 (comment).

@incanus
Copy link
Contributor

incanus commented Nov 4, 2015

I honestly thought we were being driven by a GLKViewController, but of course, I understand why we're not

@adam-mapbox I should add that if at all possible, I would use GLKViewController, but we want to remain a simple UIView derivative that devs drop in, and not dictate that they use a certain type of view controller. This is to be more like MKMapView.

Other, similar issues have come up in past (see search) but we want to remain easy for devs to integrate as a standalone view.

@fezzik21
Copy link

fezzik21 commented Nov 4, 2015

Makes sense. I also don't know how we would use GLKViewController in the manner that we ended up needing to; it isn't designed to work the way that we need.

Sent from my iPhone

On Nov 4, 2015, at 11:06 AM, Justin R. Miller notifications@github.com wrote:

I honestly thought we were being driven by a GLKViewController, but of course, I understand why we're not

@adam-mapbox I should add that if at all possible, I would use GLKViewController, but we want to remain a simple UIView derivative that devs drop in, and not dictate that they use a certain type of view controller. This is to be more like MKMapView.

Other, similar issues have come up in past (see search) but we want to remain easy for devs to integrate as a standalone view.


Reply to this email directly or view it on GitHub.

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
iOS Mapbox Maps SDK for iOS performance Speed, stability, CPU usage, memory usage, or power usage
Projects
None yet
Development

No branches or pull requests

10 participants