Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow limiting number of concurrent vector tile requests to prevent exhaustion of browser resources #13247

Draft
wants to merge 32 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
6606cbb
pulling code from previous fork
jobblefrobble Jul 11, 2024
cbf89f8
add prepare script so that repo can be installed through npm directly
jobblefrobble Jul 11, 2024
6bafba7
run lint
jobblefrobble Jul 11, 2024
04bef7f
whitespace
jobblefrobble Jul 11, 2024
404e9e1
naming
jobblefrobble Jul 11, 2024
b258721
add types and map request cancellation to call from queue
jobblefrobble Jul 11, 2024
a5322e9
extra typing
jobblefrobble Jul 12, 2024
a467752
use map rather than array for queue
jobblefrobble Jul 12, 2024
b579175
use factored function
jobblefrobble Jul 12, 2024
96cb51b
adding some debug logs
jobblefrobble Jul 12, 2024
41c4401
extra logs around cancellation
jobblefrobble Jul 12, 2024
63244d2
additional logging in worker source
jobblefrobble Jul 12, 2024
f57ab5e
let default cancel handler run for entry
jobblefrobble Jul 12, 2024
b5e0f5c
iterating through callbacks again so that we dont directly call a cal…
jobblefrobble Jul 12, 2024
b11566a
dont process tile if its status is already done
jobblefrobble Jul 12, 2024
251fc8d
make deduped request object an explicit input to vector tile loading,…
jobblefrobble Jul 12, 2024
c1af805
improve arg naming slightly
jobblefrobble Jul 12, 2024
de6cd25
add test for avoiding duplicate requests when loading vector tiles
jobblefrobble Jul 15, 2024
ce1aeea
add test for queue size being enforced
jobblefrobble Jul 15, 2024
aa313b8
add test for queue progressing after processing previous entries
jobblefrobble Jul 15, 2024
774d22f
Merge pull request #2 from continuum-industries/adding-vector-tile-re…
jobblefrobble Jul 15, 2024
41a3dd9
add test for cancelling entry in the queue
jobblefrobble Jul 15, 2024
4b46480
add test for cancelling outside the queue
jobblefrobble Jul 15, 2024
2d45842
factoring common code in tests
jobblefrobble Jul 15, 2024
7e1a094
remove logs
jobblefrobble Jul 15, 2024
67dd847
lint error
jobblefrobble Jul 15, 2024
8b9e950
follow convention in other tests for resolving requests
jobblefrobble Jul 15, 2024
f9f0027
upper casing consts
jobblefrobble Jul 15, 2024
d449ef9
Merge pull request #4 from continuum-industries/get-tests-running-for…
jobblefrobble Jul 15, 2024
19adaaa
Merge branch 'mapbox:main' into sync-to-upstream
jobblefrobble Aug 7, 2024
4c0c168
remove prepare script
jobblefrobble Aug 7, 2024
8384cc4
rename function to remove reference to image
jobblefrobble Aug 7, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
223 changes: 182 additions & 41 deletions src/source/load_vector_tile.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import {VectorTile} from '@mapbox/vector-tile';
import Protobuf from 'pbf';
import {getArrayBuffer} from '../util/ajax';
import assert from "assert";

import type {Callback} from '../types/callback';
import type {RequestedTileParameters} from './worker_source';
import type Scheduler from '../util/scheduler';
import type {Cancelable} from 'src/types/cancelable';

export type LoadVectorTileResult = {
rawData: ArrayBuffer;
Expand All @@ -23,7 +25,29 @@ export type LoadVectorTileResult = {
export type LoadVectorDataCallback = Callback<LoadVectorTileResult | null | undefined>;

export type AbortVectorData = () => void;
export type LoadVectorData = (params: RequestedTileParameters, callback: LoadVectorDataCallback) => AbortVectorData | null | undefined;
export type LoadVectorData = (params: RequestedTileParameters, callback: LoadVectorDataCallback, deduped: DedupedRequest) => AbortVectorData | null | undefined;
export type DedupedRequestInput = {key : string,
metadata: any,
requestFunc: any,
callback: LoadVectorDataCallback,
fromQueue?: boolean
};
export type VectorTileQueueEntry = DedupedRequestInput & {
cancelled: boolean,
cancel: () => void
};

let requestQueue: Map<string, VectorTileQueueEntry>, numRequests: number;
export const resetRequestQueue = () => {
requestQueue = new Map();
numRequests = 0;
};
resetRequestQueue();

const filterQueue = (key: string) => {
requestQueue.delete(key);
};

export class DedupedRequest {
entries: {
[key: string]: any;
Expand All @@ -35,62 +59,153 @@ export class DedupedRequest {
this.scheduler = scheduler;
}

request(key: string, metadata: any, request: any, callback: LoadVectorDataCallback): () => void {
const entry = this.entries[key] = this.entries[key] || {callbacks: []};
addToSchedulerOrCallDirectly({
callback,
metadata,
err,
result,
}: {
callback: LoadVectorDataCallback;
metadata: any;
err: Error | null | undefined;
result: any;
}) {
if (this.scheduler) {
this.scheduler.add(() => {
callback(err, result);
}, metadata);
} else {
callback(err, result);
}
}

getEntry = (key: string) => {
return (
this.entries[key] || {
// use a set to avoid duplicate callbacks being added when calling from queue
callbacks: new Set(),
}
);
};

request({key, metadata, requestFunc, callback, fromQueue}: DedupedRequestInput): Cancelable {
const entry = (this.entries[key] = this.getEntry(key));

const removeCallbackFromEntry = ({key, requestCallback}) => {
const entry = this.getEntry(key);
if (entry.result) {
return;
}
entry.callbacks.delete(requestCallback);
if (entry.callbacks.size) {
return;
}
if (entry.cancel) {
entry.cancel();
}
filterQueue(key);
delete this.entries[key];
};

let advanced = false;
const advanceRequestQueue = () => {
if (advanced) {
return;
}
advanced = true;
numRequests--;
assert(numRequests >= 0);
while (requestQueue.size && numRequests < 50) {
const request = requestQueue.values().next().value;
const {key, metadata, requestFunc, callback, cancelled} = request;
filterQueue(key);
if (!cancelled) {
request.cancel = this.request({
key,
metadata,
requestFunc,
callback,
fromQueue: true
}).cancel;
}
}
};

if (entry.result) {
const [err, result] = entry.result;
if (this.scheduler) {
this.scheduler.add(() => {
callback(err, result);
}, metadata);
} else {
callback(err, result);
}
return () => {};
this.addToSchedulerOrCallDirectly({
callback,
metadata,
err,
result,
});
return {cancel: () => {}};
}

entry.callbacks.push(callback);
entry.callbacks.add(callback);

const inQueue = requestQueue.has(key);
if ((!entry.cancel && !inQueue) || fromQueue) {
// Lack of attached cancel handler means this is the first request for this resource
if (numRequests >= 50) {
const queued = {
key,
metadata,
requestFunc,
callback,
cancelled: false,
cancel() {},
};
const cancelFunc = () => {
queued.cancelled = true;
removeCallbackFromEntry({
key,
requestCallback: callback,
});
};
queued.cancel = cancelFunc;
requestQueue.set(key, queued);
return queued;
}
numRequests++;

if (!entry.cancel) {
entry.cancel = request((err, result) => {
const actualRequestCancel = requestFunc((err, result) => {
entry.result = [err, result];

for (const cb of entry.callbacks) {
if (this.scheduler) {
this.scheduler.add(() => {
cb(err, result);
}, metadata);
} else {
cb(err, result);
}
this.addToSchedulerOrCallDirectly({
callback: cb,
metadata,
err,
result,
});
}
setTimeout(() => delete this.entries[key], 1000 * 3);

filterQueue(key);
advanceRequestQueue();

setTimeout(() => {
delete this.entries[key];
}, 1000 * 3);
});
entry.cancel = actualRequestCancel;
}

return () => {
if (entry.result) return;
entry.callbacks = entry.callbacks.filter(cb => cb !== callback);
if (!entry.callbacks.length) {
entry.cancel();
delete this.entries[key];
}
return {
cancel() {
removeCallbackFromEntry({
key,
requestCallback: callback,
});
},
};
}
}

/**
* @private
*/
export function loadVectorTile(
params: RequestedTileParameters,
callback: LoadVectorDataCallback,
skipParse?: boolean,
): () => void {
const key = JSON.stringify(params.request);
const makeArrayBufferHandler = ({requestParams, skipParse}) => {

const makeRequest = (callback: LoadVectorDataCallback) => {
const request = getArrayBuffer(params.request, (err?: Error | null, data?: ArrayBuffer | null, cacheControl?: string | null, expires?: string | null) => {
const request = getArrayBuffer(requestParams, (err?: Error | null, data?: ArrayBuffer | null, cacheControl?: string | null, expires?: string | null) => {
if (err) {
callback(err);
} else if (data) {
Expand All @@ -108,11 +223,37 @@ export function loadVectorTile(
};
};

return makeRequest;
};

/**
* @private
*/
export function loadVectorTile(
params: RequestedTileParameters,
callback: LoadVectorDataCallback,
deduped: DedupedRequest,
skipParse?: boolean,
providedArrayBufferHandlerMaker?: any
): () => void {
const key = JSON.stringify(params.request);

const arrayBufferCallbackMaker = providedArrayBufferHandlerMaker || makeArrayBufferHandler;
const makeRequest = arrayBufferCallbackMaker({requestParams: params.request, skipParse});

if (params.data) {
// if we already got the result earlier (on the main thread), return it directly
(this.deduped as DedupedRequest).entries[key] = {result: [null, params.data]};
deduped.entries[key] = {result: [null, params.data]};
}

const callbackMetadata = {type: 'parseTile', isSymbolTile: params.isSymbolTile, zoom: params.tileZoom};
return (this.deduped as DedupedRequest).request(key, callbackMetadata, makeRequest, callback);
const dedupedAndQueuedRequest = deduped.request({
key,
metadata: callbackMetadata,
requestFunc: makeRequest,
callback,
fromQueue: false
});

return dedupedAndQueuedRequest.cancel;
}
4 changes: 2 additions & 2 deletions src/source/vector_tile_source.ts
Original file line number Diff line number Diff line change
Expand Up @@ -258,7 +258,7 @@ class VectorTileSource extends Evented<SourceEvents> implements ISource {
// if workers are not ready to receive messages yet, use the idle time to preemptively
// load tiles on the main thread and pass the result instead of requesting a worker to do so
if (!this.dispatcher.ready) {
const cancel = loadVectorTile.call({deduped: this._deduped}, params, (err?: Error | null, data?: LoadVectorTileResult | null) => {
const cancel = loadVectorTile.call({}, params, (err?: Error | null, data?: LoadVectorTileResult | null) => {
if (err || !data) {
done.call(this, err);
} else {
Expand All @@ -270,7 +270,7 @@ class VectorTileSource extends Evented<SourceEvents> implements ISource {
};
if (tile.actor) tile.actor.send('loadTile', params, done.bind(this), undefined, true);
}
}, true);
}, this._deduped, true);
tile.request = {cancel};

} else {
Expand Down
8 changes: 5 additions & 3 deletions src/source/vector_tile_worker_source.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,17 +74,19 @@ class VectorTileWorkerSource extends Evented implements WorkerSource {
*/
loadTile(params: WorkerTileParameters, callback: WorkerTileCallback) {
const uid = params.uid;

const requestParam = params && params.request;
const perf = requestParam && requestParam.collectResourceTiming;

const workerTile = this.loading[uid] = new WorkerTile(params);
workerTile.abort = this.loadVectorData(params, (err, response) => {

const aborted = !this.loading[uid];

delete this.loading[uid];

if (workerTile.status === 'done') {
return;
}

if (aborted || err || !response) {
workerTile.status = 'done';
if (!aborted) this.loaded[uid] = workerTile;
Expand Down Expand Up @@ -134,7 +136,7 @@ class VectorTileWorkerSource extends Evented implements WorkerSource {

this.loaded = this.loaded || {};
this.loaded[uid] = workerTile;
});
}, this.deduped);
}

/**
Expand Down
Loading