mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-06 19:34:01 +00:00
fix(street-level): run at most six Mapillary tile fetches at once
Tiles a camera move leaves behind keep downloading after the client drops them; the cap bounds their memory and quota, as main's proxy did. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
35bfe203a6
commit
c18af6ad1c
3 files changed
+46
-3
No files matched your search
@@ -8,8 +8,8 @@ import {
|
|||||||
} from './mapillary/tiles.js';
|
} from './mapillary/tiles.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Tile requests per client IP per minute, cache hits included: 24 new 25-tile
|
* Tile requests per client IP per minute, cache hits included: over 60 new
|
||||||
* views a minute, more than a person flying the camera reaches.
|
* 9-tile views a minute, more than a person flying the camera reaches.
|
||||||
*/
|
*/
|
||||||
export const TILE_ROUTE_MAX_PER_MIN = 600;
|
export const TILE_ROUTE_MAX_PER_MIN = 600;
|
||||||
/** The longest upstream Retry-After passed on, in seconds. */
|
/** The longest upstream Retry-After passed on, in seconds. */
|
||||||
|
|||||||
@@ -17,6 +17,11 @@ const DROP_LAYERS = ['image'];
|
|||||||
/** A z14 tile with its image layer is ~11 MB; anything past this is wrong. */
|
/** A z14 tile with its image layer is ~11 MB; anything past this is wrong. */
|
||||||
export const TILE_MAX_BYTES = 48 * 1024 * 1024;
|
export const TILE_MAX_BYTES = 48 * 1024 * 1024;
|
||||||
const FETCH_TIMEOUT_MS = 60_000;
|
const FETCH_TIMEOUT_MS = 60_000;
|
||||||
|
/**
|
||||||
|
* Upstream fetches at once; more wait their turn. A client drops the tiles a
|
||||||
|
* camera move leaves behind, but their fetches run on, so this bounds them.
|
||||||
|
*/
|
||||||
|
export const TILE_UPSTREAM_CONCURRENCY = 6;
|
||||||
/** Tiles change only when new imagery is processed. */
|
/** Tiles change only when new imagery is processed. */
|
||||||
export const TILE_TTL_MS = 24 * 60 * 60 * 1000;
|
export const TILE_TTL_MS = 24 * 60 * 60 * 1000;
|
||||||
export const TILE_MEMORY_BUDGET_BYTES = 96 * 1024 * 1024;
|
export const TILE_MEMORY_BUDGET_BYTES = 96 * 1024 * 1024;
|
||||||
@@ -91,6 +96,23 @@ async function fetchUpstream({ z, x, y }) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let upstreamActive = 0;
|
||||||
|
/** @type {Array<() => void>} fetches waiting for a slot, oldest first */
|
||||||
|
const upstreamWaiting = [];
|
||||||
|
|
||||||
|
/** Run `task` once an upstream slot is free. */
|
||||||
|
async function withUpstreamSlot(task) {
|
||||||
|
while (upstreamActive >= TILE_UPSTREAM_CONCURRENCY)
|
||||||
|
await new Promise((resolve) => upstreamWaiting.push(resolve));
|
||||||
|
upstreamActive++;
|
||||||
|
try {
|
||||||
|
return await task();
|
||||||
|
} finally {
|
||||||
|
upstreamActive--;
|
||||||
|
upstreamWaiting.shift()?.();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/** @type {Map<string, {bytes: Buffer, at: number}>} insertion-ordered LRU */
|
/** @type {Map<string, {bytes: Buffer, at: number}>} insertion-ordered LRU */
|
||||||
const memory = new Map();
|
const memory = new Map();
|
||||||
let memoryBytes = 0;
|
let memoryBytes = 0;
|
||||||
@@ -137,7 +159,7 @@ export async function fetchTile(address) {
|
|||||||
inFlight,
|
inFlight,
|
||||||
address.key,
|
address.key,
|
||||||
async () => {
|
async () => {
|
||||||
const bytes = await fetchUpstream(address);
|
const bytes = await withUpstreamSlot(() => fetchUpstream(address));
|
||||||
remember(address.key, bytes);
|
remember(address.key, bytes);
|
||||||
return bytes;
|
return bytes;
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import {
|
|||||||
import {
|
import {
|
||||||
TILE_MAX_BYTES,
|
TILE_MAX_BYTES,
|
||||||
TILE_MEMORY_BUDGET_BYTES,
|
TILE_MEMORY_BUDGET_BYTES,
|
||||||
|
TILE_UPSTREAM_CONCURRENCY,
|
||||||
TILE_TTL_MS,
|
TILE_TTL_MS,
|
||||||
_resetTileCacheForTest,
|
_resetTileCacheForTest,
|
||||||
_tileMemoryForTest,
|
_tileMemoryForTest,
|
||||||
@@ -389,6 +390,26 @@ test('identical concurrent tile requests share one upstream fetch', async () =>
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('at most a few upstream fetches run at once; the rest wait their turn', async () => {
|
||||||
|
let release;
|
||||||
|
const gate = new Promise((resolve) => (release = resolve));
|
||||||
|
await withUpstream(
|
||||||
|
async () => (await gate, ok()),
|
||||||
|
async ({ calls, call }) => {
|
||||||
|
const tiles = TILE_UPSTREAM_CONCURRENCY + 4;
|
||||||
|
const pending = Array.from({ length: tiles }, (_, i) =>
|
||||||
|
call('/api/mapillary/tiles', `/coverage/14/${i}/3`),
|
||||||
|
);
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 10));
|
||||||
|
assert.equal(calls.length, TILE_UPSTREAM_CONCURRENCY);
|
||||||
|
release();
|
||||||
|
const done = await Promise.all(pending);
|
||||||
|
assert.ok(done.every((res) => res.statusCode === 200));
|
||||||
|
assert.equal(calls.length, tiles, 'the waiting ones ran too');
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
test('a tile held in memory past its TTL is fetched again', async (t) => {
|
test('a tile held in memory past its TTL is fetched again', async (t) => {
|
||||||
await withUpstream(
|
await withUpstream(
|
||||||
() => ok(),
|
() => ok(),
|
||||||
|
|||||||
Reference in new issue
Block a user