[fix](trx-frontend-http): batch decode history replay

Replay decode history in decoder-specific batches instead of feeding every
message through the single-message path.

This reduces per-message array churn and UI scheduling during large history
loads while keeping the existing live decode behavior unchanged.

Co-authored-by: OpenAI Codex <codex@openai.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
This commit is contained in:
2026-03-14 17:56:28 +01:00
parent 4114e0b9fa
commit abae110ecd
7 changed files with 364 additions and 112 deletions
@@ -7325,8 +7325,44 @@ function dispatchDecodeMessage(msg) {
if (msg.type === "wspr" && window.onServerWspr) window.onServerWspr(msg); if (msg.type === "wspr" && window.onServerWspr) window.onServerWspr(msg);
} }
const DECODE_HISTORY_MAX_BATCH = 12; function dispatchDecodeBatch(batch) {
const DECODE_HISTORY_SLICE_BUDGET_MS = 6; if (!Array.isArray(batch) || batch.length === 0) return;
const type = String(batch[0]?.type || "");
const uniformType = batch.every((msg) => String(msg?.type || "") === type);
if (uniformType) {
if (type === "ais" && window.onServerAisBatch) {
window.onServerAisBatch(batch);
return;
}
if (type === "vdes" && window.onServerVdesBatch) {
window.onServerVdesBatch(batch);
return;
}
if (type === "aprs" && window.onServerAprsBatch) {
window.onServerAprsBatch(batch);
return;
}
if (type === "hf_aprs" && window.onServerHfAprsBatch) {
window.onServerHfAprsBatch(batch);
return;
}
if (type === "ft8" && window.onServerFt8Batch) {
window.onServerFt8Batch(batch);
return;
}
if (type === "wspr" && window.onServerWsprBatch) {
window.onServerWsprBatch(batch);
return;
}
}
for (const msg of batch) {
dispatchDecodeMessage(msg);
}
}
const DECODE_HISTORY_MAX_BATCH = 256;
const DECODE_HISTORY_TYPE_BATCH_LIMIT = 192;
const DECODE_HISTORY_SLICE_BUDGET_MS = 10;
function scheduleDecodeHistoryDrainStep(callback) { function scheduleDecodeHistoryDrainStep(callback) {
if (typeof callback !== "function") return; if (typeof callback !== "function") return;
@@ -7343,9 +7379,19 @@ function drainDecodeHistory(buffer, index, onDone, onProgress) {
: 0; : 0;
let nextIndex = index; let nextIndex = index;
while (nextIndex < buffer.length) { while (nextIndex < buffer.length) {
dispatchDecodeMessage(buffer[nextIndex]); const batchStart = nextIndex;
const batchType = String(buffer[nextIndex]?.type || "");
nextIndex += 1; nextIndex += 1;
if (nextIndex - index >= DECODE_HISTORY_MAX_BATCH) break; while (
nextIndex < buffer.length
&& (nextIndex - batchStart) < DECODE_HISTORY_TYPE_BATCH_LIMIT
&& (nextIndex - index) < DECODE_HISTORY_MAX_BATCH
&& String(buffer[nextIndex]?.type || "") === batchType
) {
nextIndex += 1;
}
dispatchDecodeBatch(buffer.slice(batchStart, nextIndex));
if ((nextIndex - index) >= DECODE_HISTORY_MAX_BATCH) break;
if (startedAt > 0 && (performance.now() - startedAt) >= DECODE_HISTORY_SLICE_BUDGET_MS) break; if (startedAt > 0 && (performance.now() - startedAt) >= DECODE_HISTORY_SLICE_BUDGET_MS) break;
} }
if (typeof onProgress === "function") { if (typeof onProgress === "function") {
@@ -343,6 +343,53 @@ function addAisMessage(msg) {
} }
} }
function normalizeServerAisMessage(msg) {
return {
channel: msg.channel,
message_type: msg.message_type,
mmsi: msg.mmsi,
lat: msg.lat,
lon: msg.lon,
sog_knots: msg.sog_knots,
cog_deg: msg.cog_deg,
heading_deg: msg.heading_deg,
vessel_name: msg.vessel_name,
callsign: msg.callsign,
destination: msg.destination,
ts_ms: msg.ts_ms,
};
}
window.onServerAisBatch = function(messages) {
if (!Array.isArray(messages) || messages.length === 0) return;
if (aisStatus) aisStatus.textContent = aisPaused ? "Paused" : "Receiving";
const normalized = [];
for (const msg of messages) {
const next = normalizeServerAisMessage(msg);
const tsMs = Number.isFinite(next.ts_ms) ? Number(next.ts_ms) : Date.now();
next._tsMs = tsMs;
next._ts = new Date(tsMs).toLocaleTimeString([], {
hour: "2-digit",
minute: "2-digit",
second: "2-digit",
});
if (next.lat != null && next.lon != null && window.aisMapAddVessel) {
window.aisMapAddVessel(next);
}
normalized.push(next);
}
normalized.reverse();
aisMessageHistory = normalized.concat(aisMessageHistory);
pruneAisMessageHistory();
scheduleAisBarUpdate();
if (aisPaused) {
aisBufferedWhilePaused += messages.length;
updateAisSummary();
return;
}
scheduleAisHistoryRender();
};
window.pruneAisHistoryView = function() { window.pruneAisHistoryView = function() {
pruneAisMessageHistory(); pruneAisMessageHistory();
updateAisBar(); updateAisBar();
@@ -381,20 +428,7 @@ if (aisFilterInput) {
window.onServerAis = function(msg) { window.onServerAis = function(msg) {
if (aisStatus) aisStatus.textContent = aisPaused ? "Paused" : "Receiving"; if (aisStatus) aisStatus.textContent = aisPaused ? "Paused" : "Receiving";
addAisMessage({ addAisMessage(normalizeServerAisMessage(msg));
channel: msg.channel,
message_type: msg.message_type,
mmsi: msg.mmsi,
lat: msg.lat,
lon: msg.lon,
sog_knots: msg.sog_knots,
cog_deg: msg.cog_deg,
heading_deg: msg.heading_deg,
vessel_name: msg.vessel_name,
callsign: msg.callsign,
destination: msg.destination,
ts_ms: msg.ts_ms,
});
}; };
updateAisSummary(); updateAisSummary();
@@ -400,6 +400,53 @@ function addAprsPacket(pkt) {
scheduleAprsHistoryRender(); scheduleAprsHistoryRender();
} }
function normalizeServerAprsPacket(pkt) {
return {
receiver: window.getDecodeRigMeta ? window.getDecodeRigMeta() : null,
srcCall: pkt.src_call,
destCall: pkt.dest_call,
path: pkt.path,
info: pkt.info,
info_bytes: pkt.info_bytes,
type: pkt.packet_type,
crcOk: pkt.crc_ok,
ts_ms: pkt.ts_ms,
lat: pkt.lat,
lon: pkt.lon,
symbolTable: pkt.symbol_table,
symbolCode: pkt.symbol_code,
};
}
window.onServerAprsBatch = function(packets) {
if (!Array.isArray(packets) || packets.length === 0) return;
aprsStatus.textContent = aprsPaused ? "Paused" : "Receiving";
const normalized = [];
let hasCrcOk = false;
for (const pkt of packets) {
const next = normalizeServerAprsPacket(pkt);
const tsMs = Number.isFinite(next.ts_ms) ? Number(next.ts_ms) : Date.now();
next._tsMs = tsMs;
next._ts = new Date(tsMs).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit", second: "2-digit" });
if (next.lat != null && next.lon != null && window.aprsMapAddStation) {
window.aprsMapAddStation(next.srcCall, next.lat, next.lon, next.info, next.symbolTable, next.symbolCode, next);
}
if (next.crcOk) hasCrcOk = true;
normalized.push(next);
}
normalized.reverse();
aprsPacketHistory = normalized.concat(aprsPacketHistory);
pruneAprsPacketHistory();
if (hasCrcOk) scheduleAprsBarUpdate();
if (aprsPaused) {
aprsBufferedWhilePaused += packets.length;
updateAprsSummary();
updateAprsChipState();
return;
}
scheduleAprsHistoryRender();
};
document.getElementById("aprs-clear-btn").addEventListener("click", async () => { document.getElementById("aprs-clear-btn").addEventListener("click", async () => {
try { try {
await postPath("/clear_aprs_decode"); await postPath("/clear_aprs_decode");
@@ -462,21 +509,7 @@ if (aprsFilterInput) {
// --- Server-side APRS decode handler --- // --- Server-side APRS decode handler ---
window.onServerAprs = function(pkt) { window.onServerAprs = function(pkt) {
aprsStatus.textContent = aprsPaused ? "Paused" : "Receiving"; aprsStatus.textContent = aprsPaused ? "Paused" : "Receiving";
addAprsPacket({ addAprsPacket(normalizeServerAprsPacket(pkt));
receiver: window.getDecodeRigMeta ? window.getDecodeRigMeta() : null,
srcCall: pkt.src_call,
destCall: pkt.dest_call,
path: pkt.path,
info: pkt.info,
info_bytes: pkt.info_bytes,
type: pkt.packet_type,
crcOk: pkt.crc_ok,
ts_ms: pkt.ts_ms,
lat: pkt.lat,
lon: pkt.lon,
symbolTable: pkt.symbol_table,
symbolCode: pkt.symbol_code,
});
}; };
renderAprsHistory(); renderAprsHistory();
@@ -114,6 +114,59 @@ function addFt8Message(msg) {
scheduleFt8HistoryRender(); scheduleFt8HistoryRender();
} }
function normalizeServerFt8Message(msg) {
const raw = (msg.message || "").toString();
const locatorDetails = ft8ExtractLocatorDetails(raw);
const grids = locatorDetails.length > 0
? locatorDetails.map((detail) => detail.grid)
: ft8ExtractAllGrids(raw);
const station = ft8ExtractLikelyCallsign(raw);
const rfHz = normalizeFt8DisplayFreqHz(msg.freq_hz);
return {
raw,
grids,
station,
rfHz,
locatorDetails,
history: {
receiver: window.getDecodeRigMeta ? window.getDecodeRigMeta() : null,
ts_ms: msg.ts_ms,
snr_db: msg.snr_db,
dt_s: msg.dt_s,
freq_hz: Number.isFinite(rfHz) ? rfHz : msg.freq_hz,
message: msg.message,
},
};
}
window.onServerFt8Batch = function(messages) {
if (!Array.isArray(messages) || messages.length === 0) return;
ft8Status.textContent = ft8Paused ? "Paused" : "Receiving";
const normalized = [];
for (const msg of messages) {
const next = normalizeServerFt8Message(msg);
if (next.grids.length > 0 && window.ft8MapAddLocator) {
window.ft8MapAddLocator(next.raw, next.grids, "ft8", next.station, {
...msg,
freq_hz: next.rfHz,
locator_details: next.locatorDetails,
});
}
next.history._tsMs = Number.isFinite(next.history?.ts_ms) ? Number(next.history.ts_ms) : Date.now();
normalized.push(next.history);
}
normalized.reverse();
ft8MessageHistory = normalized.concat(ft8MessageHistory);
pruneFt8MessageHistory();
scheduleFt8BarUpdate();
if (ft8Paused) {
ft8BufferedWhilePaused += messages.length;
updateFt8PauseUi();
return;
}
scheduleFt8HistoryRender();
};
window.pruneFt8HistoryView = function() { window.pruneFt8HistoryView = function() {
pruneFt8MessageHistory(); pruneFt8MessageHistory();
updateFt8Bar(); updateFt8Bar();
@@ -385,28 +438,15 @@ document.getElementById("ft8-clear-btn").addEventListener("click", async () => {
// --- Server-side FT8 decode handler --- // --- Server-side FT8 decode handler ---
window.onServerFt8 = function(msg) { window.onServerFt8 = function(msg) {
ft8Status.textContent = ft8Paused ? "Paused" : "Receiving"; ft8Status.textContent = ft8Paused ? "Paused" : "Receiving";
const raw = (msg.message || "").toString(); const next = normalizeServerFt8Message(msg);
const locatorDetails = ft8ExtractLocatorDetails(raw); if (next.grids.length > 0 && window.ft8MapAddLocator) {
const grids = locatorDetails.length > 0 window.ft8MapAddLocator(next.raw, next.grids, "ft8", next.station, {
? locatorDetails.map((detail) => detail.grid)
: ft8ExtractAllGrids(raw);
const station = ft8ExtractLikelyCallsign(raw);
const rfHz = normalizeFt8DisplayFreqHz(msg.freq_hz);
if (grids.length > 0 && window.ft8MapAddLocator) {
window.ft8MapAddLocator(raw, grids, "ft8", station, {
...msg, ...msg,
freq_hz: rfHz, freq_hz: next.rfHz,
locator_details: locatorDetails, locator_details: next.locatorDetails,
}); });
} }
addFt8Message({ addFt8Message(next.history);
receiver: window.getDecodeRigMeta ? window.getDecodeRigMeta() : null,
ts_ms: msg.ts_ms,
snr_db: msg.snr_db,
dt_s: msg.dt_s,
freq_hz: Number.isFinite(rfHz) ? rfHz : msg.freq_hz,
message: msg.message,
});
}; };
updateFt8PauseUi(); updateFt8PauseUi();
@@ -352,6 +352,47 @@ function addHfAprsPacket(pkt) {
scheduleHfAprsHistoryRender(); scheduleHfAprsHistoryRender();
} }
function normalizeServerHfAprsPacket(pkt) {
return {
receiver: window.getDecodeRigMeta ? window.getDecodeRigMeta() : null,
srcCall: pkt.src_call,
destCall: pkt.dest_call,
path: pkt.path,
info: pkt.info,
info_bytes: pkt.info_bytes,
type: pkt.packet_type,
crcOk: pkt.crc_ok,
ts_ms: pkt.ts_ms,
lat: pkt.lat,
lon: pkt.lon,
symbolTable: pkt.symbol_table,
symbolCode: pkt.symbol_code,
};
}
window.onServerHfAprsBatch = function(packets) {
if (!Array.isArray(packets) || packets.length === 0) return;
if (hfAprsStatus) hfAprsStatus.textContent = hfAprsPaused ? "Paused" : "Receiving";
const normalized = [];
for (const pkt of packets) {
const next = normalizeServerHfAprsPacket(pkt);
const tsMs = Number.isFinite(next.ts_ms) ? Number(next.ts_ms) : Date.now();
next._tsMs = tsMs;
next._ts = new Date(tsMs).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit", second: "2-digit" });
normalized.push(next);
}
normalized.reverse();
hfAprsPacketHistory = normalized.concat(hfAprsPacketHistory);
pruneHfAprsPacketHistory();
if (hfAprsPaused) {
hfAprsBufferedWhilePaused += packets.length;
updateHfAprsSummary();
updateHfAprsChipState();
return;
}
scheduleHfAprsHistoryRender();
};
document.getElementById("hf-aprs-decode-toggle-btn")?.addEventListener("click", async () => { document.getElementById("hf-aprs-decode-toggle-btn")?.addEventListener("click", async () => {
try { await postPath("/toggle_hf_aprs_decode"); } catch (e) { console.error("HF APRS toggle failed", e); } try { await postPath("/toggle_hf_aprs_decode"); } catch (e) { console.error("HF APRS toggle failed", e); }
}); });
@@ -418,21 +459,7 @@ if (hfAprsFilterInput) {
// --- Server-side HF APRS decode handler --- // --- Server-side HF APRS decode handler ---
window.onServerHfAprs = function(pkt) { window.onServerHfAprs = function(pkt) {
if (hfAprsStatus) hfAprsStatus.textContent = hfAprsPaused ? "Paused" : "Receiving"; if (hfAprsStatus) hfAprsStatus.textContent = hfAprsPaused ? "Paused" : "Receiving";
addHfAprsPacket({ addHfAprsPacket(normalizeServerHfAprsPacket(pkt));
receiver: window.getDecodeRigMeta ? window.getDecodeRigMeta() : null,
srcCall: pkt.src_call,
destCall: pkt.dest_call,
path: pkt.path,
info: pkt.info,
info_bytes: pkt.info_bytes,
type: pkt.packet_type,
crcOk: pkt.crc_ok,
ts_ms: pkt.ts_ms,
lat: pkt.lat,
lon: pkt.lon,
symbolTable: pkt.symbol_table,
symbolCode: pkt.symbol_code,
});
}; };
renderHfAprsHistory(); renderHfAprsHistory();
@@ -273,6 +273,64 @@ function addVdesMessage(msg) {
} }
} }
function normalizeServerVdesMessage(msg) {
return {
message_type: msg.message_type,
bit_len: msg.bit_len,
raw_bytes: msg.raw_bytes,
lat: msg.lat,
lon: msg.lon,
vessel_name: msg.vessel_name,
callsign: msg.callsign,
destination: msg.destination,
message_label: msg.message_label,
session_id: msg.session_id,
source_id: msg.source_id,
destination_id: msg.destination_id,
data_count: msg.data_count,
asm_identifier: msg.asm_identifier,
ack_nack_mask: msg.ack_nack_mask,
channel_quality: msg.channel_quality,
payload_preview: msg.payload_preview,
link_id: msg.link_id,
sync_score: msg.sync_score,
sync_errors: msg.sync_errors,
phase_rotation: msg.phase_rotation,
fec_state: msg.fec_state,
ts_ms: msg.ts_ms,
};
}
window.onServerVdesBatch = function(messages) {
if (!Array.isArray(messages) || messages.length === 0) return;
if (vdesStatus) vdesStatus.textContent = vdesPaused ? "Paused" : "Receiving";
const normalized = [];
for (const msg of messages) {
const next = normalizeServerVdesMessage(msg);
const tsMs = Number.isFinite(next.ts_ms) ? Number(next.ts_ms) : Date.now();
next._tsMs = tsMs;
next._ts = new Date(tsMs).toLocaleTimeString([], {
hour: "2-digit",
minute: "2-digit",
second: "2-digit",
});
if (next.lat != null && next.lon != null && window.vdesMapAddPoint) {
window.vdesMapAddPoint(next);
}
normalized.push(next);
}
normalized.reverse();
vdesMessageHistory = normalized.concat(vdesMessageHistory);
pruneVdesMessageHistory();
scheduleVdesBarUpdate();
if (vdesPaused) {
vdesBufferedWhilePaused += messages.length;
updateVdesSummary();
return;
}
scheduleVdesHistoryRender();
};
if (vdesClearBtn) { if (vdesClearBtn) {
vdesClearBtn.addEventListener("click", async () => { vdesClearBtn.addEventListener("click", async () => {
try { try {
@@ -305,33 +363,10 @@ if (vdesFilterInput) {
window.onServerVdes = function(msg) { window.onServerVdes = function(msg) {
if (vdesStatus) vdesStatus.textContent = vdesPaused ? "Paused" : "Receiving"; if (vdesStatus) vdesStatus.textContent = vdesPaused ? "Paused" : "Receiving";
addVdesMessage({ const next = normalizeServerVdesMessage(msg);
message_type: msg.message_type, addVdesMessage(next);
bit_len: msg.bit_len, if (next.lat != null && next.lon != null && window.vdesMapAddPoint) {
raw_bytes: msg.raw_bytes, window.vdesMapAddPoint(next);
lat: msg.lat,
lon: msg.lon,
vessel_name: msg.vessel_name,
callsign: msg.callsign,
destination: msg.destination,
message_label: msg.message_label,
session_id: msg.session_id,
source_id: msg.source_id,
destination_id: msg.destination_id,
data_count: msg.data_count,
asm_identifier: msg.asm_identifier,
ack_nack_mask: msg.ack_nack_mask,
channel_quality: msg.channel_quality,
payload_preview: msg.payload_preview,
link_id: msg.link_id,
sync_score: msg.sync_score,
sync_errors: msg.sync_errors,
phase_rotation: msg.phase_rotation,
fec_state: msg.fec_state,
ts_ms: msg.ts_ms,
});
if (msg.lat != null && msg.lon != null && window.vdesMapAddPoint) {
window.vdesMapAddPoint(msg);
} }
}; };
@@ -94,6 +94,56 @@ function addWsprMessage(msg) {
scheduleWsprHistoryRender(); scheduleWsprHistoryRender();
} }
function normalizeServerWsprMessage(msg) {
const raw = (msg.message || "").toString();
const grids = extractAllGrids(raw);
const station = extractLikelyCallsign(raw);
const baseHz = Number.isFinite(window.ft8BaseHz) ? Number(window.ft8BaseHz) : null;
const rfHz = Number.isFinite(msg.freq_hz) && Number.isFinite(baseHz)
? (baseHz + Number(msg.freq_hz))
: (Number.isFinite(msg.freq_hz) ? Number(msg.freq_hz) : null);
return {
raw,
grids,
station,
rfHz,
history: {
receiver: window.getDecodeRigMeta ? window.getDecodeRigMeta() : null,
ts_ms: msg.ts_ms,
snr_db: msg.snr_db,
dt_s: msg.dt_s,
freq_hz: msg.freq_hz,
message: raw,
},
};
}
window.onServerWsprBatch = function(messages) {
if (!Array.isArray(messages) || messages.length === 0) return;
wsprStatus.textContent = wsprPaused ? "Paused" : "Receiving";
const normalized = [];
for (const msg of messages) {
const next = normalizeServerWsprMessage(msg);
if (next.grids.length > 0 && window.ft8MapAddLocator) {
window.ft8MapAddLocator(next.raw, next.grids, "wspr", next.station, {
...msg,
freq_hz: next.rfHz,
});
}
next.history._tsMs = Number.isFinite(next.history?.ts_ms) ? Number(next.history.ts_ms) : Date.now();
normalized.push(next.history);
}
normalized.reverse();
wsprMessageHistory = normalized.concat(wsprMessageHistory);
pruneWsprMessageHistory();
if (wsprPaused) {
wsprBufferedWhilePaused += messages.length;
updateWsprPauseUi();
return;
}
scheduleWsprHistoryRender();
};
window.pruneWsprHistoryView = function() { window.pruneWsprHistoryView = function() {
pruneWsprMessageHistory(); pruneWsprMessageHistory();
renderWsprHistory(); renderWsprHistory();
@@ -252,27 +302,14 @@ document.getElementById("wspr-clear-btn").addEventListener("click", async () =>
window.onServerWspr = function(msg) { window.onServerWspr = function(msg) {
wsprStatus.textContent = wsprPaused ? "Paused" : "Receiving"; wsprStatus.textContent = wsprPaused ? "Paused" : "Receiving";
const raw = (msg.message || "").toString(); const next = normalizeServerWsprMessage(msg);
const grids = extractAllGrids(raw); if (next.grids.length > 0 && window.ft8MapAddLocator) {
const station = extractLikelyCallsign(raw); window.ft8MapAddLocator(next.raw, next.grids, "wspr", next.station, {
const baseHz = Number.isFinite(window.ft8BaseHz) ? Number(window.ft8BaseHz) : null;
const rfHz = Number.isFinite(msg.freq_hz) && Number.isFinite(baseHz)
? (baseHz + Number(msg.freq_hz))
: (Number.isFinite(msg.freq_hz) ? Number(msg.freq_hz) : null);
if (grids.length > 0 && window.ft8MapAddLocator) {
window.ft8MapAddLocator(raw, grids, "wspr", station, {
...msg, ...msg,
freq_hz: rfHz, freq_hz: next.rfHz,
}); });
} }
addWsprMessage({ addWsprMessage(next.history);
receiver: window.getDecodeRigMeta ? window.getDecodeRigMeta() : null,
ts_ms: msg.ts_ms,
snr_db: msg.snr_db,
dt_s: msg.dt_s,
freq_hz: msg.freq_hz,
message: raw,
});
}; };
updateWsprPauseUi(); updateWsprPauseUi();