browser-page.mjs (8503B)
1 // Driven by browser.mjs over CDP; every operation has a main-thread timeout. 2 const assert = (ok,message) => { if (!ok) throw new Error(message); }; 3 const bytes = async path => { const r=await fetch(path); if(!r.ok)throw new Error(path); return r.arrayBuffer(); }; 4 const digest = async b => Array.from(new Uint8Array(await crypto.subtle.digest('SHA-256',b)),x=>x.toString(16).padStart(2,'0')).join(''); 5 function client() { 6 const worker = new Worker(new URL('./worker.mjs',import.meta.url),{type:'module'}); 7 let id=0; const waiting=new Map(), unsolicited=[]; 8 const terminate = () => { worker.terminate(); for(const w of waiting.values()){clearTimeout(w.timer);w.reject(new Error('Terminated'));} waiting.clear(); }; 9 worker.onmessage=({data})=>{ 10 const w=waiting.get(data.id); 11 if(!w){unsolicited.push(data);return;} 12 waiting.delete(data.id);clearTimeout(w.timer); 13 if(data.type==='error')w.reject(new Error(data.message));else w.resolve(data); 14 }; 15 worker.onerror=e=>{for(const w of waiting.values()){clearTimeout(w.timer);w.reject(new Error(e.message));}waiting.clear();}; 16 return {terminate,unsolicited,send:m=>new Promise((resolve,reject)=>{ 17 const request=++id; 18 const timer=setTimeout(()=>{waiting.delete(request);terminate();reject(new Error('Worker deadline'));},30_000); 19 waiting.set(request,{resolve,reject,timer});worker.postMessage({...m,id:request}); 20 })}; 21 } 22 // Only used to validate small, complete snapshots against independent goldens. 23 function snapshotTree(snapshot) { 24 assert(!snapshot.truncated,'Unexpected truncated fixture preview'); 25 const nodes=new Map(snapshot.nodes.map(n=>[n.id,n])); 26 const aliases=new Map((snapshot.aliases??[]).map(a=>[a.from,a.to])); 27 const pending=[snapshot.root], chars=[]; 28 while(pending.length) { 29 assert(chars.length<10000,'Preview fixture expansion limit'); 30 let id=pending.pop(); 31 for(let i=0;aliases.has(id);i++){assert(i<256,'Alias cycle');id=aliases.get(id);} 32 const node=nodes.get(id);assert(node,'Missing preview node'); 33 chars.push(String(node.children.length));pending.push(...[...node.children].reverse()); 34 } 35 return chars.join(''); 36 } 37 export async function run() { 38 const wasm=await bytes('/arboricx.wasm'), identity=await bytes('/test/fixtures/id.arboricx'); 39 const goldens=await (await fetch('/ext/demo-site/test/compiler-conformance.json')).json(); 40 const compiler=await bytes(`/ext/demo-site/bundles/${goldens.compilerBundleSha256}.arboricx`); 41 const init=(host,bundle,hash,name)=>({type:'init',backend:host==='wasm'?'zig-wasm':'js',bundle,hash,name,artifacts:{wasm}}); 42 const idHash=await digest(identity), samples=[], controls=[]; 43 let ticks=0;const heartbeat=setInterval(()=>ticks++,10); 44 try { 45 for(const host of ['js','wasm']) { 46 const c=client(); 47 try { 48 // A rejected hash never constructs the evaluator. Retry initialization. 49 await c.send(init(host,identity,'0'.repeat(64))).then(()=>{throw new Error('Accepted wrong hash');},e=>assert(/hash mismatch/.test(e.message),'Wrong hash error: '+e.message)); 50 const malformed=new Uint8Array(identity.byteLength+1);malformed.set(new Uint8Array(identity)); 51 let invalidRejected=false; 52 try{await c.send(init(host,malformed.buffer,await digest(malformed)));}catch{invalidRejected=true;} 53 assert(invalidRejected,'Accepted malformed bundle with matching hash'); 54 const ready=await c.send(init(host,identity,idHash)); 55 assert(ready.backend===(host==='wasm'?'zig-wasm':'js'),'Wrong selected backend'); 56 await c.send(init(host==='wasm'?'js':'wasm',identity,idHash)).then( 57 ()=>{throw new Error('Switched live session backend');}, 58 e=>assert(/Already initialized/.test(e.message),'Wrong session-switch error')); 59 let surrogateRejected=false; 60 try{await c.send({type:'text',value:'\ud800'});}catch{surrogateRejected=true;} 61 assert(surrogateRejected,'Accepted unpaired surrogate'); 62 const unicode='\ufeff\0λ雪🙂\uffff\u{10ffff}'; 63 for(let i=0;i<10;i++)assert((await c.send({type:'text',value:unicode})).value===unicode,'Unicode mismatch'); 64 } finally {c.terminate();} 65 controls.push(`${host}: hash/profile rejection, invalid surrogate recovery and Unicode`); 66 } 67 // Alternate fresh Worker order. Capture compilation (lazy definition handles) 68 // separately from independent full definition/result validation. 69 for(let round=0;round<3;round++)for(const host of round%2?['wasm','js']:['js','wasm']) { 70 const c=client(); 71 try { 72 await c.send(init(host,compiler,goldens.compilerBundleSha256,goldens.export)); 73 let repeatStats; 74 for(let index=0;index<4;index++) { 75 const golden=goldens.cases[index]; 76 const compiled=await c.send({type:'compile',source:golden.source}); 77 assert(JSON.stringify([...compiled.names].sort())===JSON.stringify(Object.keys(golden.definitions).sort()),'Definition names mismatch'); 78 const observeStart=performance.now(); 79 for(const [name,value] of Object.entries(golden.definitions)) 80 assert((await c.send({type:'inspect',name,arguments:[]})).value===value,`Definition mismatch ${name}`); 81 assert((await c.send({type:'inspect',name:golden.export,arguments:golden.arguments})).value===golden.result,'Applied result mismatch'); 82 const observedMs=performance.now()-observeStart; 83 let previewMs=0; 84 for(const [name,value] of Object.entries(golden.definitions)) { 85 const preview=await c.send({type:'preview',name,limit:256}); 86 assert(snapshotTree(preview.value)===value,`Preview mismatch ${name}`); 87 previewMs+=preview.ms; 88 } 89 const {stats}=await c.send({type:'collect'}); 90 samples.push({host,round,case:index,compileMs:compiled.ms,observeRoundTripMs:observedMs,previewMs,stats}); 91 } 92 // Replace retained exports, then verify compiler-only/definition roots 93 // stabilize live counts after warm-up and explicit collection. 94 for(let i=0;i<8;i++) { 95 await c.send({type:'compile',source:goldens.cases[0].source}); 96 assert((await c.send({type:'inspect',name:'id',arguments:goldens.cases[0].arguments})).value===goldens.cases[0].result,'Retained compiler changed'); 97 const {stats}=await c.send({type:'collect'}); 98 if(host==='wasm' && repeatStats)assert(stats.owned===repeatStats.owned && stats.liveAfterCollect===repeatStats.liveAfterCollect,'Retained-session leak: '+JSON.stringify({stats,repeatStats})); 99 repeatStats=stats; 100 } 101 for(const bad of goldens.cases.filter(x=>x.diagnostic)) { 102 let diagnostic; 103 try {await c.send({type:'compile',source:bad.source});} catch(error) {diagnostic=error.message;} 104 assert(diagnostic?.includes(bad.diagnostic),'Compiler diagnostic mismatch: '+diagnostic); 105 assert((await c.send({type:'inspect',name:'id',arguments:goldens.cases[0].arguments})).value===goldens.cases[0].result,'Failed compilation destroyed retained exports'); 106 } 107 await c.send({type:'compile',source:goldens.cases[0].source}); 108 assert((await c.send({type:'inspect',name:'id',arguments:goldens.cases[0].arguments})).value===goldens.cases[0].result,'Compiler failed to recover'); 109 } finally {c.terminate();} 110 } 111 for(const host of ['js','wasm']) { 112 const c=client(); 113 try { 114 await c.send(init(host,identity,idHash)); 115 const before=ticks; 116 assert((await c.send({type:'diverge'})).type==='running','No divergence acknowledgment'); 117 await new Promise(r=>setTimeout(r,100)); 118 assert(ticks>before,'Main thread blocked by evaluator'); 119 c.terminate(); 120 await new Promise(r=>setTimeout(r,100)); 121 assert(c.unsolicited.length===0,'Divergent evaluator returned before Stop'); 122 } finally {c.terminate();} 123 // A separate fresh Worker must still initialize and execute after Stop. 124 const fresh=client();try{await fresh.send(init(host,identity,idHash));assert((await fresh.send({type:'text',value:'restart🙂'})).value==='restart🙂','Restart failed');}finally{fresh.terminate();} 125 controls.push(`${host}: hard termination, responsive main thread and fresh restart`); 126 } 127 return {schema:1,userAgent:navigator.userAgent,wasmSha256:await digest(wasm),compilerSha256:goldens.compilerBundleSha256,samples,controls}; 128 } finally {clearInterval(heartbeat);} 129 }