From 00cb44ad2cdaf5de320fdc11e0aaac5cbefb3140 Mon Sep 17 00:00:00 2001 From: German Khokhlov Date: Wed, 11 Nov 2026 02:07:36 +0400 Subject: [PATCH] Stop refining traffic surfaces that make no progress An oblique view kept roads whose samples could not resolve from it, and the refinement retried them without end, leaving the layer loading with work pending. A refinement pass that resolves nothing new stops, and work still pending at the existing deadline is dropped. --- src/layers/traffic/flow.js | 20 ++++-- src/layers/traffic/handoff.test.mjs | 95 +++++++++++++++++++++++++++++ src/layers/traffic/surface.js | 2 + src/layers/traffic/surface.test.mjs | 8 ++- 4 files changed, 120 insertions(+), 5 deletions(-) diff --git a/src/layers/traffic/flow.js b/src/layers/traffic/flow.js index fd9a9cec6..ce4d1bf86 100644 --- a/src/layers/traffic/flow.js +++ b/src/layers/traffic/flow.js @@ -552,12 +552,21 @@ export function createFlow({ state: layerState, services, parts, source }) { paint(); } paint(true); - if (current()) layerState._surfacePending = prepared.pending.length; + // A completed-mesh pass with no new heights cannot improve this view. + // Keep the roads in _surfaceInput for a later view or surface change, + // without leaving offscreen or unavailable stations loading forever. + const retryPending = + prepared.pending.length > 0 && + (!refining || + !trafficSurfaceReady(engine) || + prepared.metrics.resolvedSamples > 0); + if (current()) + layerState._surfacePending = retryPending ? prepared.pending.length : 0; if ( current() && !preview && ((!refining && !trafficSurfaceReady(engine)) || - prepared.pending.length || + retryPending || (!refining && ready.some((road) => road.surfaceSettled === false))) ) { layerState._surfaceRefineRemove?.(); @@ -579,7 +588,7 @@ export function createFlow({ state: layerState, services, parts, source }) { stop(); // Revalidate only local samples taken at a coarser rendered LOD. This // background upgrade never holds road acquisition or first paint; - // missing samples keep retrying while refined heights stay cached. + // missing samples retry while heights make progress and stay cached. layerState._surfaceRefining = true; applyFlowThenRender( roads, @@ -600,7 +609,10 @@ export function createFlow({ state: layerState, services, parts, source }) { layerState._surfaceRefining = false; }); }); - const timer = setTimeout(stop, 10000); + const timer = setTimeout(() => { + stop(); + if (current()) layerState._surfacePending = 0; + }, 10000); 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 8e5e53b21..eb000ef4b 100644 --- a/src/layers/traffic/handoff.test.mjs +++ b/src/layers/traffic/handoff.test.mjs @@ -241,6 +241,101 @@ test('a still camera drains pending surface samples across bounded passes using assert.equal(engine.fake.renderRequests, finished, 'settled work is idle'); }); +test('a completed surface defers unavailable roads until the next view instead of refining forever', async (t) => { + const surface = incomingGlobe(t); + surface.ready = true; + surface.frame(); + const { engine } = surface; + const { state, flow } = fixture(surface); + t.after(() => { + state._activeFetchAbort.abort(); + state._surfaceRefineRemove?.(); + }); + const roads = [road(0, GROUND), road(0.008, GROUND)]; + let missing = true; + engine.fake.script('terrainHeight', (point) => + missing && geo.math.toDegrees(point.latitude) > LAT + 0.004 + ? undefined + : GROUND, + ); + await flow.applyFlowThenRender( + roads, + { ...bounds, coverage }, + 1, + 1800, + 'Loaded full', + ); + let rendered = engine.fake.renderRequests; + await settle(() => { + if (engine.fake.renderRequests > rendered || state._surfaceRefineRemove) { + rendered = engine.fake.renderRequests; + surface.frame(); + } + return !state._surfacePending && !state._surfaceRefining; + }); + assert.equal( + state._surfacePending, + 0, + 'unavailable roads are not outstanding work', + ); + assert.equal(state._surfaceRefining, false); + assert.equal(state._surfaceRefineRemove, null); + assert.equal(roads[1].surfaceReady, false, 'no unmeasured road is displayed'); + const finished = engine.fake.renderRequests; + surface.frame(); + await nextTask(); + assert.equal( + engine.fake.renderRequests, + finished, + 'the completed view is idle', + ); + + missing = false; + await flow.applyFlowThenRender( + roads, + { ...bounds, coverage }, + 1, + 1800, + 'Loaded full', + ); + assert.equal( + roads[1].surfaceReady, + true, + 'another view retries the retained road', + ); +}); + +test('the surface wait deadline defers pending roads when the mesh never settles', async (t) => { + const surface = incomingGlobe(t); + surface.frame(); + const { state, flow } = fixture(surface); + t.after(() => { + state._activeFetchAbort.abort(); + state._surfaceRefineRemove?.(); + }); + const schedule = globalThis.setTimeout; + let deadline; + t.mock.method(globalThis, 'setTimeout', (callback, delay, ...args) => { + if (delay === 10000) deadline = callback; + return schedule(callback, delay, ...args); + }); + surface.engine.fake.script('terrainHeight', (point) => + geo.math.toDegrees(point.latitude) > LAT + 0.004 ? undefined : GROUND, + ); + await flow.applyFlowThenRender( + [road(0, GROUND), road(0.008, GROUND)], + { ...bounds, coverage }, + 1, + 1800, + 'Loaded full', + ); + assert.equal(state._surfacePending, 1); + assert.equal(typeof deadline, 'function'); + deadline(); + assert.equal(state._surfacePending, 0); + assert.equal(state._surfaceRefineRemove, null); +}); + test('resuming the viewport republishes the retained roads as a pass that can still refine', () => { const labels = []; const state = createState({ services: {} }); diff --git a/src/layers/traffic/surface.js b/src/layers/traffic/surface.js index 8585d149d..76c6413d1 100644 --- a/src/layers/traffic/surface.js +++ b/src/layers/traffic/surface.js @@ -258,6 +258,7 @@ export async function prepareRoadSurfaces( const metrics = { sampleCount: 0, sampleMs: 0, + resolvedSamples: 0, cacheHits: 0, depthHits: 0, roads: roads.length, @@ -341,6 +342,7 @@ export async function prepareRoadSurfaces( height = validHeight(floor) ? floor : 0; } } else { + metrics.resolvedSamples++; height = validHeight(floor) ? Math.max(sampled, floor) : sampled; cache.heights.delete(key); cache.heights.set(key, { diff --git a/src/layers/traffic/surface.test.mjs b/src/layers/traffic/surface.test.mjs index 21211619f..ce4d95e69 100644 --- a/src/layers/traffic/surface.test.mjs +++ b/src/layers/traffic/surface.test.mjs @@ -89,6 +89,7 @@ test('per-vertex mesh heights follow terrain, reject invalid samples, and leave onMetrics: (value) => (metrics = value), }); assert.equal(metrics.sampleCount, 5); + assert.equal(metrics.resolvedSamples, 2); assert.equal(prepared.pending.length, 1); assert.equal(samples, 5); const actual = r.waypoints.map((p) => Math.round(heightOf(p))); @@ -108,7 +109,12 @@ test('per-vertex mesh heights follow terrain, reject invalid samples, and leave picks++; return 75; }); - await prepareRoadSurfaces([r], engine, ground, []); + const cached = await prepareRoadSurfaces([r], engine, ground, []); + assert.equal( + cached.metrics.resolvedSamples, + 0, + 'cache hits are not progress', + ); assert.equal(picks, 0, 'validated coordinate heights survive cache hits'); const abort = new AbortController(); abort.abort();