Files
au_whisper_web/src/lib/mobileMemoryGuard.ts
T

89 lines
3.9 KiB
TypeScript

/**
* Warn before a browser-engine model load that risks crashing the tab on
* mobile (Decisions.md D-027).
*
* Root cause: transformers.js buffers each ONNX weight file fully into a
* JS-heap `Uint8Array`, then onnxruntime-web mallocs a second full-size copy
* inside WASM linear memory to parse it at session-creation time — a
* per-file ~2x peak (`models.ts`'s `estimatePeakLoadBytes`; the
* sequential-session-load patch in `patches/` at least keeps the encoder and
* decoder from both being resident at once). Desktop browsers shrug this
* off; mobile Safari/Chrome enforce much tighter per-tab memory budgets and
* respond to going over them with an outright, uncatchable tab kill —
* observed as Whisper Medium (~460 MB download, ~555 MB estimated peak)
* crashing the browser entirely, cascading on Android into the OS's
* low-memory killer reaping other processes (including the launcher).
*
* Pure and UA-driven rather than feature-detected, same rationale as
* D-026's Firefox check: there is no memory-pressure API to query in
* advance, so a benchmark probe would cost more than it saves.
*/
export interface MobileMemoryHint {
/** `navigator.userAgent`, or undefined outside a browser (tests, SSR). */
userAgent: string | undefined;
/**
* `navigator.maxTouchPoints`. iPadOS 13+ Safari reports a desktop-Safari
* "Macintosh" user agent, so touch support is the only signal left to
* tell an iPad apart from a real Mac.
*/
maxTouchPoints: number | undefined;
/**
* `navigator.deviceMemory` (GiB, quantized: 0.25 .. 8) — Chromium only,
* and capped at 8 regardless of actual RAM. Undefined on Safari/Firefox,
* which means undefined on every iOS browser (all WebKit under the hood).
*/
deviceMemoryGB: number | undefined;
}
const MOBILE_UA_RE = /Android|iPhone|iPod|Mobile/i;
/**
* True for phones/tablets: Android/iPhone/iPod/generic-Mobile UAs, plus a
* touch-capable "Macintosh" UA (iPadOS masquerading as desktop Safari).
*/
export function isLikelyMobile(
hint: Pick<MobileMemoryHint, "userAgent" | "maxTouchPoints">,
): boolean {
if (!hint.userAgent) return false;
if (MOBILE_UA_RE.test(hint.userAgent)) return true;
return /Macintosh/.test(hint.userAgent) && (hint.maxTouchPoints ?? 0) > 1;
}
/**
* Fraction of reported device RAM a single tab can safely peak at before
* mobile OSes intervene.
*
* D-029: the original 0.5 was not conservative enough — `deviceMemory` is
* commonly 2-4 on ordinary (not just flagship) Android phones today, and at
* 0.5 that already yields a 1-2 GB "safe" budget, clearing every model in
* the catalog including Medium (~556 MB peak) and Large v3 Turbo (~762 MB) —
* silently skipping the warning for exactly the phones D-027's crash report
* was about. 0.1 keeps mid/low-end phones (deviceMemory ≤ 4) warned for
* Medium+ while still exempting genuine high-end devices (deviceMemory ≥ 6,
* 600+ MB budget) that have real headroom to spare.
*/
const SAFE_TAB_FRACTION_OF_DEVICE_MEMORY = 0.1;
/**
* Fallback risk threshold when `deviceMemory` isn't reported (notably all of
* iOS/iPadOS, and Firefox). Set between Small's (~330-350 MB) and Medium's
* (~555 MB) estimated peaks, so it flags Medium/Large without flagging the
* models this codebase has no crash evidence for (D-022's precedent: scope
* fixes to where the evidence is, not every model preemptively).
*/
const MOBILE_FALLBACK_RISK_THRESHOLD_BYTES = 400_000_000;
/**
* True when a browser-engine load of `peakBytes` is likely to crash this
* device's tab. Desktop/laptop browsers are out of scope — the crash reports
* this guards against are mobile-only.
*/
export function shouldWarnBeforeLoad(hint: MobileMemoryHint, peakBytes: number): boolean {
if (!isLikelyMobile(hint)) return false;
if (hint.deviceMemoryGB !== undefined) {
return peakBytes > hint.deviceMemoryGB * 1e9 * SAFE_TAB_FRACTION_OF_DEVICE_MEMORY;
}
return peakBytes > MOBILE_FALLBACK_RISK_THRESHOLD_BYTES;
}