Optimize max peak memory usage and add warnings on mobile for higher models
This commit is contained in:
@@ -0,0 +1,87 @@
|
||||
import { describe, test, expect } from "bun:test";
|
||||
import { isLikelyMobile, shouldWarnBeforeLoad } from "../src/lib/mobileMemoryGuard";
|
||||
|
||||
// Pure UA/heuristic logic (D-027) — no browser, no navigator.
|
||||
|
||||
const ANDROID_UA =
|
||||
"Mozilla/5.0 (Linux; Android 13; Pixel 7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0 Mobile Safari/537.36";
|
||||
const IPHONE_UA =
|
||||
"Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 Mobile/15E148 Safari/604.1";
|
||||
const IPAD_DESKTOP_UA =
|
||||
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 Safari/605.1.15";
|
||||
const DESKTOP_MAC_UA =
|
||||
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0 Safari/537.36";
|
||||
const DESKTOP_WINDOWS_UA =
|
||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0 Safari/537.36";
|
||||
|
||||
describe("isLikelyMobile", () => {
|
||||
test("detects Android and iPhone user agents", () => {
|
||||
expect(isLikelyMobile({ userAgent: ANDROID_UA, maxTouchPoints: 5 })).toBe(true);
|
||||
expect(isLikelyMobile({ userAgent: IPHONE_UA, maxTouchPoints: 5 })).toBe(true);
|
||||
});
|
||||
|
||||
test("detects iPadOS's desktop-Safari UA via touch points", () => {
|
||||
expect(isLikelyMobile({ userAgent: IPAD_DESKTOP_UA, maxTouchPoints: 5 })).toBe(true);
|
||||
// Same UA, no touch support reported — treat as a real desktop Mac.
|
||||
expect(isLikelyMobile({ userAgent: IPAD_DESKTOP_UA, maxTouchPoints: 0 })).toBe(false);
|
||||
expect(isLikelyMobile({ userAgent: IPAD_DESKTOP_UA, maxTouchPoints: undefined })).toBe(
|
||||
false,
|
||||
);
|
||||
});
|
||||
|
||||
test("does not flag real desktop browsers", () => {
|
||||
expect(isLikelyMobile({ userAgent: DESKTOP_MAC_UA, maxTouchPoints: 0 })).toBe(false);
|
||||
expect(isLikelyMobile({ userAgent: DESKTOP_WINDOWS_UA, maxTouchPoints: 0 })).toBe(false);
|
||||
});
|
||||
|
||||
test("undefined userAgent (SSR/tests) is never mobile", () => {
|
||||
expect(isLikelyMobile({ userAgent: undefined, maxTouchPoints: 5 })).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("shouldWarnBeforeLoad", () => {
|
||||
const MEDIUM_PEAK_BYTES = 555_000_000; // ~medium q4's estimated peak
|
||||
|
||||
test("never warns on desktop regardless of size or reported memory", () => {
|
||||
expect(
|
||||
shouldWarnBeforeLoad(
|
||||
{ userAgent: DESKTOP_WINDOWS_UA, maxTouchPoints: 0, deviceMemoryGB: 0.5 },
|
||||
MEDIUM_PEAK_BYTES,
|
||||
),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
test("warns on mobile with low reported deviceMemory", () => {
|
||||
// Safe budget = 1 GB * 0.5 = 500 MB, below the ~555 MB medium peak.
|
||||
expect(
|
||||
shouldWarnBeforeLoad(
|
||||
{ userAgent: ANDROID_UA, maxTouchPoints: 5, deviceMemoryGB: 1 },
|
||||
MEDIUM_PEAK_BYTES,
|
||||
),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
test("does not warn on mobile with ample reported deviceMemory", () => {
|
||||
expect(
|
||||
shouldWarnBeforeLoad(
|
||||
{ userAgent: ANDROID_UA, maxTouchPoints: 5, deviceMemoryGB: 8 },
|
||||
MEDIUM_PEAK_BYTES,
|
||||
),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
test("falls back to a flat threshold when deviceMemory is unreported (iOS)", () => {
|
||||
expect(
|
||||
shouldWarnBeforeLoad(
|
||||
{ userAgent: IPHONE_UA, maxTouchPoints: 5, deviceMemoryGB: undefined },
|
||||
MEDIUM_PEAK_BYTES,
|
||||
),
|
||||
).toBe(true);
|
||||
expect(
|
||||
shouldWarnBeforeLoad(
|
||||
{ userAgent: IPHONE_UA, maxTouchPoints: 5, deviceMemoryGB: undefined },
|
||||
50_000_000, // tiny model — well under the fallback threshold
|
||||
),
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
WHISPER_LANGUAGES,
|
||||
estimateDefaultDownloadBytes,
|
||||
estimateDownloadBytes,
|
||||
estimatePeakLoadBytes,
|
||||
formatModelSize,
|
||||
getFasterWhisperModel,
|
||||
getModel,
|
||||
@@ -163,6 +164,38 @@ describe("size estimates", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("estimatePeakLoadBytes (D-027)", () => {
|
||||
const medium = getModel("onnx-community/whisper-medium-ONNX")!;
|
||||
const turbo = getModel("onnx-community/whisper-large-v3-turbo")!;
|
||||
|
||||
test("is ~2x the larger of the two weight files, not their sum", () => {
|
||||
const dtype = resolveDefaultDtype(medium.id, "wasm"); // q4
|
||||
const peak = estimatePeakLoadBytes(medium, dtype);
|
||||
const download = estimateDownloadBytes(medium, dtype);
|
||||
// The combined download sums both files; the peak only doubles the
|
||||
// larger one — smaller than 2x the sum, but bigger than the sum alone
|
||||
// once the sequential-load patch keeps only one file resident at a time.
|
||||
expect(peak).toBeLessThan(2 * download);
|
||||
expect(peak).toBeGreaterThan(download);
|
||||
});
|
||||
|
||||
test("mixed per-part dtypes (fp16 encoder + q4 decoder) peak on the larger encoder", () => {
|
||||
const dtype = resolveDefaultDtype(turbo.id, "webgpu"); // fp16 encoder, q4 decoder
|
||||
const peak = estimatePeakLoadBytes(turbo, dtype);
|
||||
// large-v3-turbo's encoder (635M) dwarfs its decoder (174M) even before
|
||||
// fp16 vs q4 is factored in, so the encoder alone should set the peak.
|
||||
const encoderOnlyPeak = 2 * turbo.encoderParamsM * 1e6 * 2; // fp16 = 2 bytes/param
|
||||
expect(peak).toBeCloseTo(encoderOnlyPeak, -5);
|
||||
});
|
||||
|
||||
test("scales up with a larger model at the same dtype", () => {
|
||||
const tiny = getModel("onnx-community/whisper-tiny.en")!;
|
||||
expect(estimatePeakLoadBytes(medium, "q4")).toBeGreaterThan(
|
||||
estimatePeakLoadBytes(tiny, "q4"),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("WHISPER_LANGUAGES", () => {
|
||||
test("codes are unique, lowercase, and include common languages", () => {
|
||||
const codes = WHISPER_LANGUAGES.map((entry) => entry.code);
|
||||
|
||||
Reference in New Issue
Block a user