Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 47 additions & 9 deletions apps/diffr-web/src/engine/engine.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import { isAndroid, isIOS } from "vs/base/common/platform.js";

import { cacheKey } from "../cache.js";
import { agentPlugins } from "../settings.js";
/**
* The page's handle on diffr: the wasm module is fetched and compiled once, then handed to a pool
* 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";
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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);
}
Expand Down
Loading