/** * 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, ): 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, hint: Pick, ): boolean { return model.family !== "parakeet" || !isLikelyMobile(hint); } /** The catalog as offered on this device (order preserved). */ export function modelsForDevice>( models: readonly T[], hint: Pick, ): T[] { return models.filter((model) => isModelAllowedOnDevice(model, hint)); }