89 lines
3.9 KiB
TypeScript
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;
|
|
}
|