Spaces:
Runtime error
Runtime error
Introduce Playwright to run the browser-based benchmark in headless mode
Browse files- .claude/settings.local.json +14 -0
- bench-web/README.md +34 -2
- bench-web/index.html +6 -1
- bench-web/package-lock.json +109 -0
- bench-web/package.json +7 -2
- bench-web/src/cli.ts +103 -0
- bench-web/src/main.ts +47 -26
.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 (
|
| 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
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 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 |
+
};
|