Files
whisper_web/src/lib/mobileMemoryGuard.ts
T

114 lines
4.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 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.
*/
import type { WhisperModel } from "./models";
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;
}
/**
* Desktop-only models (D-WS7a): Parakeet's ~1.3 GB load peak (2 × the
* 652 MB int8 encoder, D-027's arithmetic) exceeds every phone budget the
* D-029 table considers safe, even at deviceMemory 8 (800 MB). Unlike the
* Whisper models — where a confirm lets the user judge their device — it is
* not offered at all on phones and tablets: no amount of RAM a phone reports
* makes it safe, so a "Continue anyway?" would only invite the crash.
*/
export function isModelAllowedOnDevice(
model: Pick<WhisperModel, "family">,
hint: Pick<MobileMemoryHint, "userAgent" | "maxTouchPoints">,
): boolean {
return model.family !== "parakeet" || !isLikelyMobile(hint);
}
/** The catalog as offered on this device (order preserved). */
export function modelsForDevice<T extends Pick<WhisperModel, "family">>(
models: readonly T[],
hint: Pick<MobileMemoryHint, "userAgent" | "maxTouchPoints">,
): T[] {
return models.filter((model) => isModelAllowedOnDevice(model, hint));
}