diff --git a/src/layers/traffic/controls.js b/src/layers/traffic/controls.js index 060127153..f1e0bd8b9 100644 --- a/src/layers/traffic/controls.js +++ b/src/layers/traffic/controls.js @@ -356,6 +356,7 @@ export function createControls({ state: layerState, services, parts, source }) { fetching: Boolean(layerState._fetching), flowPending: layerState._flowPending, surfaceRefining: Boolean(layerState._surfaceRefining), + surfaceWaiting: Boolean(layerState._surfaceRefineRemove), populationPending: Boolean(layerState._populationPending), heightPending: Boolean(layerState._heightPending), generation: layerState._loadGeneration, diff --git a/src/layers/traffic/flow.js b/src/layers/traffic/flow.js index ce4d1bf86..16a34c8b6 100644 --- a/src/layers/traffic/flow.js +++ b/src/layers/traffic/flow.js @@ -277,6 +277,7 @@ export function createFlow({ state: layerState, services, parts, source }) { layerState._surfaceRefining = false; layerState._surfaceRefineRemove?.(); layerState._surfaceRefineRemove = null; + layerState._surfaceRefineDeadline = null; } if (!roads.length) { // A streamed tile with no roads changes nothing on screen. A completed @@ -557,6 +558,7 @@ export function createFlow({ state: layerState, services, parts, source }) { // without leaving offscreen or unavailable stations loading forever. const retryPending = prepared.pending.length > 0 && + (!refining || performance.now() < layerState._surfaceRefineDeadline) && (!refining || !trafficSurfaceReady(engine) || prepared.metrics.resolvedSamples > 0); @@ -570,6 +572,8 @@ export function createFlow({ state: layerState, services, parts, source }) { (!refining && ready.some((road) => road.surfaceSettled === false))) ) { layerState._surfaceRefineRemove?.(); + // A ready frame can start another pass; it must not renew the budget. + layerState._surfaceRefineDeadline ??= performance.now() + 10000; const stop = () => { remove(); clearTimeout(timer); @@ -609,10 +613,13 @@ export function createFlow({ state: layerState, services, parts, source }) { layerState._surfaceRefining = false; }); }); - const timer = setTimeout(() => { - stop(); - if (current()) layerState._surfacePending = 0; - }, 10000); + const timer = setTimeout( + () => { + stop(); + if (current()) layerState._surfacePending = 0; + }, + Math.max(0, layerState._surfaceRefineDeadline - performance.now()), + ); layerState._surfaceRefineRemove = stop; engine.render.request('traffic-surface'); } diff --git a/src/layers/traffic/handoff.test.mjs b/src/layers/traffic/handoff.test.mjs index eb000ef4b..c9de578d2 100644 --- a/src/layers/traffic/handoff.test.mjs +++ b/src/layers/traffic/handoff.test.mjs @@ -306,6 +306,7 @@ test('a completed surface defers unavailable roads until the next view instead o }); test('the surface wait deadline defers pending roads when the mesh never settles', async (t) => { + t.mock.method(performance, 'now', () => 0); const surface = incomingGlobe(t); surface.frame(); const { state, flow } = fixture(surface); @@ -336,6 +337,45 @@ test('the surface wait deadline defers pending roads when the mesh never settles assert.equal(state._surfaceRefineRemove, null); }); +test('successive surface-ready frames cannot restart the refinement deadline', async (t) => { + const surface = incomingGlobe(t); + surface.ready = true; + surface.frame(); + const { state, flow } = fixture(surface); + t.after(() => { + state._activeFetchAbort.abort(); + state._surfaceRefineRemove?.(); + }); + let now = 0; + t.mock.method(performance, 'now', () => now); + surface.engine.fake.script('terrainHeight', (point) => { + if (geo.math.toDegrees(point.latitude) <= LAT + 0.004) return GROUND; + // A ready frame can be followed by more tile work during a surface pass. + surface.ready = false; + surface.frame(); + return undefined; + }); + const roads = [road(0, GROUND), road(0.008, GROUND)]; + await flow.applyFlowThenRender( + roads, + { ...bounds, coverage }, + 1, + 1800, + 'Loaded full', + ); + assert.equal(state._surfacePending, 1); + for (let pass = 0; pass < 6 && state._surfaceRefineRemove; pass++) { + now += 2000; + surface.ready = true; + surface.frame(); + await settle(() => !state._surfaceRefining); + } + assert.equal(state._surfacePending, 0, 'the whole refinement has a deadline'); + assert.equal(state._surfaceRefining, false); + assert.equal(state._surfaceRefineRemove, null); + assert.equal(roads[1].surfaceReady, false, 'missing heights stay hidden'); +}); + test('resuming the viewport republishes the retained roads as a pass that can still refine', () => { const labels = []; const state = createState({ services: {} });