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

Custom Vector Source (n+1) #9983

Merged
merged 10 commits into from
Nov 22, 2017
Merged

Custom Vector Source (n+1) #9983

merged 10 commits into from
Nov 22, 2017

Conversation

asheemmamoowala
Copy link
Contributor

@asheemmamoowala asheemmamoowala commented Sep 12, 2017

Follow up to #8473 rebased on master and updated for Async Render and other changes.

TODO:

  • API to invalidate LatLngBounds or by tile
  • Use geojson-vt without tile splitting
  • Cancellable tile requests
  • Reuse tile data for overscaled tiles
  • Find more TODOs
  • Tests
  • Android implementation

@asheemmamoowala asheemmamoowala added the ⚠️ DO NOT MERGE Work in progress, proof of concept, or on hold label Sep 12, 2017
@kkaefer kkaefer added the Core The cross-platform C++ core, aka mbgl label Sep 13, 2017
@asheemmamoowala asheemmamoowala force-pushed the ahm-custom-vector-source branch 3 times, most recently from 2415987 to 56150de Compare October 3, 2017 00:43
@asheemmamoowala
Copy link
Contributor Author

Using the Actor model to enable asynchronous communication to the RenderCustomVectorSource.

To request data

  • CustomTile is initialized with an actor reference to a CustomTileLoader
  • CustomTile invokes CustomTileLoader::fetchTile with an actor-ref to CustomTile::setTileData
  • fetchTile is forwarded to the user-provided function to CustomVectorSource

To set data

  • user-provided function calls CustomVectorSource::setTileData
  • CustomTileLoader::setTileData invokes CustomTile::setTileData

CustomTileLoader helps track the actor-refs to the tiles so that the platform bindings don't have to.

@ivovandongen
Copy link
Contributor

@asheemmamoowala Looking good!

The only thing I'm wondering is wether we can skip going back over the main thread to set the tile data through CustomSource::setTileData. I understand hiding the management of the Actor references. However, the providing the actor reference to the tile directly and calling that from the tile fetching operation (b591ed6#diff-83e9a6627797d3a9645fe63cf48f4102R84) might be more efficient? The actor reference itself makes it safe to call from multiple threads, even if the underlying Tile is already gone.

That said, I'm not sure how much overhead orchestrating the tile data setters on the main thread has to begin with.

@asheemmamoowala
Copy link
Contributor Author

@ivovandongen - I wondered about that as well . Given that each tile request results in its own operation in the platform binding - it would take away the need for managing all the tile actors in CustomTileLoader.
Will give it a try

@asheemmamoowala
Copy link
Contributor Author

@ivovandongen - having worked on your suggestion, I remembered setting it up through CustomVectorSource::setTileData so that the tile can be updated at any time after it is created. This prevents the client from having to hang on to the ActorRef<> for each tile and removes the need to ask if a tile needs an update before each frame.

Copy link
Contributor

@ivovandongen ivovandongen left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some small formatting issues

auto tuple = std::make_tuple(tileID.overscaledZ, tileID.wrap, callbackRef);
tileCallbackMap.insert({ tileID.canonical, std::vector<OverscaledIDFunctionTuple>(1, tuple) });
}
else {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

line break

tileCallbackMap.insert({ tileID.canonical, std::vector<OverscaledIDFunctionTuple>(1, tuple) });
}
else {
for(auto iter = tileCallbacks->second.begin(); iter != tileCallbacks->second.end(); iter++) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

space after for

}
else {
for(auto iter = tileCallbacks->second.begin(); iter != tileCallbacks->second.end(); iter++) {
if(std::get<0>(*iter) == tileID.overscaledZ && std::get<1>(*iter) == tileID.wrap ) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

space after if

}

void CustomTileLoader::cancelTile(const OverscaledTileID& tileID) {
if(tileCallbackMap.find(tileID.canonical) != tileCallbackMap.end()) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

space after if

void CustomTileLoader::removeTile(const OverscaledTileID& tileID) {
auto tileCallbacks = tileCallbackMap.find(tileID.canonical);
if (tileCallbacks == tileCallbackMap.end()) return;
for(auto iter = tileCallbacks->second.begin(); iter != tileCallbacks->second.end(); iter++) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

space after for

auto tileCallbacks = tileCallbackMap.find(tileID.canonical);
if (tileCallbacks == tileCallbackMap.end()) return;
for(auto iter = tileCallbacks->second.begin(); iter != tileCallbacks->second.end(); iter++) {
if(std::get<0>(*iter) == tileID.overscaledZ && std::get<1>(*iter) == tileID.wrap ) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

space after if

auto iter = tileCallbackMap.find(tileID);
if (iter == tileCallbackMap.end()) return;
dataCache[tileID] = std::make_unique<mapbox::geojson::geojson>(std::move(data));
for(auto tuple : iter->second) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

space after for

necessity = newNecessity;
if (necessity == Necessity::Required) {
loader.invoke(&style::CustomTileLoader::fetchTile, id, actor.self());
} else if(!isRenderable()) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

space after if

@asheemmamoowala asheemmamoowala removed the ⚠️ DO NOT MERGE Work in progress, proof of concept, or on hold label Oct 18, 2017
@jfirebaugh jfirebaugh mentioned this pull request Oct 24, 2017
9 tasks
@@ -111,7 +45,7 @@ extern MGL_EXPORT const MGLShapeSourceOption MGLShapeSourceOptionSimplificationT
```
*/
MGL_EXPORT
@interface MGLShapeSource : MGLSource
@interface MGLShapeSource : MGLAbstractShapeSource
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This PR is scheduled to land in iOS SDK v4.0.0, which allows backwards-incompatible changes. That means we can revisit the taxonomy proposed in #6940 (comment). Specifically, we have the opportunity to choose a name for MGLAbstractShapeSource that doesn’t contain the word “abstract” and rename the MGLShapeSourceOptions to align with whatever class they’re used with. Any ideas?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about a single MGLShapeSource with a data source that provides either tiled or non-tiled data. It would combine the existing MGLShapeSource and MGLProgrammaticShapeSource.
I don't have a good idea of what the peer Source class for a GeoJSON source would be in this scenario - maybe it makes room for an actual MGLGeoJSONSource, but I suspect that was removed earlier for some very valid argument.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like this idea – would we combine GeoJSONSource and CustomSource into a single class to facilitate this simplification?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That may be a future state, where GeoJSONSource is rebuilt on top of CustomVectorSource, but its not in the current plan

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suppose that means MGLShapeSource would need to hold a variant<GeoJSONSource, CustomSource>? More reason to rename CustomSource to something more descriptive…

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Landed on using CustomGeometrySource in core and android to match other uses of custom.

* Deprecated `+[MGLStyle trafficDayStyleURL]` and `+[MGLStyle trafficNightStyleURL]` with no replacement method. To use the Traffic Day and Traffic Night styles going forward, we recommend that you use the underlying URL. ([#9918](https://github.com/mapbox/mapbox-gl-native/pull/9918))
* Fixed an issue where stale (but still valid) map data could be ignored in offline mode. ([#10012](https://github.com/mapbox/mapbox-gl-native/pull/10012))

## 0.5.0
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks like a bad merge.

@asheemmamoowala asheemmamoowala force-pushed the ahm-custom-vector-source branch 4 times, most recently from a626b32 to 3ea2155 Compare November 1, 2017 23:40
@asheemmamoowala
Copy link
Contributor Author

asheemmamoowala commented Nov 2, 2017

Alll planned work for this PR is complete. Here's a recap of what's changed:

Core

CustomGeometrySource is a new source type that accepts callbacks for fetching and canceling tiles. Both callbacks will be called from worker threads, not the main thread; thus preventing the main thread bandwidth from hampering fetching of tiles. @ivovandongen, this addresses your earlier comment.

  • CustomGeometrySource::setTileData(CanonicalTileID, GeoJSON) with the clipped and zoom-appropriate geometry for that tile. This method can be called from the main thread or a worker thread.
  • CustomGeometrySource::invalidateTile(CanonicalTileID): invalidates a specific tile.
  • CustomGeometrySource::invalidateBounds(LatLngBounds): invalidates all tiles that occupy the given bounds at all zoom levels. tileCover turned out to be inappropriate to compute the list of tiles affected as it needed to run at every zoom level. Instead, the existing cached tiles are compared to the provided bounds for an intersection, containment, or overlap.
    CustomTileLoader manages requests from tiles. It maintains a cache of tile data set via CustomGeometrySource::setTileData(..) so as to avoid repeated calls for the same tile for wrapped or over-zoomed tiles.

iOS, macOS

MGLComputedShapeSource, a wrapper for the core source type, accepts an MGLComputedShapeSourceDataSource, which is responsible for providing the tile data. The data source methods will be invoked on worker threads, not the main thread.

Android

CustomGeometrySource, a wrapper for core source type, accepts a GeometryTileProvider which is responsible for providing the tile data. The tile provider method(s) will be invoked on worker threads, not the main thread.

Node, Qt

N/A

Synchronization

CustomTileLoader runs on a shared thread pool, and all invocations to it are via actors. This includes API requests from CustomGeometrySource and data requests from CustomGeometryTile.
In the SDK bindings, fetchTile requests are queued and executed on worker threads using NSOperationQueue, and ThreadPoolExecutor in iOS/macOS and Android respectively.

Tail work

Future Work

/cc @jfirebaugh @tobrun @ivovandongen @kkaefer @1ec5

@1ec5
Copy link
Contributor

1ec5 commented Nov 3, 2017

MGLComputedShapeSource, a wrapper for the core source type, accepts an MGLComputedShapeSourceDataSource, which is responsible for providing the tile data. The data source methods will be invoked on worker threads, not the main thread.

As in #6940 (comment) and #7485 (comment), I continue to fear that calling the callback on a background queue will be a trap for Objective-C and Swift developers, who are accustomed to doing anything UI-related on the main queue. A developer’s first instinct will be to make their view controller class the data source and have the MGLComputedShapeSourceDataSource method implements refer to instance variables, which will lead to concurrency trouble.

That said, I understand the performance considerations that led to this approach. Hopefully the presence of NSOperationQueue will point more savvy developers toward correctly isolating the data source method implementations into a separate class, but the asynchronicity does significantly diminish this feature’s attractiveness as part of the solution for #6181.

@JesseCrocker
Copy link
Contributor

I agree that calling the callback on a background queue could be confusing to some developers, though i think this will end up being a feature that beginning developers probably wont find themselves needing, and the main thread checker in xcode9 makes the kinds of errors that it could lead to far more obvious. I think if this is changed to calling the callback on the main thread then the callback needs to be passed a callback, not return data, otherwise this feature would be not usable for many uses, including anything that has to fetch data from the network.

@asheemmamoowala
Copy link
Contributor Author

asheemmamoowala commented Nov 3, 2017

A callback on the main thread would be less confusing and result in fewer synchronization errors. But we would lose the advantage of canceling stale tile requests provided by NSOperationQueue. Cancelling requests is necessary for providing smooth interactions (pan/zoom), and animations. Objective-C does not provide any primitives for canceling a callback invocation that has already been invoked.
Using an abstract class that requires inheritance could help communicate the intent of tile data requests being asynchronous tasks:

MGLShapeSourceTileProvider : {
- (NSArray<MGLShape <MGLFeature> *>*)featuresInTileAtX:(NSUInteger)x y:(NSUInteger)y zoomLevel:(NSUInteger)zoomLevel;
}

Copy link
Contributor

@ivovandongen ivovandongen left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@asheemmamoowala I focussed mostly on the Android bindings this time around. Few nit picky things aside, my main concern is the ownership/life-cylces of the core CustomGeometrySource, c++ peer and Java peer.

Right now, the c++ peer owns both the core source and maintains a global ref to the java peer, where I think the core class should own the c++ peer. This way, when the core source is destroyed, the c++ peer can be destroyed, releasing the java object. Right now, it seems that the peer classes are never cleaned up.


import static com.mapbox.mapboxsdk.style.layers.PropertyFactory.lineColor;

public class GridSourceActivity extends AppCompatActivity implements OnMapReadyCallback {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A little hint of JavaDoc would be nice here

@Override
public void onMapReady(@NonNull final MapboxMap map) {
mapboxMap = map;
new Handler(getMainLooper()).post(new Runnable() {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this posted to the main looper? Shouldn't be necessary.

std::lock_guard<std::mutex> lock(dataCacheMutex);
dataCache.erase(tileID);
}
dataCache.erase(tileID);
}

void CustomTileLoader::invalidateRegion(const LatLngBounds& bounds, Range<uint8_t> ) {
for(auto idtuple= tileCallbackMap.begin(); idtuple != tileCallbackMap.end(); idtuple++) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

space after for

/**
* Constructs a LatLngBounds from a Tile identifier.
*/
public static LatLngBounds from(int z, int x, int y) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also document the parameters for public methods please.

*
* @param id the source id
*/
public CustomGeometrySource(String id, GeometryTileProvider provider_) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing GeometryTileProvider JavaDoc

import com.mapbox.mapboxsdk.geometry.LatLngBounds;
import com.mapbox.services.commons.geojson.FeatureCollection;

public interface GeometryTileProvider {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please document public interfaces

* @param id the source id
*/
public CustomGeometrySource(String id, GeometryTileProvider provider_) {
executor = new ThreadPoolExecutor(2, 2, 60, TimeUnit.SECONDS, new ArrayBlockingQueue(80));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing generic classification on ArrayBlockingQueue: <Runnable>

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we ensure proper shutdown of the executor when the source is removed btw? At the moment, the core source might be removed while there are still tasks in the queue, these keep executing until finalisation of the Java object.

@@ -0,0 +1,122 @@
#include "custom_geometry_source.hpp"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The indentation of this file is kinda off.

options,
std::bind(&CustomGeometrySource::fetchTile, this, std::placeholders::_1),
std::bind(&CustomGeometrySource::cancelTile, this, std::placeholders::_1))) ),
javaPeer(_obj.NewGlobalRef(env)) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think this is going to work properly as the Java/c++ peers will never be cleaned up.

As far as I can tell, the java object is created, which creates the c++ peer. The c++ peer holds a global reference to the java peer, ensuring it is not GC'd. Now, the only way to clean up both would be to destroy the c++ peer, which would release the global reference on the Java peer, making it eligible for GC. This in turn would however try to call the destructor of the c++ peer as that is how it has been registered through jni::RegisterNativePeer below.

For this to work, the life-cycle of the the c++ peer should be coupled to the core CustomGeometrySource. When it is destroyed, the c++ peer should be destroyed, releasing the global reference, making the java peer eligible for GC.

@@ -0,0 +1,46 @@
#pragma once
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a left-over after refactoring right?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah - this is definitely a mistake.

*/
public CustomGeometrySource(String id, GeometryTileProvider provider_) {
executor = new ThreadPoolExecutor(2, 2, 60, TimeUnit.SECONDS, new ArrayBlockingQueue(80));
cancelledTileRequests = new HashMap<>();

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is using a HashMap safe here. Seems like it is getting modified by multiple threads, unless there is always only one WorkerThread. setTileData is not marked as a WorkerThread.

Since the class is holding an AtomicBoolean it looks like it is intended to be used by multiple threads.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for catching this @mkrussel77. HashMap will not work here and will need to use a thread-safe option like ConcurrentHashMap

@celoftis
Copy link

celoftis commented Nov 29, 2017

I'm researching mapping APIs for offline maptile capability. This feature would be great to have.

@j1m1y
Copy link

j1m1y commented Feb 1, 2018

this sdk version can load .mbtiles from local storage/sdcard ?

@1ec5
Copy link
Contributor

1ec5 commented Feb 3, 2018

This PR alone doesn’t implement MBTiles support. Built-in MBTiles support is being tracked in #6862.

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
Core The cross-platform C++ core, aka mbgl
Projects
None yet
Development

Successfully merging this pull request may close these issues.

8 participants