Mapsui: Lags on iOS (Xamarin.Forms)

Created on 20 Jan 2019  路  53Comments  路  Source: Mapsui/Mapsui

While the 2.0.0 beta 15 runs pretty well on Android already, it lags on iOS while it loads the icons for the info layer and when switching between the tiles zoom level. Compared to Mapsui 1.0 it is about 60-80% slower in this cases with the same tiles and icons density - at least on iOS. Also it seems like the lag issue is bigger since the black screen issue fix for iOS. In my test I compared the latest Mapsui 1.0 to 2.0 beta 13 and 2.0 beta 15. And used Xamarin Profiler and Debug prints to measure the timings.

I know that tracking performance issues is kinda complex and hard todo. As for now I was not able to create a relevant reproduction project. I will try to create a project to showcase the issue when possible.

bug 馃悰

All 53 comments

@Sebastian1989101 Do you add the symbols via the pin layer of XF or do you add them via a normal Mapsui layer?

For the black screen I only added one line of code, where it is checked, if the GrContext exists. I assume, that this isn鈥榯 the problem.

@charlenni To add the symboles I use a dependency property on my own Mapsui Forms wrapper. On this call I remove all other layers except the 0-Layer (because this should be the tile layer) with this loop:

            while (Map.Layers.Count > 1)
                Map.Layers.Remove(Map.Layers[1]);

After that, I add the new icon layers with this code:

            var layers = new List<Layer>();

            foreach (var marker in MapMarkers)
                layers.AddRange(GenerateIconLayers(entries, marker));

            foreach (var layer in layers)
                Map.Layers.Add(layer);

Here is the GenerateIconLayers Method:

        private IEnumerable<Layer> GenerateIconLayers(MapEntry[] entries, MapMarker marker)
        {
            var layers = new List<Layer>();

            var layername = marker.Type.ToUpper();
            var features = GetIconFeatures(entries, marker);
            if (features.Any())
            {
                var layer = new Layer(layername)
                    {
                        Name = layername,
                        DataSource = new MemoryProvider(features),
                        Style = GetSymbolStyle(marker.Type),
                        IsMapInfoLayer = true,
                        MinVisible = marker.MinVisible,
                        MaxVisible = marker.MaxVisible
                    };

                layers.Add(layer);
            }

            return layers;
        }

        private Features GetIconFeatures(IEnumerable<MapEntry> entries, MapMarker marker)
        {
            var features = new Features();
            foreach (var entry in entries.Where(e => e.Type == marker.Type))
            {
                var feature = new Feature { Geometry = new Point(entry.Longitude, entry.Latitude), ["Entry"] = entry, ["Marker"] = marker };
                features.Add(feature);
            }

            return features;
        }

        private SymbolStyle GetSymbolStyle(string type)
        {
            var assembly = typeof(MvxApp).GetTypeInfo().Assembly;
            var image = assembly.GetManifestResourceStream($"{ICON_BASE_PATH}{type}.png");
            var bitmapId = BitmapRegistry.Instance.Register(image);

            return new SymbolStyle { BitmapId = bitmapId, SymbolScale = 1, SymbolOffset = new Offset(0, 0) };
        }

This code is in use since Mapsui 0.x and was only slightly altered for newer versions (if it was altered at all? I guess I only removed the ViewChanged call at the end).

It sounds to me that it is not a performance issue but related to the speed of map refresh. Could that be? What is your impression?

@pauldendulk Thats possible because the tile layer loads even faster with 2.0 on iOS. Just the info layer symboles and when zooming it lags sometimes which was not there before. I guess the zoom lag happens when he uses more detailed tiles.

A possible suspect is the call to _map.RefreshData instead of _map.Refresh. I don't remember what I did this. I will just change that. It makes sense. It might solve your problem. I will release a version this week.

I am now releasing beta 16 that might affect this issue. Did no testing. Just theory.

@pauldendulk I will put this up for my TestFlight testers tomorrow. So I think I can give you feedback within the next week.

@pauldendulk After the first responses from the testers there are no differences with beta 16 included compared to beta 15. I can also confirm that the very same app (at least same mbtiles file and data points) runs fine in the Simulator with Mapsui 1 but lags hard with Mapsui 2 (the lag is much less on a real device but still noticeable).

Thanks for testing. Could you perhaps post an animated gif of this lag in the simulator? I use ScreenToGif for this. It might give me a hunch where to look for.

@pauldendulk Here is a short video about the issue on multiple devices (iPhone 4S > iPhone 6S > iPad 2017 > iPad Pro Gen 2). Please ignore the issue that I have to Navigate in the app when I accedential open the navigation menu, thats another bug I have to fix but I always forgot to include the one neccesarry line for the fix.. https://youtu.be/mejIEWoi9YQ (I hope YouTube updates the available resolution soon... it should go up to 2160p60...).

Beside that, I can also give you the link to the TestFlight beta if you want to see it on your own device. I just have to wait for Apple to pass the current build for beta testing.

@Sebastian1989101 I tried to reproduce this with the normal samples, but haven't any succes. With this in mind, it seems, that it is a special problem with your app.

You said, that you used the profiler to get informations. Did you find the part, which needs so much time?

@charlenni The current state of the app is using ~1500 icons on the map packed in multiple info layers due to different icons. It feels like the lag always happen when the app has to render a different info layer (because other icons are fading in which where not visible before).

Another app of mine, which has ~5000 icons with a very similar amount of layers but using Mapsui 1.x, the lag does not happen (except on the iPhone 4S - on this particular old device it is even less laggy with Mapsui 2 compared to the Mapsui 1 app - all other devices, and the Simulator, had far more lags with the latest Mapsui beta compared to Mapsui 1).

I can profile it one more time to see if I can catch the relevant part. I guess it would be better to use a copy of mapsui's code instead of the NuGet package for that so I have to change my project for this test first. Maybe I find the time next weekend to do this if it may help for the issue. Maybe I should also invest some time to understand how Mapsui works internally. Maybe I can fix this by myself with a PR then.

If you want to see the issue by yourself you can use the TF Version of the app: https://testflight.apple.com/join/fPRO3Qpy (if you accidentally open the navigation menu from a left to right swipe, you have to navigate or otherwise Mapsui will get false input - I still have to deactivate the "Open menu with swipe"-Gesture before this bug is gone. Currently the TF version is using Beta 16 of Mapsui 2.

Do you see the same behavior with the SVG sample? It uses 2000 symbols in one plane, but I dont see any lags with the iOS eimulator.

@charlenni No, the samples are all running fine.

I installed your TestFlight app on a iPad and now see the effects.

I found, that the new icons (icons, that wasn't on screen before) are loaded, if I lift my finger from the display. All icons visible before, are painted and moved very fast. So, do you have any logic to get the icons? Do you create each layer every time new?

@charlenni No the layer is created once when the Tiles DB is loaded. The only way it is recreated is, when there is a updated data source (which happens if you change the settings from First to Second quest on the settings page) or when another Tile DB should be loaded.

I can create a copy of the current repository and remove all not Mapsui relevant parts if this would help. But what exactly do you mean with "All icons visible before, are painted and moved very fast."?

@pauldendulk It is the same with the map: new tile only drawn, if I lift the finger. You could see this, if you move the map and don't lift your finger. Tiles from outside are blurred.

@Sebastian1989101 Take a part from the map, where you could see icons. Now move the map around and don't lift your finger. If you move some icons outside of the screen and bring them back to screen, than there is no lag. The icons are drawn immediately. If you than lift your finger, new tiles and new icons are drawn with a lag.

@charlenni During panning there is drawing but no fetching of new data. This is to avoid a performance penalty while panning. Is this different in 1.4?

Ok, I tested the normal SVG sample with my iPad. There isn't such a behavior. There are all symbols drawed, even if the finger is on the display and moves the map. The underlaying tiles are blurred, the symbols are correct. So Mapsui handle this correct: no fetching of new tiles, but all layers drawn correctly.

@Sebastian1989101 It seems, that you doing something strang. Without an example, that shows this behavior, I could not find any problem. Perhaps you could extend your black screen example with your symbols, so that we could see, what you are doing.

You said, that you add all symbols at startup/when loading the map. No change of layers after this point. Correct?

@pauldendulk Btw. it isn't correct, that tiles aren't fetch while moving. If I use the SVG sample and move around, the tiles are updated with the right data. No blurred tiles after short time.

@charlenni Once the tiles are in memory they are fetched from the cache but not fetched from the web service. Could it be that this is what you are seeing?

@pauldendulk No, the tiles aren鈥榯 in the cache. I start the app and go to an unknown place, than start to move. Even if I have the finger on the display and move around, the map tiles are updated.

@charlenni I'm not able to test the cut down repo but I think it should work and act in the same way (I forgot my Mac at work today <.<). The password is your github username and the link is valid for two weeks from now (because it contains a lot from an upcoming project): https://cloud.kruse-familie.eu/index.php/s/4kcqMifFq8ON6Xu

@Sebastian1989101 Could you set IsMapInfoLayer = false for all you layers to check if this reduces your problem? Just to diagnose.

So, after 4 h of debuging, I would say, I need a profiler :( Without it is very difficult to get, where the time vanish.

Which profiler do you used, Sebastian? The normal VS 2017 for Mac? The normal VS 2017 for Win doesn't work. I thought, I used it before, but seems, that this were all WPF projects.

@charlenni You say you have been debugging. Does this mean you could reproduce the problem?

Please take a look at this issue. It might be related and is easy to reproduce: https://github.com/Mapsui/Mapsui/issues/578

@pauldendulk Yes, I could reproduce it. I assume, that you could do the same. Use the Mapsui.Sample.Forms.iOS sample. Than go to the demo. Zoom in and move around. You see, that the symbols appear in the moment, where the new part of the map appears on screen. No lag. Than go to the demo. Set some markers by clicking on the screen and zoom in. Now move the map. You should see, that the pins not appear in the moment, when they should be visible. There is a lag. You could see it better, if you now go to the first demo. They pins stay and the normal SVG symbols are added. The SVG symbols appear immediately, the pins later.

It could be correlated with the IsMapInfoLayer. I test this next.

Ok, found a solution, but not understanding the problem complete.

I assume, that the problem is, that you use a normal layer. Normal layers work with data, that should be fetched, what isn't the case for a MemoryProvider. Normal layers wait for completing the fetch process and than render the content. So I assume, that Mapsui waits until all new tiles are loaded and than starts to fetch the symbols (with FetchingPostponedInMilliseconds delay?). This is the lag you see. Perhaps @pauldendulk has a better idea, whats going on. As described above, the normal sample for Forms could be used.

@Sebastian1989101 The solution: Use a MemoryLayer instead of the normal Layer. For this, define your lists as List<ILayer> instead of List<Layer> and create var layer = new MemoryLayer(); instead of var layer = new Layer(layername);. That's it. Change of 3 lines and the symbols appear lightning fast while moving around. Even, if the tiles not ready. I make a PR, so that MemoryLayer could created with a layer name.

MemoryLayer knows, that all data is already in memory and could than update the symbols immediately. So I would say: use MemoryLayer always, if you have a MemoryProvider, otherwise use Layer.

@charlenni Just tested it with a MemoryLayer and actual most of the lag is gone. Thanks. The only lag that remains is when I scroll directly after a zoom. In this case the whole map (tiles + icons) stays in place for ~1 second and then starts moving. At least that is anything I noticed after a first test. I will push the modified version to TF for all test users and wait for reports if someone notice anything else.

@Sebastian1989101 I see this lags in your app, but not in the samples. You could see them, even if you move around the map, not only when zooming before.

My first thoughts are, that they appear, when new tiles are loaded. Because, when you move the map constantly around without lifting your finger, than this doesn鈥檛 happen. If you move from one side of the map to the other, than you can see it. So the first place, where to look, would be the loading of MBTiles. Perhaps we are doing something in the main thread?

With this in mind, I checked the MBTiles sample. And yes, I could see something similar there.

@Sebastian1989101 I tested a little bit more on this. I found, that your app is slow. You could see this, when you move fast in one direction. Lower finger, move a distance, lift finger again and again. You see, that the map stops to move. If you stop your finger, all movements come after another. I couldn't reproduce this with the MBTiles or SVG demo. So I assume, that it is a problem in your app. Any idea?

@charlenni The only thing different is, that I use MVX (MvvmCross) as framework compared to the sample. But I do the very same with my other app. The only difference between this app and the other one is Mapsui 2.x vs. Mapsui 1.x. Maybe I have to profile the whole app once more to figure out where the performance gets lost. I hope I have the time next weekend for this so maybe I can find the issue.

Is there maybe some limitation with the amount of data on the layers? Maybe I should also test this app with the other mbtiles file to see if this is the issue.

@Sebastian1989101 Thank you for your answer.

Do you see the same problem with moving fast around?

Gould you use a profiler with your app? Do you have an Enterprise version of VS? That would be perfect. It would be nice to see, where the time is lost.

I not assume, that it is a problem with the amount of symbols. The SVG sample has around 5000 symbols on the map and has no such problems. And if this would the problem, than moving around with finger on screen should be bring up the same problem.

Perhaps it is a problem with the number of layers. Or a problem with the IsMapInfoLayer.

@charlenni Thanks for debugging this! You do the dirty work of maintaining open source as well. I really appreciate that.

Additional lag might be related to the other issue I pointed to. I hope to have some time tonight to look at that.

@charlenni Yes, I'm using a Enterprise VS so primarly I use the Xamarin Profiler for my Xamarin Apps combined with JetBrains tools. Can you open the generated files with the profiler even without an Enterprise license? Otherwise I can only name a few places with a lot of CPU time.

If you can open those files with the profiler, you can find a short example (~40sec of Panning and Zooming) here: https://cloud.kruse-familie.eu/index.php/s/ynXRxzXyjHyNe29

Just filter for "Mapsui" in the profiler.

@Sebastian1989101 First of all: do you see the same lags when moving fast around? If yes, is the profile file of such a session?

I could open it on the Mac. But in this case, I only see 2 s out of 42 s. And from this 2 s, only a few ms are used for Mapsui. Most time consuming are Skia (around 30%) and SQLite (around 30%). Is this correct? What is the app doing between?

@charlenni Sounds like the initial loading you see there. I'm not entirly sure how I can give you a full example. What I found in the log, was that the Rendering Process seems to happen twice on iOS.

I saw this in the code. It is a RefreshGraphics to much in MapControl. I will correct this.

But again, do you see the same behavior when you try to move the map fast around?

@charlenni Here is a "log" from the Android Simulator: https://i.imgur.com/yHCK82G.png

And here is a "log" from a real iOS device (iPhone X): https://puu.sh/CGiL5/114e0260cf.png

The iOS log is only about 1/4 of the profiling time because my mac has some problems to handle so big logs currently. In the end: On Android the app runs pretty smooth for ~90% of the time while iOS is currently a little lag fest.

@pauldendulk I assume, that the problem is Sebastians amount of 23 layers. If you lift the finger and with this the touch end event is triggered, than I call Map.RefreshData(). This than calls 23 times Layer.DataChanged() and each of them calls RefreshGraphics() so the map is drawn 23 times when you lift the finger. Could be even with hardware accelaration a bit time consuming ;)

If I don't do this, the map layer isn't updated anymore. The RefreshGraphics() is called inside of MapControl.MapDataChanged(). This is called from MemoryLayer.RefreshData(). But because MemoryLayer hasn't any changing data, we don't need a redraw at this place. Perhaps we should add a new flag NeedsRefresh to DataChangedEventArgs, which is by default true. We could then set this flag to false for MemoryLayers and check for this flag in MapControl.MapDataChanged(). I made a PR for this (#591).

@charlenni The amount of layers is getting worse as development of this app continues. So if this is bound to the amount of layers it will be indeed a problem - but why does it not lag on Android, even in the Simulator, then?

Also is this a changed thing since Mapsui 1? Because the other map had ~40 layers and no lag issues as far as I can tell.

The greatest difference between your old and new version is, that you used before a renderer approach and now a native Forms implementation without any renderers. So you used totally different parts of Mapsui.

I don鈥檛 understand, why it works on Android, because it is totally the same code. Perhaps Skia aborts on Android drawings when the next change arrives while iOS draw each image after the other. But this is only a assumption.

About the refresh logic. How it is supposed to work - in Mapsui but also in general - is that Invalidate() indicates the canvas as 'dirty' and the renderloop redraws the canvas if dirty. 23 calls to Invalidate() in a short time period should cause one redraw.

Perhaps we are not using the right way to invalidate the canvas.

The fact that it works properly on Android and not on iOS is hard to explain. It might indicate a bug in XF or skia forms.

If there is no data change then we do not need a graphics refresh. Perhaps this could help (if this is not implemented like that yet).

About the refresh logic. How it is supposed to work - in Mapsui but also in general - is that Invalidate() indicates the canvas as 'dirty' and the renderloop redraws the canvas if dirty. 23 calls to Invalidate() in a short time period should cause one redraw.

But than you have to wait after each Invalidate() a short time, if a new one arrives. This could be getting worther than now. Assume, that you move the map around. In this case, the map wouldn't draw new, because there are constantly coming new Invalidate() events.

Perhaps we are not using the right way to invalidate the canvas.

I think, we use the right way. We always draw the map, if something changes. With this, the map is always up-to-date. It works ok. Each layer should decide by itself, if a refresh is needed or not. MapControl couldn't do this.

The fact that it works properly on Android and not on iOS is hard to explain. It _might_ indicate a bug in XF or skia forms.

Perhaps Android has such a functionality, that many invalidate surface calls are put together. Perhaps it waits a short time, if another call arrives and when not, than draw the surface.

But than you have to wait after each Invalidate() a short time

Yes, but this is how a 'render loop' works. A mechanism used in many games and applications. You render at at set moments and not in between. If you render at 60 frames per second the delay is not really noticeable. The advantage of this mechanism is that you don't have to makes sure all the callers behave properly. It just becomes impossible to overload the renderer by adding more calls.

For some scenarios it could be useful to slightly alter the invalidate logic. I use this for notifications in other applications. There is always a minimal delay between updates. When a new notification comes in and the delay after the previous notifications has already passed you send it immediately.

After a bit of further testing regarding to this issue, it seems that the performance is a lot better with only one layer on it (and it feels like it performs faster than Mapsui 1.x). But using Mapsui in a iOS Simulator seems to be still a problem even with only a single layer, while this was not a deal on Mapsui 1.x - of course this is not a problem for a released app, it just extends the needs in the development process.

Also it seems like Mapsui does a lot more stuff on the main thread on iOS compared to Android - it also deadlocks the app in the initialize process (depending on device power a few ms up to a few sec).

All this only applies to the Forms implementation (or at least I only tested it there).

2.0.0-beta.17 has been released. Please give it a try. There number of memory layers should be less of an issue.

@pauldendulk At first look the lag issue seems to be a lot better than before. I will put beta 17 in the public TF channel for my app and report back as soon as I got feedback from a few users.

Edit: So far, most testers report a far better performance with the map since the update which includes beta 17. Just for personal interest: What did change that affected the performance so much?

@pauldendulk So I just received a bunch of reports and overall it still looks much better with beta 17. A few users had noticed that the performance is better when the screen is smaller (for example: on an iPhone 8 it runs smoother compared to a iPhone 8+). Also some users report some kind of initialize lag where the map needs a few seconds until it reacts to anything, but this might also come from another source. One user reported a app that was locked to the map screen without reacting to any input after he brought the app from the background back into the foreground (like a static image) and another user reported a black map until he has done some input on it after the app was in the background for a few minutes. But the initial problem due to the amount of layers seems to be gone.

In general I expect better performance with a smaller screen. Less pixels to draw.

I consider the initial issue as fixed. You mention some other possible issues. Let's deal with them in separate issues when they become more clear.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

lukaszpitulski picture lukaszpitulski  路  5Comments

lukaszpitulski picture lukaszpitulski  路  8Comments

lukaszpitulski picture lukaszpitulski  路  3Comments

YordanYanakiev picture YordanYanakiev  路  6Comments

othmaniav picture othmaniav  路  7Comments