mapbox / mapbox-sdk-cs

C# libraries for Mapbox API layer
https://mapbox.github.io/mapbox-sdk-cs/
Other
20 stars 11 forks source link

Improve Data Fetching in Unity #11

Closed david-rhodes closed 7 years ago

david-rhodes commented 7 years ago

TL;DR

Our current Unity implementation for making web requests is quite inefficient. Let's find ways to improve it.

Possible Inefficiencies

Other Considerations

Possible Solutions

Next Steps

It would be great to have a C# expert (@BergWerkGIS, I'm looking at you) experiment in this area.

/cc @mapbox/games

brnkhy commented 7 years ago

Each low level object that wants to make a request needs a FileSource. This is often duplicated between objects.

If I get that right, this should be up to developer and they can easily pass same filesource to all services. At least that's what I'm doing in my demos (TerrainDemoUnity branch).

I also tried running everything in editor mode but as you said it doesn't work without some ugly hacks. But I think that's Unity3D's problem and we can go with them.

convenience layer

I totally agree on this one. People will feel lost without proper feedbacks, info etc. But that also means lots of editor script work I guess (or we can just debug.log for a while)

@david-rhodes we can implement both options (coroutines and threads) as filesources right? I think we should have both options anyway.

david-rhodes commented 7 years ago

If I get that right, this should be up to developer and they can easily pass same filesource to all services. At least that's what I'm doing in my demos (TerrainDemoUnity branch).

Sort of. I'm personally using a "singleton" FileSource, but if we add threading, I think it should be hidden from developers. They make a request through a singleton and we make a new thread if needed. Something like that.

But I think that's Unity3D's problem and we can go with them.

Not sure what "go with them means." This is our problem, because we're going to want the ability to make web requests at edit time (custom editors/property drawers, etc.).

I think we should have both options anyway.

True. This would just be another implementation of IFileSource.

tmpsantos commented 7 years ago

Web requests are using coroutines. Each coroutine allocates memory on the heap (every tick). Requesting hundreds of tiles will lead to "massive" GC spikes.

The file source could implement a queue in this case to throttle the max number of requests. We do that on Mapbox GL Native. Often when you are zooming or panning the map, the requests in the queue are cancelled and they never really happen.

We started with co-routines because it was the only way to support the WebPlayer. The interesting thing about the FileSource abstraction is that you could replace it with a ThreadedFileSource if you will never target the WebPlayer.

wilhelmberg commented 7 years ago

"bookmarking" for myself: https://github.com/BruTile/BruTile/issues/10 https://github.com/FObermaier/DotSpatial.Plugins/blob/master/DotSpatial.Plugins.BruTileLayer/TileFetcher.cs

parahunter commented 7 years ago

In regards to the coroutines not running outside play mode here's some ways to solve it

  1. Move that code into separate threads as you have already suggested
  2. Where you create the coroutine, detect if editor is playing, if not pass it onto your own coroutine runner that uses EditorApplication.Update to run the coroutine. You can use [InitializeOnLoad] to subscribe to that callback when the Unity editor is opened. Not sure if WWW will run outside play mode though but usually Unity is good at making sure everything that runs in play mode can also be done in edit mode
  3. block the main thread until the data is returned if the call is done in edit mode (ugly)
wilhelmberg commented 7 years ago

Tried an easy route for a quick win via the backported Task Parallel Library replacing this foreach with Parallel.ForEach.

Not working šŸ˜¢:

InternalCreateBuffer can only be called from the main thread.
Constructors and field initializers will be executed from the loading thread when loading a scene.
Don't use this function in the constructor or field initializers, instead move initialization code to the Awake or Start function.
UnityEngine.Networking.UnityWebRequest:Get(String)
Mapbox.Unity.HTTPRequest:.ctor(MonoBehaviour, String, Action`1) (at Assets/Mapbox/Core/Unity/HTTPRequest.cs:21)
Mapbox.Unity.FileSource:Request(String, Action`1) (at Assets/Mapbox/Core/Unity/FileSource.cs:78)
Mapbox.Map.Tile:Initialize(Parameters, Action) (at Assets/Mapbox/Core/Map/Tile.cs:87)
Mapbox.Map.<Update>c__AnonStorey0:<>m__1(CanonicalTileId) (at Assets/Mapbox/Core/Map/Map.cs:246)
System.Threading.Tasks.ThreadPoolTaskScheduler:TaskExecuteWaitCallback(Object)
wilhelmberg commented 7 years ago

Interesting, above error occurs with Slippy example but not with VoxelWorld. Looking for differences ....

wilhelmberg commented 7 years ago

Argh, VoxelWorld requests only one tile (=> no additional threads), that's why it works.

david-rhodes commented 7 years ago

@BergWerkGIS This may be helpful: http://blog.theknightsofunity.com/using-threads-unity/

I don't know how this Task Parallel Library works, but we may have to create our own thread and control what tasks get sent to it. Unity has a lot of oddities when dealing with threads. @parahunter might be able to help here, too.

wilhelmberg commented 7 years ago

@david-rhodes thanks, also tried this (same error):

            foreach (var id in cover)
            {
                Tile.Parameters param;
                param.Id = id;
                param.MapId = this.mapId;
                param.Fs = this.fs;

                Thread worker = new Thread(getTile);
                worker.IsBackground = true;
                worker.Start(param);
            }

Reading your link now ...

david-rhodes commented 7 years ago

@BergWerkGIS, @parahunter suggested that the issue might be using native Unity objects that are not thread safe, such as WWW. Perhaps we start by adding another implementation of web requests before attempting to thread?

wilhelmberg commented 7 years ago

@david-rhodes I now know what the problem is, it's the same like with updating those good ol' WinForms from another thread.

Slippy example works with UnityToolbag.Dispatcher from your comment above and this quick'n'dirty hack to Core/Map.cs:

            foreach (var id in cover)
            {
                Tile.Parameters param;
                param.Id = id;
                param.MapId = this.mapId;
                param.Fs = this.fs;

                Thread worker = new Thread(getTile);
                worker.IsBackground = true;
                worker.Start(param);
            }
        private void getTile(object objParam)
        {
            Tile.Parameters param = (Tile.Parameters)objParam;
            UnityEngine.Debug.LogFormat("{0}: creating T()", param.Id);

            if ("0/0/0" == param.Id.ToString())
            {
                UnityEngine.Debug.LogFormat("{0}: aborting", param.Id);
                return;
            }
            var tile = new T();

            UnityToolbag.Dispatcher.InvokeAsync(() =>
            {
                UnityEngine.Debug.LogFormat("{0}: tile.Initialize", param.Id);
                tile.Initialize(param, () => { this.NotifyNext(tile); });
                UnityEngine.Debug.LogFormat("{0}: AFTER tile.Initialize", param.Id);

                this.tiles.Add(tile);
                UnityEngine.Debug.LogFormat("{0}: NotifyNext", param.Id);
                this.NotifyNext(tile);
            });
        }
david-rhodes commented 7 years ago

@BergWerkGIS There's a note in that link:

Dispatcher is a class that is included in UnityToolbag library. It checks if the code passed to dispatcher is always executed in the main thread, so you can safely execute Unity API code within.

Does this mean our requests are not necessarily threaded, now? Did you profile or check if there's some way to see if the tasks are in fact being threaded properly?

david-rhodes commented 7 years ago

@BergWerkGIS, ss part of this ticket, please keep an eye on whether it makes sense to create multiple FileSource objects. Currently, this is a singleton, but that may not make sense with the final architecture.

wilhelmberg commented 7 years ago

Does this mean our requests are not necessarily threaded, now?

With the snipped above (new Thread(getTile)) requests are threaded. Before they weren't.

Did you profile or check if there's some way to see if the tasks are in fact being threaded properly?

Not really, but the Debug.Logs for different tiles are not in order anymore - I guess that means the request are threaded properly šŸ˜„

ss part of this ticket, please keep an eye on whether it makes sense to create multiple FileSource objects. Currently, this is a singleton, but that may not make sense with the final architecture.

That's what I'm thinking too. From my point of view most of the logic should live in a central place: probably Core/Map.cs as I think requesting just a single tile will be the exception and most users will use a map. So single tiles need less information about themselves.

Map.cs should keep track of:

david-rhodes commented 7 years ago

As per our scrum talk today regarding platform compatibility:

wilhelmberg commented 7 years ago

Please link the reference ticket here about WebGL compatibility

@tmpsantos 's comment near the top of this thread:

We started with co-routines because it was the only way to support the WebPlayer. The interesting thing about the FileSource abstraction is that you could replace it with a ThreadedFileSource if you will never target the WebPlayer.

wilhelmberg commented 7 years ago

Did you profile or check if there's some way to see if the tasks are in fact being threaded properly?

Order threads were created: image

Order tiles arrived: image

wilhelmberg commented 7 years ago

To be continued in #46