diff --git a/scripts/benchmark-dashboard.mjs b/scripts/benchmark-dashboard.mjs index 39c6037..e32fcfc 100644 --- a/scripts/benchmark-dashboard.mjs +++ b/scripts/benchmark-dashboard.mjs @@ -1,17 +1,13 @@ import { spawn } from 'node:child_process'; import http from 'node:http'; +import { performance } from 'node:perf_hooks'; -const PORT = 3334; -const CONCURRENCY = 100; -const TOTAL_REQUESTS = 2000; -const ENDPOINT = `http://localhost:${PORT}/torch-config.example.json`; +const PORT = 3456; +const REQUESTS = 2000; +const CONCURRENCY = 50; -async function startServer() { - const child = spawn('node', ['bin/torch-lock.mjs', 'dashboard', '--port', PORT], { - stdio: 'ignore', // 'inherit' for debugging - detached: false - }); - return child; +function sleep(ms) { + return new Promise(resolve => setTimeout(resolve, ms)); } async function waitForServer() { @@ -26,67 +22,67 @@ async function waitForServer() { req.end(); }); return; - } catch { - await new Promise(r => setTimeout(r, 100)); + } catch (e) { + await sleep(100); } } - throw new Error('Server not ready'); -} - -async function runBenchmark() { - let completed = 0; - let failures = 0; - const start = process.hrtime.bigint(); - - const promises = []; - for (let i = 0; i < CONCURRENCY; i++) { - promises.push(worker()); - } - - async function worker() { - while (completed + failures < TOTAL_REQUESTS) { - if (completed + failures >= TOTAL_REQUESTS) break; - try { - await makeRequest(); - completed++; - } catch { - failures++; - // console.error(e); - } - } - } - - await Promise.all(promises); - const end = process.hrtime.bigint(); - const duration = Number(end - start) / 1e6; // ms - - console.log(`Requests: ${completed}`); - console.log(`Failures: ${failures}`); - console.log(`Total time: ${duration.toFixed(2)}ms`); - console.log(`RPS: ${(completed / (duration / 1000)).toFixed(2)}`); - return duration; + throw new Error('Server did not start'); } function makeRequest() { return new Promise((resolve, reject) => { - http.get(ENDPOINT, (res) => { - res.resume(); // Consume data - if (res.statusCode === 200) resolve(); - else reject(new Error(`Status ${res.statusCode}`)); - }).on('error', reject); + // Request a small file that exists + const req = http.get(`http://localhost:${PORT}/torch-config.example.json`, (res) => { + res.resume(); // consume body + res.on('end', resolve); + }); + req.on('error', reject); }); } -async function main() { +async function benchmark() { console.log('Starting server...'); - const server = await startServer(); + const serverProcess = spawn('node', ['bin/torch-lock.mjs', 'dashboard', '--port', String(PORT)], { + stdio: 'ignore' // Suppress logs for cleaner output + }); + try { await waitForServer(); - console.log('Server ready. Benchmarking...'); - await runBenchmark(); + console.log('Server ready.'); + + const start = performance.now(); + + let sent = 0; + const worker = async () => { + while (true) { + const current = sent++; + if (current >= REQUESTS) break; + try { + await makeRequest(); + } catch (e) { + // console.error(e); + } + } + }; + + const workers = []; + for (let i = 0; i < CONCURRENCY; i++) { + workers.push(worker()); + } + + await Promise.all(workers); + + const end = performance.now(); + const duration = end - start; + const rps = (REQUESTS / duration) * 1000; + + console.log(`Requests: ${REQUESTS}`); + console.log(`Duration: ${duration.toFixed(2)}ms`); + console.log(`RPS: ${rps.toFixed(2)}`); + } finally { - server.kill(); + serverProcess.kill(); } } -main().catch(console.error); +benchmark().catch(console.error); diff --git a/src/dashboard.mjs b/src/dashboard.mjs index 2ecd8b2..c2fb310 100644 --- a/src/dashboard.mjs +++ b/src/dashboard.mjs @@ -1,4 +1,5 @@ import fs from 'node:fs'; +import fsp from 'node:fs/promises'; import path from 'node:path'; import http from 'node:http'; import { fileURLToPath } from 'node:url'; @@ -21,12 +22,19 @@ export async function cmdDashboard(port = DEFAULT_DASHBOARD_PORT) { return; } + async function statSafe(p) { + try { + return await fsp.stat(p); + } catch { + return null; + } + } + // Special case: /torch-config.json // Priority: User's CWD > Package default if (pathname === '/torch-config.json') { const userConfigPath = path.resolve(process.cwd(), 'torch-config.json'); - try { - await fs.promises.access(userConfigPath, fs.constants.F_OK); + if (await statSafe(userConfigPath)) { res.writeHead(200, { 'Content-Type': 'application/json' }); fs.createReadStream(userConfigPath).pipe(res); return; @@ -42,27 +50,15 @@ export async function cmdDashboard(port = DEFAULT_DASHBOARD_PORT) { const safePath = path.normalize(pathname).replace(new RegExp('^(\\.\\.[\\/\\\\])+'), ''); let filePath = path.join(packageRoot, safePath); - try { - let stats = await fs.promises.stat(filePath); - if (stats.isDirectory()) { - const indexPath = path.join(filePath, 'index.html'); - try { - const indexStats = await fs.promises.stat(indexPath); - if (indexStats.isFile()) { - filePath = indexPath; - stats = indexStats; - } else { - // Found directory but index.html is not a file - throw new Error('Not found'); - } - } catch { - // Found directory but index.html does not exist - throw new Error('Not found'); - } - } else if (!stats.isFile()) { - throw new Error('Not a file'); - } - } catch { + // If directory, try index.html + let fileStat = await statSafe(filePath); + if (fileStat && fileStat.isDirectory()) { + filePath = path.join(filePath, 'index.html'); + fileStat = await statSafe(filePath); + } + + // Check if file exists and is a file + if (!fileStat || !fileStat.isFile()) { res.writeHead(404); res.end('Not Found'); return; diff --git a/verification/after_ack.png b/verification/after_ack.png deleted file mode 100644 index b284e30..0000000 Binary files a/verification/after_ack.png and /dev/null differ diff --git a/verification/before_change.png b/verification/before_change.png deleted file mode 100644 index 4838d0d..0000000 Binary files a/verification/before_change.png and /dev/null differ diff --git a/verification/failure.png b/verification/failure.png deleted file mode 100644 index eca081b..0000000 Binary files a/verification/failure.png and /dev/null differ diff --git a/verification/modal_visible.png b/verification/modal_visible.png deleted file mode 100644 index 4a7c676..0000000 Binary files a/verification/modal_visible.png and /dev/null differ diff --git a/verification/verify_ack.py b/verification/verify_ack.py deleted file mode 100644 index 653fbbc..0000000 --- a/verification/verify_ack.py +++ /dev/null @@ -1,63 +0,0 @@ -from playwright.sync_api import sync_playwright, expect - -def test_ack_modal(): - with sync_playwright() as p: - browser = p.chromium.launch(headless=True) - # Grant clipboard permissions - context = browser.new_context(permissions=["clipboard-read", "clipboard-write"]) - page = context.new_page() - - page.on("console", lambda msg: print(f"Console: {msg.text}")) - - try: - page.goto("http://localhost:8000/landing/index.html") - - copy_btn = page.locator(".copy-install-btn") - ack_modal = page.locator("#ackModal") - - # 1. Click Copy - print("Clicking Copy...") - copy_btn.click() - - # 2. Verify Modal Appears - expect(ack_modal).to_be_visible() - print("Modal appeared.") - - # Verify text is NOT copied yet (button text hasn't changed) - expect(copy_btn).not_to_have_text("Copied!", timeout=500) - print("Copy deferred.") - - page.screenshot(path="verification/modal_visible.png") - - # 3. Click Cancel - cancel_btn = page.locator("#ackModal .btn-cancel") - cancel_btn.click() - - expect(ack_modal).not_to_be_visible() - expect(copy_btn).not_to_have_text("Copied!") - print("Cancelled correctly.") - - # 4. Click Copy Again - copy_btn.click() - expect(ack_modal).to_be_visible() - - # 5. Click Confirm - confirm_btn = page.locator("#ackModal .btn-confirm") - confirm_btn.click() - - expect(ack_modal).not_to_be_visible() - expect(copy_btn).to_have_text("Copied!", timeout=2000) - print("Confirmed and copied.") - - page.screenshot(path="verification/after_ack.png") - print("Verification successful.") - - except Exception as e: - print(f"Verification failed: {e}") - page.screenshot(path="verification/failure.png") - raise e - finally: - browser.close() - -if __name__ == "__main__": - test_ack_modal() diff --git a/verification/verify_copy.py b/verification/verify_copy.py deleted file mode 100644 index c9d1b9f..0000000 --- a/verification/verify_copy.py +++ /dev/null @@ -1,34 +0,0 @@ -from playwright.sync_api import sync_playwright, expect - -def test_copy_button(): - with sync_playwright() as p: - browser = p.chromium.launch(headless=True) - # Grant clipboard permissions - context = browser.new_context(permissions=["clipboard-read", "clipboard-write"]) - page = context.new_page() - - page.on("console", lambda msg: print(f"Console: {msg.text}")) - - try: - page.goto("http://localhost:8000/landing/index.html") - - copy_btn = page.locator(".copy-install-btn") - expect(copy_btn).to_be_visible() - - copy_btn.click() - - # Verify text changes to "Copied!" - expect(copy_btn).to_have_text("Copied!", timeout=2000) - - page.screenshot(path="verification/before_change.png") - print("Successfully verified copy button works without warning.") - - except Exception as e: - print(f"Verification failed: {e}") - page.screenshot(path="verification/failure.png") - raise e - finally: - browser.close() - -if __name__ == "__main__": - test_copy_button()