114 lines
4.9 KiB
TypeScript
114 lines
4.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.
|
||
*/
|
||
|
||
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));
|
||
}
|