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

Commit

Permalink
[android] shutting down thread pool of the CustomGeometrySource when …
Browse files Browse the repository at this point in the history
…the source is destroyed
  • Loading branch information
LukasPaczos committed Aug 6, 2018
1 parent 2c5bebb commit abbb13c
Show file tree
Hide file tree
Showing 8 changed files with 134 additions and 24 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -15,26 +15,31 @@
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;

/**
* Custom Vector Source, allows using FeatureCollections.
*
*/
@UiThread
public class CustomGeometrySource extends Source {
private ExecutorService executor;
public static final String THREAD_PREFIX = "CustomGeom";
public static final int THREAD_POOL_LIMIT = 4;
private static final AtomicInteger poolCount = new AtomicInteger();
private final ExecutorService executor;
private GeometryTileProvider provider;
private final Map<TileID, AtomicBoolean> cancelledTileRequests = new ConcurrentHashMap<>();

/**
* Create a CustomGeometrySource
*
* @param id The source id.
* @param id The source id.
* @param provider The tile provider that returns geometry data for this source.
*/
public CustomGeometrySource(String id, GeometryTileProvider provider) {
Expand All @@ -45,21 +50,31 @@ public CustomGeometrySource(String id, GeometryTileProvider provider) {
* Create a CustomGeometrySource with non-default CustomGeometrySourceOptions.
* <p>Supported options are minZoom, maxZoom, buffer, and tolerance.</p>
*
* @param id The source id.
* @param id The source id.
* @param provider The tile provider that returns geometry data for this source.
* @param options CustomGeometrySourceOptions.
* @param options CustomGeometrySourceOptions.
*/
public CustomGeometrySource(String id, GeometryTileProvider provider, CustomGeometrySourceOptions options) {
super();
this.provider = provider;
executor = Executors.newFixedThreadPool(4);
executor = Executors.newFixedThreadPool(THREAD_POOL_LIMIT, new ThreadFactory() {
final AtomicInteger threadCount = new AtomicInteger();
final int poolId = poolCount.getAndIncrement();

@Override
public Thread newThread(@NonNull Runnable runnable) {
return new Thread(
runnable,
String.format(Locale.US, "%s-%d-%d", THREAD_PREFIX, poolId, threadCount.getAndIncrement()));
}
});
initialize(id, options);
}

/**
* Invalidate previously provided features within a given bounds at all zoom levels.
* Invoking this method will result in new requests to `GeometryTileProvider` for regions
* that contain, include, or intersect with the provided bounds.
* Invalidate previously provided features within a given bounds at all zoom levels.
* Invoking this method will result in new requests to `GeometryTileProvider` for regions
* that contain, include, or intersect with the provided bounds.
*
* @param bounds The region in which features should be invalidated at all zoom levels
*/
Expand All @@ -73,8 +88,8 @@ public void invalidateRegion(LatLngBounds bounds) {
* in new requests to `GeometryTileProvider` for visible tiles.
*
* @param zoomLevel Tile zoom level.
* @param x Tile X coordinate.
* @param y Tile Y coordinate.
* @param x Tile X coordinate.
* @param y Tile Y coordinate.
*/
public void invalidateTile(int zoomLevel, int x, int y) {
checkThread();
Expand All @@ -87,9 +102,9 @@ public void invalidateTile(int zoomLevel, int x, int y) {
* background threads.
*
* @param zoomLevel Tile zoom level.
* @param x Tile X coordinate.
* @param y Tile Y coordinate.
* @param data Feature collection for the tile.
* @param x Tile X coordinate.
* @param y Tile Y coordinate.
* @param data Feature collection for the tile.
*/
public void setTileData(int zoomLevel, int x, int y, FeatureCollection data) {
checkThread();
Expand Down Expand Up @@ -140,7 +155,12 @@ private void fetchTile(int z, int x, int y) {
TileID tileID = new TileID(z, x, y);
cancelledTileRequests.put(tileID, cancelFlag);
GeometryTileRequest request = new GeometryTileRequest(tileID, provider, this, cancelFlag);
executor.execute(request);

synchronized (executor) {
if (!executor.isShutdown()) {
executor.execute(request);
}
}
}

@WorkerThread
Expand All @@ -152,6 +172,13 @@ private void cancelTile(int z, int x, int y) {
}
}

@Keep
private void releaseThreads() {
synchronized (executor) {
executor.shutdownNow();
}
}

private static class TileID {
public int z;
public int x;
Expand All @@ -164,7 +191,7 @@ public TileID(int _z, int _x, int _y) {
}

public int hashCode() {
return Arrays.hashCode(new int[]{z, x, y});
return Arrays.hashCode(new int[] {z, x, y});
}

public boolean equals(Object object) {
Expand All @@ -177,7 +204,7 @@ public boolean equals(Object object) {
}

if (object instanceof TileID) {
TileID other = (TileID)object;
TileID other = (TileID) object;
return this.z == other.z && this.x == other.x && this.y == other.y;
}
return false;
Expand Down Expand Up @@ -205,7 +232,7 @@ public void run() {

FeatureCollection data = provider.getFeaturesForBounds(LatLngBounds.from(id.z, id.x, id.y), id.z);
CustomGeometrySource source = sourceRef.get();
if (!isCancelled() && source != null && data != null) {
if (!isCancelled() && source != null && data != null) {
source.setTileData(id, data);
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package com.mapbox.mapboxsdk.testapp.style

import android.support.test.espresso.Espresso.onView
import android.support.test.espresso.matcher.ViewMatchers.isRoot
import com.mapbox.mapboxsdk.style.sources.CustomGeometrySource
import com.mapbox.mapboxsdk.style.sources.CustomGeometrySource.THREAD_POOL_LIMIT
import com.mapbox.mapboxsdk.style.sources.CustomGeometrySource.THREAD_PREFIX
import com.mapbox.mapboxsdk.testapp.action.MapboxMapAction.invoke
import com.mapbox.mapboxsdk.testapp.action.OrientationChangeAction.orientationLandscape
import com.mapbox.mapboxsdk.testapp.action.OrientationChangeAction.orientationPortrait
import com.mapbox.mapboxsdk.testapp.activity.BaseActivityTest
import com.mapbox.mapboxsdk.testapp.activity.style.GridSourceActivity
import com.mapbox.mapboxsdk.testapp.activity.style.GridSourceActivity.ID_GRID_LAYER
import com.mapbox.mapboxsdk.testapp.activity.style.GridSourceActivity.ID_GRID_SOURCE
import org.junit.Assert
import org.junit.Test

class CustomGeometrySourceTest : BaseActivityTest() {

override fun getActivityClass(): Class<*> = GridSourceActivity::class.java

@Test
fun sourceNotLeakingThreadsTest() {
validateTestSetup()
waitAction(4000)
onView(isRoot()).perform(orientationLandscape())
waitAction(2000)
onView(isRoot()).perform(orientationPortrait())
waitAction(2000)
Assert.assertFalse("Threads should be shutdown when the source is destroyed.",
Thread.getAllStackTraces().keys.filter {
it.name.startsWith(THREAD_PREFIX)
}.count() > THREAD_POOL_LIMIT)
}

@Test
fun threadsShutdownWhenSourceRemovedTest() {
validateTestSetup()
invoke(mapboxMap) { uiController, mapboxMap ->
mapboxMap.removeLayer(ID_GRID_LAYER)
uiController.loopMainThreadForAtLeast(3000)
mapboxMap.removeSource(ID_GRID_SOURCE)
uiController.loopMainThreadForAtLeast(1000)
Assert.assertTrue("There should be no threads running when the source is removed.",
Thread.getAllStackTraces().keys.filter {
it.name.startsWith(CustomGeometrySource.THREAD_PREFIX)
}.count() == 0)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,8 @@
*/
public class GridSourceActivity extends AppCompatActivity implements OnMapReadyCallback {

private static final String ID_GRID_SOURCE = "grid_source";
private static final String ID_GRID_LAYER = "grid_layer";
public static final String ID_GRID_SOURCE = "grid_source";
public static final String ID_GRID_LAYER = "grid_layer";

private MapView mapView;
private MapboxMap mapboxMap;
Expand Down
4 changes: 3 additions & 1 deletion platform/android/src/native_map_view.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -892,7 +892,9 @@ void NativeMapView::removeSource(JNIEnv& env, jni::Object<Source> obj, jlong sou
assert(sourcePtr != 0);

mbgl::android::Source *source = reinterpret_cast<mbgl::android::Source *>(sourcePtr);
source->removeFromMap(env, obj, *map);
if (source->removeFromMap(env, obj, *map)) {
source->releaseJavaPeer();
}
}

void NativeMapView::addImage(JNIEnv& env, jni::String name, jni::Object<Bitmap> bitmap, jni::jfloat scale, jni::jboolean sdf) {
Expand Down
23 changes: 22 additions & 1 deletion platform/android/src/style/sources/custom_geometry_source.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,9 @@ namespace android {
: Source(env, coreSource, createJavaPeer(env), frontend) {
}

CustomGeometrySource::~CustomGeometrySource() = default;
CustomGeometrySource::~CustomGeometrySource() {
releaseThreads();
}

void CustomGeometrySource::fetchTile (const mbgl::CanonicalTileID& tileID) {
android::UniqueEnv _env = android::AttachEnv();
Expand All @@ -78,6 +80,17 @@ namespace android {
peer.Call(*_env, cancelTile, (int)tileID.z, (int)tileID.x, (int)tileID.y);
};

void CustomGeometrySource::releaseThreads() {
android::UniqueEnv _env = android::AttachEnv();

static auto releaseThreads = javaClass.GetMethod<void ()>(*_env, "releaseThreads");

assert(javaPeer);

auto peer = jni::Cast(*_env, *javaPeer, javaClass);
peer.Call(*_env, releaseThreads);
};

void CustomGeometrySource::setTileData(jni::JNIEnv& env,
jni::jint z,
jni::jint x,
Expand Down Expand Up @@ -120,6 +133,14 @@ namespace android {
return jni::Object<Source>(CustomGeometrySource::javaClass.New(env, constructor, reinterpret_cast<jni::jlong>(this)).Get());
}

bool CustomGeometrySource::removeFromMap(JNIEnv& env, jni::Object<Source> source, mbgl::Map& map) {
bool successfullyRemoved = Source::removeFromMap(env, source, map);
if (successfullyRemoved) {
releaseThreads();
}
return successfullyRemoved;
}

void CustomGeometrySource::registerNative(jni::JNIEnv& env) {
// Lookup the class
CustomGeometrySource::javaClass = *jni::Class<CustomGeometrySource>::Find(env).NewGlobalRef(env).release();
Expand Down
3 changes: 3 additions & 0 deletions platform/android/src/style/sources/custom_geometry_source.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,11 @@ class CustomGeometrySource : public Source {

~CustomGeometrySource();

bool removeFromMap(JNIEnv&, jni::Object<Source>, mbgl::Map&) override;

void fetchTile(const mbgl::CanonicalTileID& tileID);
void cancelTile(const mbgl::CanonicalTileID& tileID);
void releaseThreads();
void setTileData(jni::JNIEnv& env, jni::jint z, jni::jint x, jni::jint y, jni::Object<geojson::FeatureCollection> jf);

void invalidateTile(jni::JNIEnv& env, jni::jint z, jni::jint x, jni::jint y);
Expand Down
7 changes: 6 additions & 1 deletion platform/android/src/style/sources/source.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ namespace android {
rendererFrontend = &frontend;
}

void Source::removeFromMap(JNIEnv&, jni::Object<Source>, mbgl::Map& map) {
bool Source::removeFromMap(JNIEnv&, jni::Object<Source>, mbgl::Map& map) {
// Cannot remove if not attached yet
if (ownedSource) {
throw std::runtime_error("Cannot remove detached source");
Expand All @@ -119,6 +119,11 @@ namespace android {
ownedSource = map.getStyle().removeSource(source.getID());

// The source may not be removed if any layers still reference it
return ownedSource != nullptr;
}

void Source::releaseJavaPeer() {
// We can't release the peer if the source was not removed from the map
if (!ownedSource) {
return;
}
Expand Down
4 changes: 3 additions & 1 deletion platform/android/src/style/sources/source.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,9 @@ class Source : private mbgl::util::noncopyable {

void addToMap(JNIEnv&, jni::Object<Source>, mbgl::Map&, AndroidRendererFrontend&);

void removeFromMap(JNIEnv&, jni::Object<Source>, mbgl::Map&);
virtual bool removeFromMap(JNIEnv&, jni::Object<Source>, mbgl::Map&);

void releaseJavaPeer();

jni::String getId(jni::JNIEnv&);

Expand Down

0 comments on commit abbb13c

Please sign in to comment.