diff --git a/scripts/benchmark-dashboard.mjs b/scripts/benchmark-dashboard.mjs new file mode 100644 index 0000000..e32fcfc --- /dev/null +++ b/scripts/benchmark-dashboard.mjs @@ -0,0 +1,88 @@ +import { spawn } from 'node:child_process'; +import http from 'node:http'; +import { performance } from 'node:perf_hooks'; + +const PORT = 3456; +const REQUESTS = 2000; +const CONCURRENCY = 50; + +function sleep(ms) { + return new Promise(resolve => setTimeout(resolve, ms)); +} + +async function waitForServer() { + for (let i = 0; i < 50; i++) { + try { + await new Promise((resolve, reject) => { + const req = http.get(`http://localhost:${PORT}/dashboard/`, (res) => { + if (res.statusCode === 200) resolve(); + else reject(new Error(`Status ${res.statusCode}`)); + }); + req.on('error', reject); + req.end(); + }); + return; + } catch (e) { + await sleep(100); + } + } + throw new Error('Server did not start'); +} + +function makeRequest() { + return new Promise((resolve, 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 benchmark() { + console.log('Starting server...'); + 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.'); + + 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 { + serverProcess.kill(); + } +} + +benchmark().catch(console.error); diff --git a/src/dashboard.mjs b/src/dashboard.mjs index 7fde009..247c16b 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'; @@ -9,7 +10,7 @@ export async function cmdDashboard(port = DEFAULT_DASHBOARD_PORT) { // this file is in /src/dashboard.mjs, so '..' goes to const packageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'); - const server = http.createServer((req, res) => { + const server = http.createServer(async (req, res) => { // URL parsing const url = new URL(req.url, `http://${req.headers.host}`); let pathname = url.pathname; @@ -21,11 +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'); - if (fs.existsSync(userConfigPath)) { + if (await statSafe(userConfigPath)) { res.writeHead(200, { 'Content-Type': 'application/json' }); fs.createReadStream(userConfigPath).pipe(res); return; @@ -40,12 +49,14 @@ export async function cmdDashboard(port = DEFAULT_DASHBOARD_PORT) { let filePath = path.join(packageRoot, safePath); // If directory, try index.html - if (fs.existsSync(filePath) && fs.statSync(filePath).isDirectory()) { + 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 (!fs.existsSync(filePath) || !fs.statSync(filePath).isFile()) { + 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()