diff --git a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/app.js b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/app.js index 6c41a03..9ff897a 100644 --- a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/app.js +++ b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/app.js @@ -2951,6 +2951,35 @@ function yieldToMain() { return new Promise((resolve) => setTimeout(resolve, 0)); } +const uiFrameJobs = new Map(); +let uiFrameJobsHandle = null; + +function flushUiFrameJobs() { + uiFrameJobsHandle = null; + const jobs = Array.from(uiFrameJobs.values()); + uiFrameJobs.clear(); + for (const job of jobs) { + try { + job(); + } catch (err) { + console.error("Deferred UI job failed:", err); + } + } +} + +function scheduleUiFrameJob(key, job) { + if (typeof job !== "function") return; + uiFrameJobs.set(key, job); + if (uiFrameJobsHandle !== null) return; + if (typeof requestAnimationFrame === "function") { + uiFrameJobsHandle = requestAnimationFrame(flushUiFrameJobs); + } else { + uiFrameJobsHandle = setTimeout(flushUiFrameJobs, 16); + } +} + +window.trxScheduleUiFrameJob = scheduleUiFrameJob; + async function postPath(path) { const resp = await fetch(path, { method: "POST" }); if (authEnabled && resp.status === 401) { @@ -5397,8 +5426,7 @@ window.aprsMapAddStation = function(call, lat, lon, info, symbolTable, symbolCod stationMarkers.set(call, entry); if (aprsMap) { _aprsAddMarkerToMap(call, entry); - rebuildMapLocatorFilters(); - applyMapFilter(); + scheduleDecodeMapMaintenance(); } } }; @@ -5537,8 +5565,7 @@ window.aisMapAddVessel = function(msg) { trackPoints: [nextPoint], msg, }); - rebuildMapLocatorFilters(); - applyMapFilter(); + scheduleDecodeMapMaintenance(); }; window.vdesMapAddPoint = function(msg) { @@ -5572,8 +5599,7 @@ window.vdesMapAddPoint = function(msg) { marker._vdesKey = key; entry.marker = marker; mapMarkers.add(marker); - rebuildMapLocatorFilters(); - applyMapFilter(); + scheduleDecodeMapMaintenance(); }; function maidenheadToBounds(grid) { @@ -5642,6 +5668,14 @@ function updateMapP2pPathsToggle() { btn.classList.toggle("is-active", mapP2pRadioPathsEnabled); } +function scheduleDecodeMapMaintenance() { + scheduleUiFrameJob("decode-map-maintenance", () => { + rebuildDecodeContactPaths(); + rebuildMapLocatorFilters(); + applyMapFilter(); + }); +} + function escapeMapHtml(input) { return String(input) .replaceAll("&", "&") @@ -5913,8 +5947,7 @@ window.ft8MapAddLocator = function(message, grids, type = "ft8", station = null, existing.marker.setPopupContent(tooltipHtml); sendLocatorOverlayToBack(existing.marker); assignLocatorMarkerMeta(existing.marker, existing.sourceType, existing.bandMeta); - rebuildMapLocatorFilters(); - applyMapFilter(); + scheduleDecodeMapMaintenance(); continue; } @@ -5936,9 +5969,7 @@ window.ft8MapAddLocator = function(message, grids, type = "ft8", station = null, locatorMarkers.set(key, { marker, grid, stations, stationDetails, sourceType: markerType, bandMeta }); mapMarkers.add(marker); } - rebuildDecodeContactPaths(); - rebuildMapLocatorFilters(); - applyMapFilter(); + scheduleDecodeMapMaintenance(); }; // --- Sub-tab navigation --- @@ -6691,12 +6722,31 @@ function dispatchDecodeMessage(msg) { if (msg.type === "wspr" && window.onServerWspr) window.onServerWspr(msg); } +const DECODE_HISTORY_MAX_BATCH = 12; +const DECODE_HISTORY_SLICE_BUDGET_MS = 6; + +function scheduleDecodeHistoryDrainStep(callback) { + if (typeof callback !== "function") return; + if (typeof requestAnimationFrame === "function") { + requestAnimationFrame(() => callback()); + } else { + setTimeout(callback, 16); + } +} + function drainDecodeHistory(buffer, index, onDone) { - const CHUNK = 30; - const end = Math.min(index + CHUNK, buffer.length); - for (let i = index; i < end; i++) dispatchDecodeMessage(buffer[i]); - if (end < buffer.length) { - setTimeout(() => drainDecodeHistory(buffer, end, onDone), 0); + const startedAt = typeof performance !== "undefined" && typeof performance.now === "function" + ? performance.now() + : 0; + let nextIndex = index; + while (nextIndex < buffer.length) { + dispatchDecodeMessage(buffer[nextIndex]); + nextIndex += 1; + if (nextIndex - index >= DECODE_HISTORY_MAX_BATCH) break; + if (startedAt > 0 && (performance.now() - startedAt) >= DECODE_HISTORY_SLICE_BUDGET_MS) break; + } + if (nextIndex < buffer.length) { + scheduleDecodeHistoryDrainStep(() => drainDecodeHistory(buffer, nextIndex, onDone)); } else if (typeof onDone === "function") { onDone(); } diff --git a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/plugins/ais.js b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/plugins/ais.js index 40d7179..d4e27ee 100644 --- a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/plugins/ais.js +++ b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/plugins/ais.js @@ -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) { diff --git a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/plugins/aprs.js b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/plugins/aprs.js index dfdb0ba..ec40b8a 100644 --- a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/plugins/aprs.js +++ b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/plugins/aprs.js @@ -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 () => { diff --git a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/plugins/ft8.js b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/plugins/ft8.js index 4513f5c..6fcae63 100644 --- a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/plugins/ft8.js +++ b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/plugins/ft8.js @@ -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) { diff --git a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/plugins/hf-aprs.js b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/plugins/hf-aprs.js index 0d1267b..aeaa7f7 100644 --- a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/plugins/hf-aprs.js +++ b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/plugins/hf-aprs.js @@ -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 () => { diff --git a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/plugins/vdes.js b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/plugins/vdes.js index ff86af0..b9420aa 100644 --- a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/plugins/vdes.js +++ b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/plugins/vdes.js @@ -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(); } } diff --git a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/plugins/wspr.js b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/plugins/wspr.js index 4063101..4141639 100644 --- a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/plugins/wspr.js +++ b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/plugins/wspr.js @@ -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) {