From 1b9bb844e69cf6da456fcfb6646a8a965a236240 Mon Sep 17 00:00:00 2001 From: Sameh Khamis Date: Sun, 8 Nov 2026 14:02:35 -0800 Subject: [PATCH] revert(cockpit): no water read timeout, and loaded reads not raise-only Both reviews of the timeout found that Cesium cannot cancel a full-detail read: each one given up kept its nine picks traversing every frame, so stalled tiles piled up a new set every 30 s, while slow but valid answers were dropped. Raise-only loaded reads then locked in a wrong-high height. Back to one full-detail read at a time whose answer always applies, and loaded reads that may adjust a river height but never take it to the sea; a coarse probe can again lower it partway until the full read lands. Keeps the test for another ship selected inside the double-click memory, and corrects CURRENT-STATE: a failed read keeps the height it had. --- docs/CURRENT-STATE.md | 7 ++--- src/cockpitSubjects.js | 7 ----- src/ui/cockpitBridge.test.mjs | 53 ----------------------------------- src/ui/cockpitCamera.js | 47 ++++++++++--------------------- 4 files changed, 18 insertions(+), 96 deletions(-) diff --git a/docs/CURRENT-STATE.md b/docs/CURRENT-STATE.md index 91a1f8028..c30a25840 100644 --- a/docs/CURRENT-STATE.md +++ b/docs/CURRENT-STATE.md @@ -1574,10 +1574,9 @@ the full-detail read replaces it; it repeats every 5 s. Only one full-detail read runs at a time: while one is still running, a refresh reads the loaded tiles instead, and the slow read applies whenever it lands. Once a full-detail read has answered, loaded-tile reads no longer change the height; -before then they may only raise it. A read that sampled nothing keeps the -height it had, and a full-detail read still running after 30 s -(`VESSEL_WATERLINE_READ_TIMEOUT_MS`) is given up for a fresh one. A failed read keeps -the sea surface. +before then they may find raised water but never take it back to the sea. +A read that sampled nothing, or failed, keeps the height it had and is +tried again on the next refresh. The HUD keeps its frame, vision styles, briefing, weather and Display/Radio utilities. It names the ship, reads `{type} · LIVE AIS · EYE {height}`, shows diff --git a/src/cockpitSubjects.js b/src/cockpitSubjects.js index aff25b680..aab08ff72 100644 --- a/src/cockpitSubjects.js +++ b/src/cockpitSubjects.js @@ -38,13 +38,6 @@ export const VESSEL_WATERLINE_PROBE_COUNT = 8; /** How often the bridge reads the water under its ship again, ms. */ export const VESSEL_WATERLINE_REFRESH_MS = 5000; -/** - * A full-detail water read still running after this long is given up and - * a fresh one started, ms: tile requests can stall, and one read that never - * lands would otherwise hold the height for the rest of the bridge. - */ -export const VESSEL_WATERLINE_READ_TIMEOUT_MS = 30000; - /** A surface less than this above sea level is the sea itself, metres. */ export const VESSEL_WATERLINE_MIN_RISE_M = 2; diff --git a/src/ui/cockpitBridge.test.mjs b/src/ui/cockpitBridge.test.mjs index d5b4d2789..7c4ad4b1a 100644 --- a/src/ui/cockpitBridge.test.mjs +++ b/src/ui/cockpitBridge.test.mjs @@ -1376,59 +1376,6 @@ test('before any full read lands, a coarse loaded read keeps the river it found' ship.viewer.scene.sampleHeight = () => 0.5; frames(ship.cockpit, clock, ship.shots, 240); near(eyeHeightM(ship.shots.at(-1)) - seaEyeM, 27, 'still on the river', 0.5); - // Nor partway down: a probe on a 15 m bank is not the river either. - ship.viewer.scene.sampleHeight = () => 15; - frames(ship.cockpit, clock, ship.shots, 240); - near(eyeHeightM(ship.shots.at(-1)) - seaEyeM, 27, 'not lowered partway', 0.5); -}); - -test('a full water read that never lands is given up for a fresh one', async (t) => { - const clock = manualClock(t, START_MS); - const sea = await bridgeAt(t, clock); - frames(sea.cockpit, clock, sea.shots, 60); - const seaEyeM = eyeHeightM(sea.shots.at(-1)); - sea.cockpit.exit(); - - clock.set(START_MS); - const record = { ...RECORD, lastPositionEpoch: START_MS / 1000 }; - const ship = fixture(t, { record }); - const reads = []; - ship.viewer.scene.sampleHeightMostDetailed = (positions) => - new Promise((resolve) => reads.push({ positions, resolve })); - // Coarse tiles first put the ship 27 m up; it is really at sea. - ship.viewer.scene.sampleHeight = () => 27; - ship.cockpit.enter(); - await settle(); - ship.cockpit.update(); - await settle(); - assert.equal(reads.length, 1); - // The first read stalls. Twenty seconds on, still no second read. - frames(ship.cockpit, clock, ship.shots, 400); - await settle(); - assert.equal(reads.length, 1, 'not stacked while it may still land'); - // Past the timeout, a fresh read starts, and its answer applies. - frames(ship.cockpit, clock, ship.shots, 300); - await settle(); - assert.equal(reads.length, 2, 'a fresh read after the stalled one'); - const answer = (read, height) => - read.resolve( - read.positions.map((position) => Object.assign(position, { height })), - ); - answer(reads[1], 0.5); - await settle(); - // The eye eases down; give it time to settle. - frames(ship.cockpit, clock, ship.shots, 400); - near(eyeHeightM(ship.shots.at(-1)), seaEyeM, 'back on the sea', 0.5); - // The stalled read landing now changes nothing. - answer(reads[0], 27); - await settle(); - frames(ship.cockpit, clock, ship.shots, 60); - near( - eyeHeightM(ship.shots.at(-1)), - seaEyeM, - 'the given-up read is dropped', - 0.5, - ); }); test('a client clock minutes fast does not age the bridge into a stale feed', async (t) => { diff --git a/src/ui/cockpitCamera.js b/src/ui/cockpitCamera.js index e3efdfa99..d5c268b35 100644 --- a/src/ui/cockpitCamera.js +++ b/src/ui/cockpitCamera.js @@ -9,7 +9,6 @@ import { } from '../cockpitMath.js'; import { VESSEL_DEAD_RECKON_MAX_S, - VESSEL_WATERLINE_READ_TIMEOUT_MS, VESSEL_WATERLINE_REFRESH_MS, deadReckonVesselFix, vesselCameraProfile, @@ -111,11 +110,10 @@ export function update() { * every frame until it settles): while one is still running, a refresh * reads the loaded tiles instead, and the slow read applies whenever it * lands. Once a full-detail read has answered, only full-detail reads - * change the height; before then a loaded-tile read may only raise it, so - * one coarse probe cannot drop a river bridge, and a read that sampled - * nothing keeps the height it had. A full-detail read still running after - * VESSEL_WATERLINE_READ_TIMEOUT_MS is given up for a fresh one, so a - * stalled read cannot hold a wrong height for the whole bridge. + * change the height; before then a loaded-tile read may find raised water + * but never takes a raised height back to the sea. One coarse probe cannot + * drop a river bridge to the sea, and a read that sampled nothing keeps + * the height it had. * @param {{latitude: number, longitude: number}} [at] Where to read the * water: the dead-reckoned position the camera rides. * @returns {number} Ellipsoidal metres. @@ -142,49 +140,35 @@ function vesselSurfaceM(info, nowMs, at = info) { // Whether a full-detail read has answered for this ship. landed: false, inFlight: false, - // The running full-detail read: its number and start time. - attempt: 0, - startedMs: 0, }; this.vesselWaterline = read; } else { read.sampledMs = nowMs; if (read.inFlight && !read.landed) { const loadedM = loadedWaterlineM(scene, probes, seaM); - // Lowering the water is the full-detail read's to say. - if ( - Number.isFinite(loadedM) && - !(Number.isFinite(read.heightM) && loadedM < read.heightM) - ) - read.heightM = loadedM; + // Taking a river back to the sea is the full-detail read's to say. + const lowers = + Number.isFinite(read.heightM) && + read.heightM > seaM && + !(loadedM > seaM); + if (Number.isFinite(loadedM) && !lowers) read.heightM = loadedM; } - if ( - read.inFlight && - nowMs - read.startedMs >= VESSEL_WATERLINE_READ_TIMEOUT_MS - ) - read.inFlight = false; } - if (!read.inFlight) - readWaterline.call(this, read, scene, probes, seaM, nowMs); + if (!read.inFlight) readWaterline.call(this, read, scene, probes, seaM); } return read && Number.isFinite(read.heightM) ? Math.max(seaM, read.heightM) : seaM; } -/** - * Start one full-detail water read for `read`; its answer lands on it - * unless it was given up for a newer read. - */ -function readWaterline(read, scene, probes, seaM, nowMs) { - const attempt = ++read.attempt; +/** Start one full-detail water read for `read`; its answer lands on it. */ +function readWaterline(read, scene, probes, seaM) { read.inFlight = true; - read.startedMs = nowMs; Promise.resolve() .then(() => scene.sampleHeightMostDetailed(probes)) .then( (samples) => { - if (this.vesselWaterline !== read || attempt !== read.attempt) return; + if (this.vesselWaterline !== read) return; read.inFlight = false; // However late it lands, it applies; one that sampled nothing (tiles // not loaded yet) is no answer and keeps the height already read. @@ -194,8 +178,7 @@ function readWaterline(read, scene, probes, seaM, nowMs) { read.heightM = vesselWaterlineM(seaM, heights); }, () => { - if (this.vesselWaterline === read && attempt === read.attempt) - read.inFlight = false; + if (this.vesselWaterline === read) read.inFlight = false; }, ); }