[fix](trx-frontend-http): smooth decode history loading

Reduce main-thread stalls while decode history replays.\n\nCoalesce decoder list redraws and map maintenance so spectrum\nand controls stay responsive during history import.\n\nVerification: node --check on modified frontend JS files.\n\nCo-authored-by: OpenAI Codex <codex@openai.com>

Signed-off-by: Stan Grams <sjg@haxx.space>
This commit is contained in:
2026-03-14 12:56:02 +01:00
parent 28d5494c3b
commit c4d1313625
7 changed files with 174 additions and 38 deletions
@@ -17,6 +17,22 @@ let aisMessageHistory = [];
let aisPaused = false;
let aisBufferedWhilePaused = 0;
function scheduleAisUi(key, job) {
if (typeof window.trxScheduleUiFrameJob === "function") {
window.trxScheduleUiFrameJob(key, job);
return;
}
job();
}
function scheduleAisHistoryRender() {
scheduleAisUi("ais-history", () => renderAisHistory());
}
function scheduleAisBarUpdate() {
scheduleAisUi("ais-bar", () => updateAisBar());
}
function formatAisMhz(freqHz) {
return `${(freqHz / 1_000_000).toFixed(3)} MHz`;
}
@@ -283,10 +299,11 @@ function renderAisHistory() {
updateAisSummary();
return;
}
aisMessagesEl.innerHTML = "";
const fragment = document.createDocumentFragment();
for (let i = 0; i < aisMessageHistory.length; i += 1) {
aisMessagesEl.appendChild(renderAisRow(aisMessageHistory[i]));
fragment.appendChild(renderAisRow(aisMessageHistory[i]));
}
aisMessagesEl.replaceChildren(fragment);
updateAisSummary();
}
@@ -301,13 +318,13 @@ function addAisMessage(msg) {
aisMessageHistory.unshift(msg);
if (aisMessageHistory.length > AIS_MAX_MESSAGES) aisMessageHistory.length = AIS_MAX_MESSAGES;
updateAisBar();
scheduleAisBarUpdate();
if (aisPaused) {
aisBufferedWhilePaused += 1;
updateAisSummary();
} else {
renderAisHistory();
scheduleAisHistoryRender();
}
if (msg.lat != null && msg.lon != null && window.aisMapAddVessel) {
@@ -21,6 +21,22 @@ let aprsHideCrc = false;
let aprsCollapseDup = false;
let aprsTypeFilter = "all";
function scheduleAprsUi(key, job) {
if (typeof window.trxScheduleUiFrameJob === "function") {
window.trxScheduleUiFrameJob(key, job);
return;
}
job();
}
function scheduleAprsHistoryRender() {
scheduleAprsUi("aprs-history", () => renderAprsHistory());
}
function scheduleAprsBarUpdate() {
scheduleAprsUi("aprs-bar", () => updateAprsBar());
}
function renderAprsInfo(pkt) {
const bytes = Array.isArray(pkt.info_bytes) ? pkt.info_bytes : null;
if (bytes && bytes.length > 0) {
@@ -294,10 +310,11 @@ function renderAprsHistory() {
return;
}
const visible = aprsVisiblePackets();
aprsPacketsEl.innerHTML = "";
const fragment = document.createDocumentFragment();
for (let i = 0; i < visible.length; i++) {
aprsPacketsEl.appendChild(renderAprsRow(visible[i], i === 0));
fragment.appendChild(renderAprsRow(visible[i], i === 0));
}
aprsPacketsEl.replaceChildren(fragment);
updateAprsSummary();
updateAprsChipState();
}
@@ -354,7 +371,7 @@ function addAprsPacket(pkt) {
window.aprsMapAddStation(pkt.srcCall, pkt.lat, pkt.lon, pkt.info, pkt.symbolTable, pkt.symbolCode, pkt);
}
if (pkt.crcOk) updateAprsBar();
if (pkt.crcOk) scheduleAprsBarUpdate();
if (aprsPaused) {
aprsBufferedWhilePaused += 1;
@@ -363,7 +380,7 @@ function addAprsPacket(pkt) {
return;
}
renderAprsHistory();
scheduleAprsHistoryRender();
}
document.getElementById("aprs-clear-btn").addEventListener("click", async () => {
@@ -13,6 +13,22 @@ let ft8MessageHistory = [];
let ft8Paused = false;
let ft8BufferedWhilePaused = 0;
function scheduleFt8Ui(key, job) {
if (typeof window.trxScheduleUiFrameJob === "function") {
window.trxScheduleUiFrameJob(key, job);
return;
}
job();
}
function scheduleFt8HistoryRender() {
scheduleFt8Ui("ft8-history", () => renderFt8History());
}
function scheduleFt8BarUpdate() {
scheduleFt8Ui("ft8-bar", () => updateFt8Bar());
}
function normalizeFt8DisplayFreqHz(freqHz) {
const rawHz = Number(freqHz);
if (!Number.isFinite(rawHz)) return null;
@@ -66,23 +82,24 @@ function renderFt8History() {
updateFt8PauseUi();
return;
}
ft8MessagesEl.innerHTML = "";
const fragment = document.createDocumentFragment();
for (let i = 0; i < ft8MessageHistory.length; i += 1) {
ft8MessagesEl.appendChild(renderFt8Row(ft8MessageHistory[i]));
fragment.appendChild(renderFt8Row(ft8MessageHistory[i]));
}
ft8MessagesEl.replaceChildren(fragment);
updateFt8PauseUi();
}
function addFt8Message(msg) {
ft8MessageHistory.unshift(msg);
if (ft8MessageHistory.length > FT8_MAX_MESSAGES) ft8MessageHistory.length = FT8_MAX_MESSAGES;
updateFt8Bar();
scheduleFt8BarUpdate();
if (ft8Paused) {
ft8BufferedWhilePaused += 1;
updateFt8PauseUi();
return;
}
renderFt8History();
scheduleFt8HistoryRender();
}
function ft8BarRfText(msg) {
@@ -19,6 +19,14 @@ let hfAprsHideCrc = false;
let hfAprsCollapseDup = false;
let hfAprsTypeFilter = "all";
function scheduleHfAprsHistoryRender() {
if (typeof window.trxScheduleUiFrameJob === "function") {
window.trxScheduleUiFrameJob("hf-aprs-history", () => renderHfAprsHistory());
return;
}
renderHfAprsHistory();
}
function hfAprsPacketCategory(pkt) {
const type = String(pkt.type || "").toLowerCase();
const info = String(pkt.info || "").toLowerCase();
@@ -294,10 +302,11 @@ function renderHfAprsHistory() {
return;
}
const visible = hfAprsVisiblePackets();
hfAprsPacketsEl.innerHTML = "";
const fragment = document.createDocumentFragment();
for (let i = 0; i < visible.length; i++) {
hfAprsPacketsEl.appendChild(renderHfAprsRow(visible[i], i === 0));
fragment.appendChild(renderHfAprsRow(visible[i], i === 0));
}
hfAprsPacketsEl.replaceChildren(fragment);
updateHfAprsSummary();
updateHfAprsChipState();
}
@@ -324,7 +333,7 @@ function addHfAprsPacket(pkt) {
return;
}
renderHfAprsHistory();
scheduleHfAprsHistoryRender();
}
document.getElementById("hf-aprs-decode-toggle-btn")?.addEventListener("click", async () => {
@@ -15,6 +15,22 @@ let vdesMessageHistory = [];
let vdesPaused = false;
let vdesBufferedWhilePaused = 0;
function scheduleVdesUi(key, job) {
if (typeof window.trxScheduleUiFrameJob === "function") {
window.trxScheduleUiFrameJob(key, job);
return;
}
job();
}
function scheduleVdesHistoryRender() {
scheduleVdesUi("vdes-history", () => renderVdesHistory());
}
function scheduleVdesBarUpdate() {
scheduleVdesUi("vdes-bar", () => updateVdesBar());
}
function currentVdesCenterText() {
const raw = (document.getElementById("freq")?.value || "").replace(/[^\d]/g, "");
const hz = raw ? Number(raw) : 0;
@@ -216,10 +232,11 @@ function renderVdesHistory() {
updateVdesSummary();
return;
}
vdesMessagesEl.innerHTML = "";
const fragment = document.createDocumentFragment();
for (let i = 0; i < vdesMessageHistory.length; i += 1) {
vdesMessagesEl.appendChild(renderVdesRow(vdesMessageHistory[i]));
fragment.appendChild(renderVdesRow(vdesMessageHistory[i]));
}
vdesMessagesEl.replaceChildren(fragment);
updateVdesSummary();
}
@@ -234,13 +251,13 @@ function addVdesMessage(msg) {
vdesMessageHistory.unshift(msg);
if (vdesMessageHistory.length > VDES_MAX_MESSAGES) vdesMessageHistory.length = VDES_MAX_MESSAGES;
updateVdesBar();
scheduleVdesBarUpdate();
if (vdesPaused) {
vdesBufferedWhilePaused += 1;
updateVdesSummary();
} else {
renderVdesHistory();
scheduleVdesHistoryRender();
}
}
@@ -11,6 +11,14 @@ let wsprMessageHistory = [];
let wsprPaused = false;
let wsprBufferedWhilePaused = 0;
function scheduleWsprHistoryRender() {
if (typeof window.trxScheduleUiFrameJob === "function") {
window.trxScheduleUiFrameJob("wspr-history", () => renderWsprHistory());
return;
}
renderWsprHistory();
}
function fmtWsprTime(tsMs) {
if (!tsMs) return "--:--:--";
return new Date(tsMs).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit", second: "2-digit" });
@@ -55,10 +63,11 @@ function renderWsprHistory() {
updateWsprPauseUi();
return;
}
wsprMessagesEl.innerHTML = "";
const fragment = document.createDocumentFragment();
for (let i = 0; i < wsprMessageHistory.length; i += 1) {
wsprMessagesEl.appendChild(renderWsprRow(wsprMessageHistory[i]));
fragment.appendChild(renderWsprRow(wsprMessageHistory[i]));
}
wsprMessagesEl.replaceChildren(fragment);
updateWsprPauseUi();
}
@@ -70,7 +79,7 @@ function addWsprMessage(msg) {
updateWsprPauseUi();
return;
}
renderWsprHistory();
scheduleWsprHistoryRender();
}
function escapeWsprHtml(input) {