diff --git a/apps/diffr-web/src/engine/engine.ts b/apps/diffr-web/src/engine/engine.ts index 22a5e12a6..0ab0b46fd 100644 --- a/apps/diffr-web/src/engine/engine.ts +++ b/apps/diffr-web/src/engine/engine.ts @@ -1,3 +1,5 @@ +import { isAndroid, isIOS } from "vs/base/common/platform.js"; + import { cacheKey } from "../cache.js"; import { agentPlugins } from "../settings.js"; /** @@ -5,8 +7,8 @@ import { agentPlugins } from "../settings.js"; * of workers, so files diff in parallel without each worker paying for its own compile. The pool * starts at one and grows while work queues, a worker at a time: each spends a second or two * compiling diffr's queries, and workers starting together slow each other several times over. - * Every worker goes after a few idle seconds, since wasm memory never shrinks: the compiled module - * stays, so the next file's worker starts in tens of milliseconds. + * Every worker goes after a few idle seconds, since wasm memory never shrinks, and the compiled + * module with the last of them: compiling again costs the next file well under a tenth of a second. */ // Gzipped by the build (vite.config.ts): static hosts cap a file at 25 MiB, and the engine is larger. import wasmUrl from "../wasm/diffr_web_bg.wasm.gz?url"; @@ -53,15 +55,49 @@ export interface EngineStats { diffMs: number; } -const MAX_WORKERS = Math.max( - 1, - Math.min(8, (navigator.hardwareConcurrency || 4) - 1), -); +declare global { + interface Navigator { + /** Chrome's: the device's memory in GB, rounded and capped at 8. Safari and Firefox lack it. */ + readonly deviceMemory?: number; + } +} -const IDLE_MS = 3_000; +const MB = 1024 * 1024; + +/** + * How many workers the pool grows to, and the wasm memory past which a worker is replaced once its + * queue drains. Each worker's memory never shrinks, so together they must fit what the browser + * lets the tab hold. iOS kills a tab over its budget (often well under 1 GB) rather than failing + * the allocation, and says nothing of its memory or cores, so it gets one lean worker. + */ +function workerBudget() { + const cores = Math.max( + 1, + Math.min(8, (navigator.hardwareConcurrency || 4) - 1), + ); + + const { deviceMemory } = navigator; + + if (isIOS) return { workers: 1, memoryLimit: 256 * MB }; -/** Past this, a worker is replaced once its queue drains. */ -const MEMORY_LIMIT = 1024 * 1024 * 1024; + if (isAndroid) + return { + workers: + deviceMemory !== undefined && deviceMemory >= 8 + ? Math.min(2, cores) + : 1, + memoryLimit: 384 * MB, + }; + + if (deviceMemory !== undefined && deviceMemory < 8) + return { workers: Math.min(2, cores), memoryLimit: 512 * MB }; + + return { workers: cores, memoryLimit: 1024 * MB }; +} + +const { workers: MAX_WORKERS, memoryLimit: MEMORY_LIMIT } = workerBudget(); + +const IDLE_MS = 3_000; const stats: EngineStats = { maxWorkers: MAX_WORKERS, @@ -307,6 +343,8 @@ export class Engine { } } + // Each worker holds the module it was sent, so with none left the page holds the last. + if (!stats.workers) compiled = undefined; changed(); }, IDLE_MS); }