whitphx HF Staff commited on
Commit
29e20e0
·
1 Parent(s): b4867cc

Introduce Playwright to run the browser-based benchmark in headless mode

Browse files
.claude/settings.local.json ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "permissions": {
3
+ "allow": [
4
+ "Bash(npm run bench:*)",
5
+ "Bash(npm install:*)",
6
+ "Bash(npm run bench:cli:*)",
7
+ "Bash(timeout 120 npm run bench:cli -- Xenova/all-MiniLM-L6-v2 feature-extraction --mode warm --repeats 2 --device wasm)",
8
+ "Bash(timeout 120 npm run bench:cli -- Xenova/all-MiniLM-L6-v2 feature-extraction --mode warm --repeats 2 --device webgpu)",
9
+ "Bash(timeout 180 npm run bench:cli -- Xenova/all-MiniLM-L6-v2 feature-extraction --mode cold --repeats 2 --device wasm)"
10
+ ],
11
+ "deny": [],
12
+ "ask": []
13
+ }
14
+ }
bench-web/README.md CHANGED
@@ -1,18 +1,50 @@
1
- # bench-web (warm/cold, repeats, p50/p90)
2
 
3
  ## Setup
4
  ```bash
5
  cd bench-web
6
  npm i
 
7
  ```
8
 
9
- ## Run (dev)
10
  ```bash
11
  npm run dev
12
  # open http://localhost:5173
13
  ```
14
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
15
  ## How it works
 
16
  - **warm**: prefetch once (non-measured) → auto-reload → measure `repeats` times with disk caches populated.
17
  - **cold**: clear Cache Storage & IndexedDB, then measure in the same tab
18
  - Note: only the 1st iteration is strictly cold within a single page session.
 
 
 
 
 
 
 
 
1
+ # bench-web (warm/cold, repeats, p50/p90, CPU/GPU)
2
 
3
  ## Setup
4
  ```bash
5
  cd bench-web
6
  npm i
7
+ npm run bench:install # Install Playwright browsers for CLI mode
8
  ```
9
 
10
+ ## Run (Interactive UI)
11
  ```bash
12
  npm run dev
13
  # open http://localhost:5173
14
  ```
15
 
16
+ ## Run (CLI with Playwright)
17
+ ```bash
18
+ npm run bench:cli -- <model> <task> --mode <warm|cold> --repeats <n> --device <wasm|webgpu> [--browser <chromium|firefox|webkit>] [--headed true]
19
+ ```
20
+
21
+ ### Examples
22
+ ```bash
23
+ # WASM (CPU) benchmark
24
+ npm run bench:cli -- Xenova/all-MiniLM-L6-v2 feature-extraction --mode warm --repeats 3 --device wasm
25
+
26
+ # WebGPU benchmark
27
+ npm run bench:cli -- Xenova/all-MiniLM-L6-v2 feature-extraction --mode warm --repeats 3 --device webgpu
28
+
29
+ # Cold mode
30
+ npm run bench:cli -- Xenova/all-MiniLM-L6-v2 feature-extraction --mode cold --repeats 3 --device wasm
31
+
32
+ # With Firefox
33
+ npm run bench:cli -- Xenova/all-MiniLM-L6-v2 feature-extraction --mode warm --repeats 3 --device wasm --browser firefox
34
+
35
+ # Headed mode (for debugging)
36
+ npm run bench:cli -- Xenova/all-MiniLM-L6-v2 feature-extraction --mode warm --repeats 3 --device wasm --headed true
37
+ ```
38
+
39
  ## How it works
40
+ ### Interactive UI
41
  - **warm**: prefetch once (non-measured) → auto-reload → measure `repeats` times with disk caches populated.
42
  - **cold**: clear Cache Storage & IndexedDB, then measure in the same tab
43
  - Note: only the 1st iteration is strictly cold within a single page session.
44
+
45
+ ### CLI Mode
46
+ - Starts a Vite dev server and launches headless browser via Playwright
47
+ - **warm**: prefetch once (non-measured) → measure `repeats` times with caches populated (no page reload)
48
+ - **cold**: clears all caches before each run
49
+ - **device**: `wasm` for CPU, `webgpu` for GPU acceleration
50
+ - Supports Chromium, Firefox, and WebKit browsers
bench-web/index.html CHANGED
@@ -32,12 +32,17 @@
32
  </select>
33
  <label for="repeats">Repeats</label>
34
  <input id="repeats" type="number" value="3" min="1" style="width: 5rem;" />
 
 
 
 
 
35
  </div>
36
  <div class="row">
37
  <button id="run">Run benchmark</button>
38
  <span id="status"></span>
39
  </div>
40
- <pre id="out">{}</pre>
41
  <script type="module" src="/src/main.ts"></script>
42
  </body>
43
  </html>
 
32
  </select>
33
  <label for="repeats">Repeats</label>
34
  <input id="repeats" type="number" value="3" min="1" style="width: 5rem;" />
35
+ <label for="device">Device</label>
36
+ <select id="device">
37
+ <option value="webgpu" selected>webgpu</option>
38
+ <option value="wasm">wasm (CPU)</option>
39
+ </select>
40
  </div>
41
  <div class="row">
42
  <button id="run">Run benchmark</button>
43
  <span id="status"></span>
44
  </div>
45
+ <code><pre id="out">{}</pre></code>
46
  <script type="module" src="/src/main.ts"></script>
47
  </body>
48
  </html>
bench-web/package-lock.json CHANGED
@@ -11,6 +11,9 @@
11
  "@huggingface/transformers": "^3.7.4"
12
  },
13
  "devDependencies": {
 
 
 
14
  "typescript": "^5.9.3",
15
  "vite": "^7.1.7"
16
  }
@@ -927,6 +930,22 @@
927
  "node": ">=18.0.0"
928
  }
929
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
930
  "node_modules/@protobufjs/aspromise": {
931
  "version": "1.1.2",
932
  "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz",
@@ -1497,6 +1516,19 @@
1497
  "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
1498
  }
1499
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
1500
  "node_modules/global-agent": {
1501
  "version": "3.0.0",
1502
  "resolved": "https://registry.npmjs.org/global-agent/-/global-agent-3.0.0.tgz",
@@ -1702,6 +1734,53 @@
1702
  "integrity": "sha512-fnWVljUchTro6RiCFvCXBbNhJc2NijN7oIQxbwsyL0buWJPG85v81ehlHI9fXrJsMNgTofEoWIQeClKpgxFLrg==",
1703
  "license": "MIT"
1704
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1705
  "node_modules/postcss": {
1706
  "version": "8.5.6",
1707
  "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
@@ -1755,6 +1834,16 @@
1755
  "node": ">=12.0.0"
1756
  }
1757
  },
 
 
 
 
 
 
 
 
 
 
1758
  "node_modules/roarr": {
1759
  "version": "2.15.4",
1760
  "resolved": "https://registry.npmjs.org/roarr/-/roarr-2.15.4.tgz",
@@ -1945,6 +2034,26 @@
1945
  "license": "0BSD",
1946
  "optional": true
1947
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1948
  "node_modules/type-fest": {
1949
  "version": "0.13.1",
1950
  "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.13.1.tgz",
 
11
  "@huggingface/transformers": "^3.7.4"
12
  },
13
  "devDependencies": {
14
+ "@playwright/test": "^1.55.1",
15
+ "playwright": "^1.55.1",
16
+ "tsx": "^4.20.6",
17
  "typescript": "^5.9.3",
18
  "vite": "^7.1.7"
19
  }
 
930
  "node": ">=18.0.0"
931
  }
932
  },
933
+ "node_modules/@playwright/test": {
934
+ "version": "1.55.1",
935
+ "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.55.1.tgz",
936
+ "integrity": "sha512-IVAh/nOJaw6W9g+RJVlIQJ6gSiER+ae6mKQ5CX1bERzQgbC1VSeBlwdvczT7pxb0GWiyrxH4TGKbMfDb4Sq/ig==",
937
+ "dev": true,
938
+ "license": "Apache-2.0",
939
+ "dependencies": {
940
+ "playwright": "1.55.1"
941
+ },
942
+ "bin": {
943
+ "playwright": "cli.js"
944
+ },
945
+ "engines": {
946
+ "node": ">=18"
947
+ }
948
+ },
949
  "node_modules/@protobufjs/aspromise": {
950
  "version": "1.1.2",
951
  "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz",
 
1516
  "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
1517
  }
1518
  },
1519
+ "node_modules/get-tsconfig": {
1520
+ "version": "4.10.1",
1521
+ "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.10.1.tgz",
1522
+ "integrity": "sha512-auHyJ4AgMz7vgS8Hp3N6HXSmlMdUyhSUrfBF16w153rxtLIEOE+HGqaBppczZvnHLqQJfiHotCYpNhl0lUROFQ==",
1523
+ "dev": true,
1524
+ "license": "MIT",
1525
+ "dependencies": {
1526
+ "resolve-pkg-maps": "^1.0.0"
1527
+ },
1528
+ "funding": {
1529
+ "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1"
1530
+ }
1531
+ },
1532
  "node_modules/global-agent": {
1533
  "version": "3.0.0",
1534
  "resolved": "https://registry.npmjs.org/global-agent/-/global-agent-3.0.0.tgz",
 
1734
  "integrity": "sha512-fnWVljUchTro6RiCFvCXBbNhJc2NijN7oIQxbwsyL0buWJPG85v81ehlHI9fXrJsMNgTofEoWIQeClKpgxFLrg==",
1735
  "license": "MIT"
1736
  },
1737
+ "node_modules/playwright": {
1738
+ "version": "1.55.1",
1739
+ "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.55.1.tgz",
1740
+ "integrity": "sha512-cJW4Xd/G3v5ovXtJJ52MAOclqeac9S/aGGgRzLabuF8TnIb6xHvMzKIa6JmrRzUkeXJgfL1MhukP0NK6l39h3A==",
1741
+ "dev": true,
1742
+ "license": "Apache-2.0",
1743
+ "dependencies": {
1744
+ "playwright-core": "1.55.1"
1745
+ },
1746
+ "bin": {
1747
+ "playwright": "cli.js"
1748
+ },
1749
+ "engines": {
1750
+ "node": ">=18"
1751
+ },
1752
+ "optionalDependencies": {
1753
+ "fsevents": "2.3.2"
1754
+ }
1755
+ },
1756
+ "node_modules/playwright-core": {
1757
+ "version": "1.55.1",
1758
+ "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.55.1.tgz",
1759
+ "integrity": "sha512-Z6Mh9mkwX+zxSlHqdr5AOcJnfp+xUWLCt9uKV18fhzA8eyxUd8NUWzAjxUh55RZKSYwDGX0cfaySdhZJGMoJ+w==",
1760
+ "dev": true,
1761
+ "license": "Apache-2.0",
1762
+ "bin": {
1763
+ "playwright-core": "cli.js"
1764
+ },
1765
+ "engines": {
1766
+ "node": ">=18"
1767
+ }
1768
+ },
1769
+ "node_modules/playwright/node_modules/fsevents": {
1770
+ "version": "2.3.2",
1771
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
1772
+ "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
1773
+ "dev": true,
1774
+ "hasInstallScript": true,
1775
+ "license": "MIT",
1776
+ "optional": true,
1777
+ "os": [
1778
+ "darwin"
1779
+ ],
1780
+ "engines": {
1781
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
1782
+ }
1783
+ },
1784
  "node_modules/postcss": {
1785
  "version": "8.5.6",
1786
  "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
 
1834
  "node": ">=12.0.0"
1835
  }
1836
  },
1837
+ "node_modules/resolve-pkg-maps": {
1838
+ "version": "1.0.0",
1839
+ "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz",
1840
+ "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==",
1841
+ "dev": true,
1842
+ "license": "MIT",
1843
+ "funding": {
1844
+ "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1"
1845
+ }
1846
+ },
1847
  "node_modules/roarr": {
1848
  "version": "2.15.4",
1849
  "resolved": "https://registry.npmjs.org/roarr/-/roarr-2.15.4.tgz",
 
2034
  "license": "0BSD",
2035
  "optional": true
2036
  },
2037
+ "node_modules/tsx": {
2038
+ "version": "4.20.6",
2039
+ "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.20.6.tgz",
2040
+ "integrity": "sha512-ytQKuwgmrrkDTFP4LjR0ToE2nqgy886GpvRSpU0JAnrdBYppuY5rLkRUYPU1yCryb24SsKBTL/hlDQAEFVwtZg==",
2041
+ "dev": true,
2042
+ "license": "MIT",
2043
+ "dependencies": {
2044
+ "esbuild": "~0.25.0",
2045
+ "get-tsconfig": "^4.7.5"
2046
+ },
2047
+ "bin": {
2048
+ "tsx": "dist/cli.mjs"
2049
+ },
2050
+ "engines": {
2051
+ "node": ">=18.0.0"
2052
+ },
2053
+ "optionalDependencies": {
2054
+ "fsevents": "~2.3.3"
2055
+ }
2056
+ },
2057
  "node_modules/type-fest": {
2058
  "version": "0.13.1",
2059
  "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.13.1.tgz",
bench-web/package.json CHANGED
@@ -6,13 +6,18 @@
6
  "scripts": {
7
  "dev": "vite",
8
  "build": "vite build",
9
- "preview": "vite preview"
 
 
10
  },
11
  "dependencies": {
12
  "@huggingface/transformers": "^3.7.4"
13
  },
14
  "devDependencies": {
 
 
 
15
  "typescript": "^5.9.3",
16
  "vite": "^7.1.7"
17
  }
18
- }
 
6
  "scripts": {
7
  "dev": "vite",
8
  "build": "vite build",
9
+ "preview": "vite preview",
10
+ "bench:cli": "tsx src/cli.ts",
11
+ "bench:install": "playwright install"
12
  },
13
  "dependencies": {
14
  "@huggingface/transformers": "^3.7.4"
15
  },
16
  "devDependencies": {
17
+ "@playwright/test": "^1.55.1",
18
+ "playwright": "^1.55.1",
19
+ "tsx": "^4.20.6",
20
  "typescript": "^5.9.3",
21
  "vite": "^7.1.7"
22
  }
23
+ }
bench-web/src/cli.ts ADDED
@@ -0,0 +1,103 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { chromium, firefox, webkit, Browser, Page } from "playwright";
2
+ import { createServer } from "vite";
3
+
4
+ // CLI for running browser benchmarks headlessly via Playwright
5
+
6
+ const modelId = process.argv[2] || "Xenova/distilbert-base-uncased";
7
+ const task = process.argv[3] || "feature-extraction";
8
+
9
+ function getArg(name: string, def?: string) {
10
+ const i = process.argv.indexOf(`--${name}`);
11
+ if (i !== -1 && i + 1 < process.argv.length) return process.argv[i + 1];
12
+ return def;
13
+ }
14
+
15
+ const mode = getArg("mode", "warm") as "warm" | "cold";
16
+ const repeats = Math.max(1, parseInt(getArg("repeats", "3") || "3", 10));
17
+ const device = getArg("device", "webgpu") as "webgpu" | "wasm";
18
+ const browserType = getArg("browser", "chromium") as "chromium" | "firefox" | "webkit";
19
+ const headed = getArg("headed") === "true";
20
+
21
+ async function main() {
22
+ console.log(`Model : ${modelId}`);
23
+ console.log(`Task : ${task}`);
24
+ console.log(`Mode : ${mode}`);
25
+ console.log(`Repeats : ${repeats}`);
26
+ console.log(`Device : ${device}`);
27
+ console.log(`Browser : ${browserType}`);
28
+ console.log(`Headed : ${headed}`);
29
+
30
+ // Start Vite dev server
31
+ const server = await createServer({
32
+ server: {
33
+ port: 5173,
34
+ strictPort: false,
35
+ },
36
+ logLevel: "error",
37
+ });
38
+
39
+ await server.listen();
40
+
41
+ const port = server.config.server.port || 5173;
42
+ const url = `http://localhost:${port}`;
43
+
44
+ console.log(`Vite server started at ${url}`);
45
+
46
+ let browser: Browser;
47
+ const launchOptions = {
48
+ headless: !headed,
49
+ args: device === "wasm"
50
+ ? ["--disable-gpu", "--disable-software-rasterizer"]
51
+ : ["--enable-unsafe-webgpu", "--enable-features=Vulkan"]
52
+ };
53
+
54
+ switch (browserType) {
55
+ case "firefox":
56
+ browser = await firefox.launch(launchOptions);
57
+ break;
58
+ case "webkit":
59
+ browser = await webkit.launch(launchOptions);
60
+ break;
61
+ case "chromium":
62
+ default:
63
+ browser = await chromium.launch(launchOptions);
64
+ break;
65
+ }
66
+
67
+ try {
68
+ const context = await browser.newContext();
69
+ const page = await context.newPage();
70
+
71
+ // Expose console logs
72
+ page.on("console", (msg) => {
73
+ const type = msg.type();
74
+ if (type === "error" || type === "warning") {
75
+ console.log(`[browser ${type}]`, msg.text());
76
+ }
77
+ });
78
+
79
+ // Navigate to the app
80
+ await page.goto(url);
81
+
82
+ // Wait for the page to be ready
83
+ await page.waitForSelector("#run");
84
+
85
+ console.log("\nStarting benchmark...");
86
+
87
+ // Use the exposed CLI function from main.ts
88
+ const result = await page.evaluate(({ modelId, task, mode, repeats, device }) => {
89
+ return (window as any).runBenchmarkCLI({ modelId, task, mode, repeats, device });
90
+ }, { modelId, task, mode, repeats, device });
91
+
92
+ console.log("\n" + JSON.stringify(result, null, 2));
93
+
94
+ } finally {
95
+ await browser.close();
96
+ await server.close();
97
+ }
98
+ }
99
+
100
+ main().catch((e) => {
101
+ console.error(e);
102
+ process.exit(1);
103
+ });
bench-web/src/main.ts CHANGED
@@ -7,6 +7,7 @@ const modelEl = document.getElementById("model") as HTMLInputElement;
7
  const taskEl = document.getElementById("task") as HTMLSelectElement;
8
  const modeEl = document.getElementById("mode") as HTMLSelectElement;
9
  const repeatsEl = document.getElementById("repeats") as HTMLInputElement;
 
10
 
11
  function now() { return performance.now(); }
12
  function percentile(values: number[], q: number) {
@@ -15,11 +16,11 @@ function percentile(values: number[], q: number) {
15
  const i0 = Math.floor(i), i1 = Math.ceil(i);
16
  return i0 === i1 ? a[i0] : a[i0] + (a[i1] - a[i0]) * (i - i0);
17
  }
18
- async function clearCaches({ clearSession=false }: { clearSession?: boolean } = {}) {
19
  try {
20
  const keys = await caches.keys();
21
  await Promise.all(keys.map((k) => caches.delete(k)));
22
- } catch {}
23
  try {
24
  const anyIDB: any = indexedDB as any;
25
  if (typeof anyIDB.databases === "function") {
@@ -29,26 +30,26 @@ async function clearCaches({ clearSession=false }: { clearSession?: boolean } =
29
  indexedDB.deleteDatabase("transformers-cache");
30
  indexedDB.deleteDatabase("model-cache");
31
  }
32
- } catch {}
33
  try {
34
  localStorage.clear();
35
  if (clearSession) sessionStorage.clear();
36
- } catch {}
37
  }
38
- async function benchOnce(modelId: string, task: string) {
39
  const t0 = now();
40
- const pipe = await pipeline(task, modelId, {});
41
  const t1 = now();
42
  const t2 = now();
43
  await pipe("The quick brown fox jumps over the lazy dog.");
44
  const t3 = now();
45
  return { load_ms: +(t1 - t0).toFixed(1), first_infer_ms: +(t3 - t2).toFixed(1) };
46
  }
47
- async function runMany(modelId: string, task: string, repeats: number) {
48
  const loads: number[] = [];
49
  const firsts: number[] = [];
50
  for (let i = 0; i < repeats; i++) {
51
- const r = await benchOnce(modelId, task);
52
  loads.push(r.load_ms);
53
  firsts.push(r.first_infer_ms);
54
  }
@@ -57,11 +58,11 @@ async function runMany(modelId: string, task: string, repeats: number) {
57
  first_infer_ms: { p50: +percentile(firsts, 0.5).toFixed(1), p90: +percentile(firsts, 0.9).toFixed(1), raw: firsts },
58
  };
59
  }
60
- async function runCold(modelId: string, task: string, repeats: number) {
61
  statusEl.textContent = "clearing caches (cold)...";
62
  await clearCaches();
63
  statusEl.textContent = "running (cold)...";
64
- const metrics = await runMany(modelId, task, repeats);
65
  return {
66
  platform: "browser",
67
  runtime: navigator.userAgent,
@@ -69,32 +70,40 @@ async function runCold(modelId: string, task: string, repeats: number) {
69
  repeats,
70
  model: modelId,
71
  task,
 
72
  metrics,
73
  notes: "Only the 1st iteration is strictly cold in a single page session."
74
  };
75
  }
76
- async function runWarm(modelId: string, task: string, repeats: number) {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
77
  const flag = sessionStorage.getItem("__warm_ready__");
78
  if (!flag) {
79
  statusEl.textContent = "prefetching (warmup) ...";
80
- const p = await pipeline(task, modelId, {});
81
  await p("warmup");
82
- sessionStorage.setItem("__warm_ready__", JSON.stringify({ modelId, task, repeats }));
83
  location.reload();
84
  return null;
85
  } else {
86
  sessionStorage.removeItem("__warm_ready__");
87
- statusEl.textContent = "running (warm)...";
88
- const metrics = await runMany(modelId, task, repeats);
89
- return {
90
- platform: "browser",
91
- runtime: navigator.userAgent,
92
- mode: "warm",
93
- repeats,
94
- model: modelId,
95
- task,
96
- metrics
97
- };
98
  }
99
  }
100
  async function run() {
@@ -102,12 +111,14 @@ async function run() {
102
  const task = taskEl.value;
103
  const mode = modeEl.value as "warm" | "cold";
104
  const repeats = Math.max(1, parseInt(repeatsEl.value || "3", 10));
 
105
  out.textContent = "{}";
106
  if (mode === "cold") {
107
- const r = await runCold(modelId, task, repeats);
108
  if (r) { out.textContent = JSON.stringify(r, null, 2); statusEl.textContent = "done (cold)"; }
109
  } else {
110
- const r = await runWarm(modelId, task, repeats);
 
111
  if (r) { out.textContent = JSON.stringify(r, null, 2); statusEl.textContent = "done (warm)"; }
112
  }
113
  }
@@ -120,3 +131,13 @@ async function run() {
120
  btn.addEventListener("click", () => {
121
  run().catch((e) => { out.textContent = String(e); statusEl.textContent = "error"; console.error(e); });
122
  });
 
 
 
 
 
 
 
 
 
 
 
7
  const taskEl = document.getElementById("task") as HTMLSelectElement;
8
  const modeEl = document.getElementById("mode") as HTMLSelectElement;
9
  const repeatsEl = document.getElementById("repeats") as HTMLInputElement;
10
+ const deviceEl = document.getElementById("device") as HTMLSelectElement;
11
 
12
  function now() { return performance.now(); }
13
  function percentile(values: number[], q: number) {
 
16
  const i0 = Math.floor(i), i1 = Math.ceil(i);
17
  return i0 === i1 ? a[i0] : a[i0] + (a[i1] - a[i0]) * (i - i0);
18
  }
19
+ async function clearCaches({ clearSession = false }: { clearSession?: boolean } = {}) {
20
  try {
21
  const keys = await caches.keys();
22
  await Promise.all(keys.map((k) => caches.delete(k)));
23
+ } catch { }
24
  try {
25
  const anyIDB: any = indexedDB as any;
26
  if (typeof anyIDB.databases === "function") {
 
30
  indexedDB.deleteDatabase("transformers-cache");
31
  indexedDB.deleteDatabase("model-cache");
32
  }
33
+ } catch { }
34
  try {
35
  localStorage.clear();
36
  if (clearSession) sessionStorage.clear();
37
+ } catch { }
38
  }
39
+ async function benchOnce(modelId: string, task: string, device: string) {
40
  const t0 = now();
41
+ const pipe = await pipeline(task, modelId, { device });
42
  const t1 = now();
43
  const t2 = now();
44
  await pipe("The quick brown fox jumps over the lazy dog.");
45
  const t3 = now();
46
  return { load_ms: +(t1 - t0).toFixed(1), first_infer_ms: +(t3 - t2).toFixed(1) };
47
  }
48
+ async function runMany(modelId: string, task: string, repeats: number, device: string) {
49
  const loads: number[] = [];
50
  const firsts: number[] = [];
51
  for (let i = 0; i < repeats; i++) {
52
+ const r = await benchOnce(modelId, task, device);
53
  loads.push(r.load_ms);
54
  firsts.push(r.first_infer_ms);
55
  }
 
58
  first_infer_ms: { p50: +percentile(firsts, 0.5).toFixed(1), p90: +percentile(firsts, 0.9).toFixed(1), raw: firsts },
59
  };
60
  }
61
+ async function runCold(modelId: string, task: string, repeats: number, device: string) {
62
  statusEl.textContent = "clearing caches (cold)...";
63
  await clearCaches();
64
  statusEl.textContent = "running (cold)...";
65
+ const metrics = await runMany(modelId, task, repeats, device);
66
  return {
67
  platform: "browser",
68
  runtime: navigator.userAgent,
 
70
  repeats,
71
  model: modelId,
72
  task,
73
+ device,
74
  metrics,
75
  notes: "Only the 1st iteration is strictly cold in a single page session."
76
  };
77
  }
78
+ async function runWarmDirect(modelId: string, task: string, repeats: number, device: string) {
79
+ statusEl.textContent = "prefetching (warmup) ...";
80
+ const p = await pipeline(task, modelId, { device });
81
+ await p("warmup");
82
+ statusEl.textContent = "running (warm)...";
83
+ const metrics = await runMany(modelId, task, repeats, device);
84
+ return {
85
+ platform: "browser",
86
+ runtime: navigator.userAgent,
87
+ mode: "warm",
88
+ repeats,
89
+ model: modelId,
90
+ task,
91
+ device,
92
+ metrics
93
+ };
94
+ }
95
+ async function runWarm(modelId: string, task: string, repeats: number, device: string) {
96
  const flag = sessionStorage.getItem("__warm_ready__");
97
  if (!flag) {
98
  statusEl.textContent = "prefetching (warmup) ...";
99
+ const p = await pipeline(task, modelId, { device });
100
  await p("warmup");
101
+ sessionStorage.setItem("__warm_ready__", JSON.stringify({ modelId, task, repeats, device }));
102
  location.reload();
103
  return null;
104
  } else {
105
  sessionStorage.removeItem("__warm_ready__");
106
+ return await runWarmDirect(modelId, task, repeats, device);
 
 
 
 
 
 
 
 
 
 
107
  }
108
  }
109
  async function run() {
 
111
  const task = taskEl.value;
112
  const mode = modeEl.value as "warm" | "cold";
113
  const repeats = Math.max(1, parseInt(repeatsEl.value || "3", 10));
114
+ const device = deviceEl.value;
115
  out.textContent = "{}";
116
  if (mode === "cold") {
117
+ const r = await runCold(modelId, task, repeats, device);
118
  if (r) { out.textContent = JSON.stringify(r, null, 2); statusEl.textContent = "done (cold)"; }
119
  } else {
120
+ const r = await runWarm(modelId, task, repeats, device);
121
+ console.log("warm run result:", r);
122
  if (r) { out.textContent = JSON.stringify(r, null, 2); statusEl.textContent = "done (warm)"; }
123
  }
124
  }
 
131
  btn.addEventListener("click", () => {
132
  run().catch((e) => { out.textContent = String(e); statusEl.textContent = "error"; console.error(e); });
133
  });
134
+
135
+ // Expose for CLI use
136
+ (window as any).runBenchmarkCLI = async function (params: { modelId: string, task: string, mode: string, repeats: number, device: string }) {
137
+ if (params.mode === "cold") {
138
+ return await runCold(params.modelId, params.task, params.repeats, params.device);
139
+ } else {
140
+ // For warm, use the direct function that skips reload logic
141
+ return await runWarmDirect(params.modelId, params.task, params.repeats, params.device);
142
+ }
143
+ };