From 20b77a774ac04d0c38889e139c531f9e98e70643 Mon Sep 17 00:00:00 2001 From: BoulderBadgeDad Date: Wed, 3 Jun 2026 08:18:41 -0700 Subject: [PATCH] =?UTF-8?q?Artist=20Map=20v2=20=E2=80=94=20the=20real=20lo?= =?UTF-8?q?ck:=20downscale=20artist=20images=20on=20decode=20(6GB=20?= =?UTF-8?q?=E2=86=92=20~100MB)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Perf telemetry was the giveaway: after the buffer cap, rebuild + draw were both ~10ms, yet fps stayed 1-3 and the browser 'locked'. Cheap draw + locked system = memory/GPU thrash, not drawing. Cause: artist images load at up to 1000×1000, and a dense map holds ~1500 of them — ~1500 × 1000² × 4B ≈ 6 GB of decoded ImageBitmap memory. The browser GCs/ evicts textures constantly → systemic lag the canvas timers don't see. Fix: decode straight to a 128px avatar via createImageBitmap resize options (nodes render tiny anyway). ~1500 × 128² × 4B ≈ 100 MB instead of 6 GB. Falls back to full decode on engines that ignore the resize opts. This is the one that should actually make it smooth. Perf overlay stays on 'd'. --- webui/static/discover.js | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/webui/static/discover.js b/webui/static/discover.js index 4135dbfc..f638acdf 100644 --- a/webui/static/discover.js +++ b/webui/static/discover.js @@ -6606,16 +6606,33 @@ function artMapToggleSimilar() { showToast(_artMap._hideSimilar ? 'Showing watchlist only' : 'Showing all artists', 'info', 1500); } +// Artist images come in at up to 1000×1000. Nodes are drawn tiny, so holding +// full-res bitmaps is pointless and ruinous: ~1500 nodes × 1000² × 4 bytes ≈ 6 GB +// of decoded image memory → GC/GPU thrash that locks the browser even though the +// per-frame draw is cheap. Decode straight to a small avatar (~128px) so the +// whole map's images fit in ~100 MB instead of gigabytes. +const _ARTMAP_IMG_PX = 128; +function _artMapDecodeSmall(blob) { + if (!blob) return Promise.resolve(null); + try { + return createImageBitmap(blob, { + resizeWidth: _ARTMAP_IMG_PX, resizeHeight: _ARTMAP_IMG_PX, resizeQuality: 'medium', + }).catch(() => createImageBitmap(blob).catch(() => null)); // older engines ignore opts + } catch (e) { + return createImageBitmap(blob).catch(() => null); + } +} + function _artMapLoadImage(url) { // Try direct CORS fetch first (zero server load, works for Spotify/iTunes/Discogs) return fetch(url, { mode: 'cors' }) .then(r => r.ok ? r.blob() : Promise.reject('not ok')) - .then(b => createImageBitmap(b)) + .then(b => _artMapDecodeSmall(b)) .catch(() => { // Fallback: server proxy for CDNs without CORS headers return fetch('/api/image-proxy?url=' + encodeURIComponent(url)) .then(r => r.ok ? r.blob() : null) - .then(b => b ? createImageBitmap(b) : null) + .then(b => _artMapDecodeSmall(b)) .catch(() => null); }); }