diff --git a/CHANGELOG.md b/CHANGELOG.md index f23c37e..7850664 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,11 @@ Follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) · versioned wi --- +## [1.20.39] — 2026-08-19 + +### Changed +- **Low-memory devices now receive conservative batch concurrency.** Shared frontend worker pools cap themselves at two concurrent tasks when the browser reports approximately 4 GB RAM or less, reducing memory spikes during bulk operations. + ## [1.20.38] — 2026-08-19 ### Added diff --git a/TODO.md b/TODO.md index ab08e5c..22668aa 100644 --- a/TODO.md +++ b/TODO.md @@ -37,7 +37,7 @@ Priority levels: - [ ] Update only affected table rows instead of rebuilding entire tables. - [ ] Add an LRU cache for audio and images and remove old entries automatically. - [ ] Revoke discarded object URLs with `URL.revokeObjectURL(objectUrl)`. -- [ ] Add low-memory mode using `navigator.deviceMemory` where available. +- [x] Add low-memory mode using `navigator.deviceMemory` where available; shared worker pools cap at two tasks on devices reporting approximately 4 GB RAM or less. - [ ] Disable image preloading and reduce table/cache sizes on devices with approximately 4 GB RAM. ## P1 — Backend stability and bounded concurrency diff --git a/VERSION b/VERSION index 2bd71a6..df40f00 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.20.38 +1.20.39 diff --git a/static/index.html b/static/index.html index b583fac..7cdb9f4 100644 --- a/static/index.html +++ b/static/index.html @@ -10,7 +10,7 @@ - + @@ -27,7 +27,7 @@ - + @@ -378,7 +378,7 @@ window.toggleNavTree = function(treeId, chevronId) { - + diff --git a/static/js/utils.js b/static/js/utils.js index dd9b6fb..9a357cf 100644 --- a/static/js/utils.js +++ b/static/js/utils.js @@ -336,10 +336,20 @@ function debounce(fn, ms) { let t; return (...a) => { clearTimeout(t); t = setTi // Log a non-fatal error instead of silently swallowing it (was `.catch(()=>{})`). // Use in best-effort paths so failures are visible in the console for debugging. function logErr(context, err) { try { console.warn(`[${context}]`, err && (err.message || err)); } catch (_) {} } +// Keep batch work conservative on small client devices. Desktop machines +// without the Device Memory API keep the normal pool size; browsers that do +// expose it get a hard cap before multiple audio/image requests can build up. +function ttvcIsLowMemoryDevice() { + const memory = Number(globalThis.navigator?.deviceMemory); + return Number.isFinite(memory) && memory > 0 && memory <= 4; +} +window.ttvcIsLowMemoryDevice = ttvcIsLowMemoryDevice; // Run an async worker over items with bounded concurrency (default 4). `onProgress(done,total)` // fires after each item. Replaces slow serial `for…await` loops over network calls. async function runPool(items, worker, concurrency = 4, onProgress) { const arr = [...items]; let i = 0, done = 0; const total = arr.length; + const requested = Math.max(1, Number(concurrency) || 1); + const effectiveConcurrency = ttvcIsLowMemoryDevice() ? Math.min(2, requested) : requested; async function run() { while (i < arr.length) { const idx = i++; @@ -347,7 +357,7 @@ async function runPool(items, worker, concurrency = 4, onProgress) { done++; if (onProgress) { try { onProgress(done, total); } catch (_) {} } } } - await Promise.all(Array.from({ length: Math.min(concurrency, arr.length) }, run)); + await Promise.all(Array.from({ length: Math.min(effectiveConcurrency, arr.length) }, run)); } async function copyText(text) { try {