#!/usr/bin/env node // pre-screen-sources.js — Identify hyperscript sources that hang the WASM parser // // For each unique _="..." source in hs-behavioral-data.js, spawns a child process // that loads the WASM kernel + HS modules, then tries to compile. If it takes >3s, // it's marked as hanging. // // Output: // tests/playwright/hs-safe-sources.json — sources that compile OK (or error) // tests/playwright/hs-hanging-sources.json — sources that hang the parser/compiler // // Usage: node tests/playwright/pre-screen-sources.js const { execFileSync } = require('child_process'); const fs = require('fs'); const path = require('path'); const PROJECT_ROOT = path.resolve(__dirname, '../..'); const DATA_FILE = path.join(__dirname, 'hs-behavioral-data.js'); const WORKER_FILE = path.join(__dirname, '_pre-screen-worker-timed.js'); // Extract unique sources const data = require(DATA_FILE); const sourcesSet = new Set(); for (const t of data) { const matches = t.html.matchAll(/_=['"]([^'"]+)['"]/g); for (const m of matches) sourcesSet.add(m[1]); } const allSources = [...sourcesSet]; console.log(`Found ${allSources.length} unique hyperscript sources`); // Process in batches — each batch is a fresh child process const BATCH_SIZE = 15; const TIMEOUT_MS = 30000; // 30s for a batch of 15 (allows ~2s per source) const safe = []; const errors = []; const hanging = []; const timings = []; // {source, ms, status} function testBatch(sources) { // Write sources to temp file for the worker const tmpFile = '/tmp/hs-batch-input.json'; fs.writeFileSync(tmpFile, JSON.stringify(sources)); try { const output = execFileSync(process.execPath, [WORKER_FILE], { timeout: TIMEOUT_MS, encoding: 'utf8', env: { ...process.env, HS_BATCH_FILE: tmpFile }, cwd: PROJECT_ROOT, }); // Parse results from worker stdout const lines = output.trim().split('\n'); for (const line of lines) { if (line.startsWith('RESULT:')) { const { source, status, detail, ms } = JSON.parse(line.slice(7)); timings.push({ source, status, ms }); if (status === 'ok') { safe.push(source); } else if (status === 'error') { errors.push({ source, detail }); safe.push(source); // errors are still "safe" — they don't hang } else { hanging.push(source); } } } return true; } catch (e) { if (e.killed || e.signal === 'SIGTERM') { // Whole batch timed out — need to bisect return false; } // Other error (crash) — treat all as hanging console.error(` Batch crashed: ${e.message.slice(0, 200)}`); return false; } } function bisect(sources) { if (sources.length === 0) return; if (sources.length === 1) { // Single source that we know hangs console.log(` HANG: ${sources[0].slice(0, 80)}`); hanging.push(sources[0]); return; } console.log(` Bisecting ${sources.length} sources...`); // Try first half const mid = Math.ceil(sources.length / 2); const first = sources.slice(0, mid); const second = sources.slice(mid); if (!testBatch(first)) { bisect(first); } if (!testBatch(second)) { bisect(second); } } // Process batches const totalBatches = Math.ceil(allSources.length / BATCH_SIZE); for (let i = 0; i < allSources.length; i += BATCH_SIZE) { const batch = allSources.slice(i, i + BATCH_SIZE); const batchNum = Math.floor(i / BATCH_SIZE) + 1; process.stdout.write(`Batch ${batchNum}/${totalBatches} (${batch.length} sources)... `); if (testBatch(batch)) { console.log(`OK (${safe.length} safe, ${hanging.length} hanging so far)`); } else { console.log(`TIMEOUT — bisecting`); bisect(batch); console.log(` After bisect: ${safe.length} safe, ${hanging.length} hanging`); } } // Write results const safeFile = path.join(__dirname, 'hs-safe-sources.json'); const hangFile = path.join(__dirname, 'hs-hanging-sources.json'); const errFile = path.join(__dirname, 'hs-error-sources.json'); fs.writeFileSync(safeFile, JSON.stringify(safe, null, 2) + '\n'); fs.writeFileSync(hangFile, JSON.stringify(hanging, null, 2) + '\n'); fs.writeFileSync(errFile, JSON.stringify(errors, null, 2) + '\n'); console.log(`\nDone!`); console.log(` Safe (no hang): ${safe.length} (written to ${path.relative(PROJECT_ROOT, safeFile)})`); console.log(` Errors: ${errors.length} (written to ${path.relative(PROJECT_ROOT, errFile)})`); console.log(` Hanging: ${hanging.length} (written to ${path.relative(PROJECT_ROOT, hangFile)})`); if (hanging.length > 0) { console.log(`\nHanging sources:`); for (const s of hanging) { console.log(` - ${s}`); } } if (errors.length > 0) { console.log(`\nError sources:`); for (const e of errors) { console.log(` - ${e.source}`); console.log(` ${e.detail}`); } } // Timing summary — show slowest sources if (timings.length > 0) { timings.sort((a, b) => b.ms - a.ms); console.log(`\nSlowest sources (top 20):`); for (let i = 0; i < Math.min(20, timings.length); i++) { const t = timings[i]; console.log(` ${t.ms}ms [${t.status}] ${t.source.slice(0, 80)}`); } const total = timings.reduce((s, t) => s + t.ms, 0); const avg = Math.round(total / timings.length); console.log(`\nTotal: ${total}ms, Avg: ${avg}ms, Max: ${timings[0].ms}ms`); // Flag anything over 3s as "slow" (potential near-hang) const slow = timings.filter(t => t.ms > 3000); if (slow.length > 0) { console.log(`\nWARNING: ${slow.length} sources took >3s:`); for (const t of slow) { console.log(` ${t.ms}ms ${t.source}`); } } }