Optimize max peak memory usage and add warnings on mobile for higher models

This commit is contained in:
Lucas Winther committed 2026-07-28 07:23:37 +02:00
1 parent f6019b3a42
commit a0ef5548af
10 files changed
+428 -7

No files matched your search

+65 -6
View File
@@ -32,8 +32,12 @@ import {
DEFAULT_MODEL_ID,
DEFAULT_TASK,
MODELS,
estimatePeakLoadBytes,
formatModelSize,
getModel,
resolveDefaultDtype,
} from "./lib/models";
import { shouldWarnBeforeLoad, type MobileMemoryHint } from "./lib/mobileMemoryGuard";
import {
applyLoadProgress,
initialLoadProgress,
@@ -84,6 +88,12 @@ export function App() {
// entirely. Session-only on purpose: a browser update may fix the backend.
const deviceOverrides = useRef(new Map<string, InferenceDevice>());
// Mobile memory-crash warnings the user already accepted this session
// (D-027), keyed "modelId::device" — once confirmed, repeat loads of the
// same combination (e.g. the lazy auto-load after an explicit Download)
// don't ask again.
const mobileWarningAcknowledged = useRef(new Set<string>());
// Resolves when the current browser-engine load reports ready (rejects on
// load failure/cancel). Transcribe posts await it, so no request is ever
// queued inside a worker that a load-restart (D-025) is about to terminate
@@ -203,6 +213,46 @@ export function App() {
return deviceOverrides.current.get(overrideKey(forModelId, preference)) ?? preference;
}
/**
* Confirm before a browser-engine load that risks crashing the tab on
* mobile (D-027). Returns true if the load should proceed — either it
* isn't risky, or the user already accepted the risk (this load, or an
* identical one earlier this session). A declined confirm returns false
* with no side effects, so callers can bail out before touching any state.
*/
function confirmRiskyMobileLoad(forModelId: string, preference: DevicePreference): boolean {
const targetModel = getModel(forModelId);
if (!targetModel) return true;
// Mirrors ModelPicker's size-estimate fallback: until the worker reports
// an active device, assume WASM unless the user asked for an accelerator.
const device: InferenceDevice =
activeDevice ??
(preference === "webgpu" ? "webgpu" : preference === "webnn" ? "webnn" : "wasm");
const key = overrideKey(forModelId, device);
if (mobileWarningAcknowledged.current.has(key)) return true;
const peakBytes = estimatePeakLoadBytes(targetModel, resolveDefaultDtype(forModelId, device));
const hint: MobileMemoryHint = {
userAgent: typeof navigator !== "undefined" ? navigator.userAgent : undefined,
maxTouchPoints: typeof navigator !== "undefined" ? navigator.maxTouchPoints : undefined,
deviceMemoryGB:
typeof navigator !== "undefined"
? (navigator as Navigator & { deviceMemory?: number }).deviceMemory
: undefined,
};
if (!shouldWarnBeforeLoad(hint, peakBytes)) return true;
const proceed = window.confirm(
`${targetModel.displayName} needs roughly ${formatModelSize(peakBytes)} of memory at ` +
"once to load — more than many phones and tablets allow a browser tab to use " +
"before force-closing it. This can crash your browser (and on some phones, the " +
"whole screen briefly goes back to the home screen). A smaller model (Tiny, Base " +
"or Small) is much safer on mobile.\n\nContinue anyway?",
);
if (proceed) mobileWarningAcknowledged.current.add(key);
return proceed;
}
function newLoadGate(): void {
let resolve!: () => void;
let reject!: (error: Error) => void;
@@ -406,6 +456,7 @@ export function App() {
/** Explicit model download (D-020) — pre-caches weights for offline use. */
function handleDownloadModel() {
if (busy || liveStatus !== "idle" || loadStarted) return;
if (!confirmRiskyMobileLoad(modelId, devicePreference)) return;
startLoad(modelId, devicePreference);
}
@@ -457,13 +508,17 @@ export function App() {
async function handleFile(file: File) {
if (busy || liveStatus !== "idle") return;
const run = ++fileRun.current;
// The toggle's choice only takes effect with a session and an available
// server engine (D-010); everything else runs on-device as always.
const useServer =
engineChoice === "server" &&
auth.user !== null &&
serverStatus?.available === true;
// Browser-engine runs may need to load the model right away (D-020); bail
// out before any state changes if the user declines a mobile crash risk
// warning (D-027).
if (!useServer && !confirmRiskyMobileLoad(modelId, devicePreference)) return;
const run = ++fileRun.current;
setBusy(true);
setRunEngine(useServer ? "server" : "browser");
setError(null);
@@ -603,6 +658,15 @@ export function App() {
async function startLive() {
if (busy || liveStatus !== "idle") return;
const useServer =
engineChoice === "server" &&
auth.user !== null &&
serverStatus?.available === true;
// Browser-engine live sessions may need to load the model right away
// (D-020); bail out before any state changes if the user declines a
// mobile crash risk warning (D-027).
if (!useServer && !confirmRiskyMobileLoad(modelId, devicePreference)) return;
setError(null);
setResult(null);
setPartialText("");
@@ -612,11 +676,6 @@ export function App() {
setLivePending(0);
setLiveStatus("starting");
const useServer =
engineChoice === "server" &&
auth.user !== null &&
serverStatus?.available === true;
const options = (getModel(modelId)?.multilingual ?? false) ? { language, task } : {};
if (useServer) {