File size: 6,411 Bytes
f8d016c
29e20e0
f8d016c
29e20e0
 
 
 
 
 
 
 
 
fd2180b
945bdba
29e20e0
 
 
 
945bdba
 
 
 
 
 
 
 
 
29e20e0
 
 
355b8e2
29e20e0
 
 
 
 
355b8e2
29e20e0
 
 
 
 
 
 
 
 
 
317d5b5
 
 
11d0b50
 
 
 
 
 
317d5b5
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
29e20e0
 
317d5b5
29e20e0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
11d0b50
 
 
 
 
29e20e0
 
 
 
 
 
 
 
fd2180b
 
945bdba
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
fd2180b
 
945bdba
fd2180b
 
945bdba
fd2180b
 
 
 
945bdba
 
 
 
fd2180b
 
29e20e0
945bdba
 
 
29e20e0
 
 
11d0b50
 
 
 
 
 
 
 
 
 
 
 
 
 
 
29e20e0
 
 
 
 
 
f8d016c
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
import { chromium, firefox, webkit, Browser } from "playwright";
import { createServer } from "vite";
import { getArg } from "../core/args.js";

// CLI for running browser benchmarks headlessly via Playwright

const modelId = process.argv[2] || "Xenova/distilbert-base-uncased";
const task = process.argv[3] || "feature-extraction";

const mode = getArg("mode", "warm") as "warm" | "cold";
const repeats = Math.max(1, parseInt(getArg("repeats", "3") || "3", 10));
const device = getArg("device", "webgpu") as "webgpu" | "wasm";
const dtype = getArg("dtype"); // optional: fp32, fp16, q8, q4, etc.
const batchSize = Math.max(1, parseInt(getArg("batch-size", "1") || "1", 10));
const browserType = getArg("browser", "chromium") as "chromium" | "firefox" | "webkit";
const headed = getArg("headed") === "true";

async function main() {
  console.log(`Model      : ${modelId}`);
  console.log(`Task       : ${task}`);
  console.log(`Mode       : ${mode}`);
  console.log(`Repeats    : ${repeats}`);
  console.log(`Device     : ${device}`);
  console.log(`DType      : ${dtype || 'auto'}`);
  console.log(`Batch Size : ${batchSize}`);
  console.log(`Browser    : ${browserType}`);
  console.log(`Headed     : ${headed}`);

  // Start Vite dev server
  const server = await createServer({
    configFile: false, // Don't load vite.config.ts to avoid permission issues in read-only filesystems
    server: {
      port: 5173,
      strictPort: false,
    },
    logLevel: "error",
    cacheDir: process.env.VITE_CACHE_DIR || "node_modules/.vite",
  });

  await server.listen();

  const port = server.config.server.port || 5173;
  const url = `http://localhost:${port}`;

  console.log(`Vite server started at ${url}`);

  let browser: Browser;

  // Build args based on mode
  const args = device === "wasm"
    ? [
        "--disable-gpu",
        "--disable-software-rasterizer",
        // Increase WASM memory limits for large models
        "--js-flags=--max-old-space-size=8192",
      ]
    : [
        // Official WebGPU flags from Chrome team
        // https://developer.chrome.com/blog/supercharge-web-ai-testing#enable-webgpu
        "--enable-unsafe-webgpu",
        "--enable-features=Vulkan",
      ];

  // Add headless-specific flags only in headless mode
  if (!headed && device !== "wasm") {
    args.push(
      "--no-sandbox",
      "--headless=new",
      "--use-angle=vulkan",
      "--disable-vulkan-surface"
    );
  }

  const launchOptions = {
    headless: !headed,
    args,
  };

  switch (browserType) {
    case "firefox":
      browser = await firefox.launch(launchOptions);
      break;
    case "webkit":
      browser = await webkit.launch(launchOptions);
      break;
    case "chromium":
    default:
      browser = await chromium.launch(launchOptions);
      break;
  }

  try {
    const context = await browser.newContext();
    const page = await context.newPage();

    // Expose console logs
    page.on("console", (msg) => {
      const type = msg.type();
      if (type === "error" || type === "warning") {
        console.log(`[browser ${type}]`, msg.text());
      }
    });

    // Catch page errors
    page.on("pageerror", (error) => {
      console.error(`[browser error]`, error.message);
    });

    // Navigate to the app
    await page.goto(url);

    // Wait for the page to be ready
    await page.waitForSelector("#run");

    console.log("\nStarting benchmark...");

    // Check WebGPU availability if using webgpu device
    if (device === "webgpu") {
      const gpuInfo = await page.evaluate(async () => {
        if (!('gpu' in navigator)) {
          return { available: false, adapter: null, features: null };
        }
        try {
          const adapter = await (navigator as any).gpu.requestAdapter();
          if (!adapter) {
            return { available: false, adapter: null, features: null };
          }
          const features = Array.from(adapter.features || []);
          const limits = adapter.limits ? {
            maxTextureDimension2D: adapter.limits.maxTextureDimension2D,
            maxComputeWorkgroupSizeX: adapter.limits.maxComputeWorkgroupSizeX,
          } : null;
          return {
            available: true,
            adapterInfo: adapter.info ? adapter.info.description : 'Unknown',
            features,
            limits
          };
        } catch (e) {
          return { available: false, adapter: null, error: String(e) };
        }
      });

      if (!gpuInfo.available) {
        console.error("\n❌ WebGPU is not available in this browser!");
        console.error("Make sure to use --enable-unsafe-webgpu flag for Chromium.");
        if (gpuInfo.error) console.error("Error:", gpuInfo.error);
        throw new Error("WebGPU not available");
      }

      console.log("✓ WebGPU is available");
      console.log(`  Adapter: ${gpuInfo.adapterInfo}`);
      if (gpuInfo.features && gpuInfo.features.length > 0) {
        console.log(`  Features: ${gpuInfo.features.slice(0, 3).join(', ')}${gpuInfo.features.length > 3 ? '...' : ''}`);
      }
    }

    // Use the exposed CLI function from main.ts
    const result = await page.evaluate(({ modelId, task, mode, repeats, device, dtype, batchSize }) => {
      return (window as any).runBenchmarkCLI({ modelId, task, mode, repeats, device, dtype, batchSize });
    }, { modelId, task, mode, repeats, device, dtype, batchSize });

    console.log("\n" + JSON.stringify(result, null, 2));

    // Log helpful messages if there's an error
    if (result.error) {
      console.error("\n❌ Benchmark completed with error:");
      console.error(`   Type: ${result.error.type}`);
      console.error(`   Stage: ${result.error.stage}`);
      console.error(`   Message: ${result.error.message}`);

      if (result.error.type === "memory_error" && device === "wasm") {
        console.error("\nSuggestions:");
        console.error("  1. Try using --device=webgpu instead of --device=wasm");
        console.error("  2. Use a smaller model variant");
        console.error("  3. Reduce the batch size with --batch-size=1");
      }
    }

  } finally {
    await browser.close();
    await server.close();
  }
}

// Check if this module is being run directly (not imported)
const isMainModule = process.argv[1]?.includes('web/cli');

if (isMainModule) {
  main().catch((e) => {
    console.error(e);
    process.exit(1);
  });
}

export { main };