The core CustomGeometrySource is configured with a "fetch tile function" that glues out into the platform-specific wrappers (and eventually the user-provided custom implementation). The darwin implementation of this function is in MGLComputedShapeSource.mm and the Android implementation is in CustomGeometrySource.java. Both implementations follow a similar pattern:
- Wrap the tile ID up in a "request" object
- Pass that request object off to be run on a shared pool of 4 worker threads
- When processing a request on the worker thread, synchronously call into the user-provided custom source to get data (
id<MGLComputedShapeSourceDataSource> on darwin, GeometryTileProvider on Android), and then once the synchronous call returns, queue an asynchronous setTileData callback that feeds back into the core CustomGeometrySource
I think this model would make sense if the primary job of the user-provided tile provider was to run some computationally expensive operation, but it doesn't seem to me like a very good fit for the case where the tile provider's job involves asynchronously requesting resources itself. Because the provider doesn't have access to a callback, it needs to block on asynchronous requests, which is an awkward interface and also opens the potential for clogging up the 4 worker threads with blocking requests. Also, this forces the provider to support synchronized cross-thread access to itself (a bit of an anti-pattern), even when it's not necessary.
I think it would be better if we leave concurrency choices to the user, so for instance GeometryTileProvider::getFeaturesForBounds would take a callback as an argument (some kind of wrapper on the core ActorRef that's being used for the underlying callback). Then, for someone implementing GeometryTileProvider:
- If the computation is simple, just do it synchronously and call the callback immediately.
- If the computation depends on an asynchronous resource like a network or database request, go ahead and make the request and pass the callback through. Don't worry about threads, that's an implementation detail of your network/database layer.
- If you actually have a lot of CPU work to do and you want to fire up worker threads and give them a queue of tasks, fine go ahead and do it, and it's on you to think about how to manage concurrency.
/cc @asheemmamoowala @jfirebaugh @1ec5 @ivovandongen
The core
CustomGeometrySourceis configured with a "fetch tile function" that glues out into the platform-specific wrappers (and eventually the user-provided custom implementation). The darwin implementation of this function is inMGLComputedShapeSource.mmand the Android implementation is inCustomGeometrySource.java. Both implementations follow a similar pattern:id<MGLComputedShapeSourceDataSource>on darwin,GeometryTileProvideron Android), and then once the synchronous call returns, queue an asynchronoussetTileDatacallback that feeds back into the coreCustomGeometrySourceI think this model would make sense if the primary job of the user-provided tile provider was to run some computationally expensive operation, but it doesn't seem to me like a very good fit for the case where the tile provider's job involves asynchronously requesting resources itself. Because the provider doesn't have access to a callback, it needs to block on asynchronous requests, which is an awkward interface and also opens the potential for clogging up the 4 worker threads with blocking requests. Also, this forces the provider to support synchronized cross-thread access to itself (a bit of an anti-pattern), even when it's not necessary.
I think it would be better if we leave concurrency choices to the user, so for instance
GeometryTileProvider::getFeaturesForBoundswould take a callback as an argument (some kind of wrapper on the coreActorRefthat's being used for the underlying callback). Then, for someone implementingGeometryTileProvider:/cc @asheemmamoowala @jfirebaugh @1ec5 @ivovandongen