46 lines
14 KiB
JavaScript
46 lines
14 KiB
JavaScript
import fs from 'node:fs/promises';
|
|
import vm from 'node:vm';
|
|
import os from 'node:os';
|
|
import {Worker} from 'node:worker_threads';
|
|
import {performance} from 'node:perf_hooks';
|
|
|
|
const html=await fs.readFile(new URL('../index.html',import.meta.url),'utf8');
|
|
const scripts=[...html.matchAll(/<script[^>]*>([\s\S]*?)<\/script>/g)].map(m=>m[1]);
|
|
const kc={};vm.runInNewContext(scripts[0],kc);const kernels=kc.MANDEL_WEBGPU_KERNELS;
|
|
const element={width:800,height:600,clientWidth:800,clientHeight:600,style:{},hidden:false,value:'',textContent:'',disabled:false,addEventListener(){},classList:{toggle(){}},setAttribute(){},removeAttribute(){},showModal(){},close(){},toBlob(){},getBoundingClientRect(){return{left:0,top:0,width:800,height:600}}};
|
|
const app={MANDEL_WEBGPU_KERNELS:kernels,document:{querySelector:()=>element,getElementById:()=>element,documentElement:{classList:{toggle(){}}},body:{classList:{toggle(){}},appendChild(){}}},navigator:{hardwareConcurrency:4,deviceMemory:8},location:{search:'',hash:''},history:{replaceState(){}},performance,URLSearchParams,URL:{createObjectURL:()=>'',revokeObjectURL(){}},TextEncoder,Blob:class{},console,addEventListener(){},requestAnimationFrame:()=>1,cancelAnimationFrame(){},setTimeout,clearTimeout,setInterval,clearInterval,innerWidth:800,innerHeight:600,devicePixelRatio:1,localStorage:{getItem(){return null},setItem(){},removeItem(){}}};
|
|
const cut=scripts[1].indexOf('// ── boot / teardown');
|
|
vm.runInNewContext(scripts[1].slice(0,cut)+`\nglobalThis.internal={fastReferenceWorkerSource,cpuFallbackWorkerSource,chooseBackend};})();`,app);
|
|
const internal=app.internal;
|
|
const corpus=JSON.parse(await fs.readFile(new URL('./benchmark-corpus.json',import.meta.url),'utf8')).views;
|
|
const median=a=>{const b=[...a].sort((x,y)=>x-y),m=b.length>>1;return b.length%2?b[m]:(b[m-1]+b[m])/2};
|
|
const f=Math.fround;
|
|
function analyticF32(cr,ci){cr=f(cr);ci=f(ci);const y2=f(ci*ci),x=f(cr-.25),q=f(f(x*x)+y2),lhs=f(q*f(q+x)),rhs=f(.25*y2),margin=f(16*5.960464477539063e-8*f(f(Math.abs(lhs)+Math.abs(rhs))+1));if(lhs<f(rhs-margin))return true;const x2=f(cr+1),bulb=f(f(x2*x2)+y2),bm=f(16*5.960464477539063e-8*f(f(Math.abs(bulb)+.0625)+1));return bulb<f(.0625-bm)}
|
|
function coordF32(view,x,y,w,h){const scale=f(f(view.span)/f(w)),cr=f(f(view.re)+f(f(f(x+.5)-f(.5*w))*scale)),ci=f(f(view.im)+f(f(f(.5*h)-f(y+.5))*scale));return[cr,ci]}
|
|
function cMulF(a,b){return[f(f(a[0]*b[0])-f(a[1]*b[1])),f(f(a[0]*b[1])+f(a[1]*b[0]))]}
|
|
function iterateProd(cr,ci,maxIter,periodic){if(analyticF32(cr,ci))return{cls:'analytic',work:0,iter:0};let zr=f(0),zi=f(0),n=0,cycleR=f(0),cycleI=f(0),power=1,lam=0,candPeriod=0,candAge=0,candR=0,candI=0,candMul=[1,0],candRepeats=0;while(n<maxIter){const prev=[zr,zi],zr2=f(zr*zr),zi2=f(zi*zi);zi=f(f(f(2*zr)*zi)+ci);zr=f(f(zr2-zi2)+cr);n++;const mag=f(f(zr*zr)+f(zi*zi));if(mag>4)return{cls:'escaped',work:n,iter:n};if(periodic&&candPeriod>0){candMul=cMulF(candMul,[f(2*prev[0]),f(2*prev[1])]);candAge++;if(candAge>=candPeriod){const tol=f(4*5.960464477539063e-8*f(1+Math.max(Math.abs(zr),Math.abs(zi)))),close=Math.max(Math.abs(f(zr-candR)),Math.abs(f(zi-candI)))<=tol,attracting=f(f(candMul[0]*candMul[0])+f(candMul[1]*candMul[1]))<.9025;if(close&&attracting){candRepeats++;if(candRepeats>=2)return{cls:'heuristic',work:n,iter:n};candR=zr;candI=zi;candAge=0;candMul=[1,0]}else{candPeriod=0;candRepeats=0}}}if(periodic&&candPeriod===0&&n>=64){const tol=f(4*5.960464477539063e-8*f(1+Math.max(Math.abs(zr),Math.abs(zi))));if(Math.max(Math.abs(f(zr-cycleR)),Math.abs(f(zi-cycleI)))<=tol){const period=lam+1;if(period<=2048){candPeriod=period;candAge=0;candR=zr;candI=zi;candMul=[1,0];candRepeats=0}}}lam++;if(lam>=power){cycleR=zr;cycleI=zi;power=Math.min(65536,power*2);lam=0}}return{cls:'limit',work:maxIter,iter:maxIter}}
|
|
function iterateOracle(cr,ci,maxIter){let zr=0,zi=0;for(let n=1;n<=maxIter;n++){const zr2=zr*zr,zi2=zi*zi;zi=2*zr*zi+ci;zr=zr2-zi2+cr;if(zr*zr+zi*zi>4)return n}return 0}
|
|
function periodicityBench(){const w=96,h=64,maxIter=8192,oracleIter=65536,rows=[];for(const view of corpus){const snap={bits:256,re:fromDec(String(view.re)),im:fromDec(String(view.im)),span:fromDec(String(view.span))},backend=internal.chooseBackend(snap,800).backend;let baseWork=0,heurWork=0,candidates=0,falseCandidates=0,baseLimit=0,heurLimit=0;const t0=performance.now();for(let y=0;y<h;y++)for(let x=0;x<w;x++){const [cr,ci]=coordF32(view,x,y,w,h),b=iterateProd(cr,ci,maxIter,false),q=iterateProd(cr,ci,maxIter,true);baseWork+=b.work;heurWork+=q.work;if(b.cls==='limit')baseLimit++;if(q.cls==='limit')heurLimit++;if(q.cls==='heuristic'){candidates++;if(iterateOracle(cr,ci,oracleIter)>q.iter)falseCandidates++}}rows.push({view:view.name,productionBackend:backend,pixels:w*h,baseWork,heuristicWork:heurWork,workReduction:backend==='direct'?1-heurWork/Math.max(1,baseWork):null,candidates:backend==='direct'?candidates:0,falseCandidates:backend==='direct'?falseCandidates:0,baseLimit,heuristicLimit:backend==='direct'?heurLimit:baseLimit,diagnosticIfForcedDirect:backend==='direct'?null:{workReduction:1-heurWork/Math.max(1,baseWork),candidates,falseCandidates},ms:performance.now()-t0})}return{w,h,maxIter,oracleIter,rows,note:'Production DIRECT_F32 rule with two attracting-cycle confirmations. FAST/perturbation periodic early-stop is intentionally disabled.'}}
|
|
function fromDec(s,bits=256){s=String(s).trim();let neg=s.startsWith('-');if(neg)s=s.slice(1);if(s.startsWith('+'))s=s.slice(1);const [mant,es='0']=s.toLowerCase().split('e'),exp=parseInt(es,10)||0,[i='0',fr='']=mant.split('.');let digits=(i+fr).replace(/^0+(?=\d)/,'')||'0',places=fr.length-exp;if(places<0){digits+='0'.repeat(-places);places=0}const den=10n**BigInt(places),v=(BigInt(digits)*(1n<<BigInt(bits))+den/2n)/den;return neg?-v:v}
|
|
function workerBuildFast(span){let result;const c={performance,postMessage:m=>{result=m},BigInt,Float32Array,Float64Array,ArrayBuffer,DataView,Math,Number,String,Map,Set,Uint32Array};c.self=c;vm.runInNewContext(internal.fastReferenceWorkerSource(),c);c.self.onmessage({data:{type:'build',id:1,key:'bench',re:fromDec('-0.743643887037151').toString(),im:fromDec('0.13182590420533').toString(),span:fromDec(span).toString(),sourceBits:256,targetBits:320,width:800,height:600,iter:1200}});if(result?.type==='error')throw new Error(result.error);return result}
|
|
function cMul(a,b){return[a[0]*b[0]-a[1]*b[1],a[0]*b[1]+a[1]*b[0]]}
|
|
function refsAt(buf,n){const d=new DataView(buf),o=n*16;return[d.getFloat32(o,true)+d.getFloat32(o+8,true),d.getFloat32(o+4,true)+d.getFloat32(o+12,true)]}
|
|
function evalSeries(coeffs,dc){let sum=[0,0],pow=[1,0];for(let k=0;k<4;k++){pow=cMul(pow,dc);const a=[coeffs[k*2],coeffs[k*2+1]],t=cMul(a,pow);sum=[sum[0]+t[0],sum[1]+t[1]]}return sum}
|
|
function directDelta(refs,jump,dc){let d=[0,0];for(let n=0;n<jump;n++){const z=refsAt(refs,n),lin=cMul([2*z[0],2*z[1]],d),sq=cMul(d,d);d=[lin[0]+sq[0]+dc[0],lin[1]+sq[1]+dc[1]]}return d}
|
|
function seriesBench(){const spans=['1e-5','1e-7','1e-9','1e-11'],rows=[];for(const spanText of spans){const r=workerBuildFast(spanText),span=Number(spanText),samples=[[-.5,-.375],[-.5,.375],[.5,-.375],[.5,.375],[0,-.375],[0,.375],[-.5,0],[.5,0]];let maxAbsError=0,maxRelError=0;for(const [x,y] of samples){const dc=[x*span,y*span],p=evalSeries(r.series.coeffs,dc),e=directDelta(r.refs,r.series.jump,dc),ae=Math.hypot(p[0]-e[0],p[1]-e[1]),re=ae/Math.max(1e-30,Math.hypot(e[0],e[1]));maxAbsError=Math.max(maxAbsError,ae);maxRelError=Math.max(maxRelError,re)}rows.push({span:Number(spanText),jump:r.series.jump,errorLog2:r.series.errorLog2,maxAbsError,maxRelativeError:maxRelError,refLen:r.refLen,buildMs:r.buildMs})}return{rows,note:'Production fast-reference worker output. Error compares packed 4th-order series against direct perturbation over 8 screen-domain deltas.'}}
|
|
function sparseMemoryBench(){const presets={fast:524288,standard:1048576,high:4194304},rows=[];for(const [preset,n] of Object.entries(presets))for(const activeRatio of [1,.25,.1,.01]){const oldBytes=44*n,capacity=Math.min(n,Math.max(Math.ceil(n*activeRatio),Math.ceil(n*activeRatio*1.25)+256)),newBytes=44*capacity+4*Math.ceil(n/32),reduction=1-newBytes/oldBytes;rows.push({preset,pixels:n,activeRatio,capacity,oldBytes,newBytes,reduction})}return{rows,note:'Production allocation: 32 B state + 2x4 B queues + 4 B pixel map per active slot, plus 1-bit membership bitmap. Capacity adds 25% + 256 headroom.'}}
|
|
// Same perturbation locality surrogate as the first evaluation; production now requests up to 3 failure-guided references instead of one.
|
|
function analytic(cr,ci){const y2=ci*ci,x=cr-.25,q=x*x+y2;return q*(q+x)<.25*y2||(cr+1)*(cr+1)+y2<.0625}
|
|
function iterate(cr,ci,maxIter){if(analytic(cr,ci))return{cls:'analytic',iter:0};let zr=0,zi=0;for(let n=1;n<=maxIter;n++){const zr2=zr*zr,zi2=zi*zi;zi=2*zr*zi+ci;zr=zr2-zi2+cr;if(zr*zr+zi*zi>4)return{cls:'escaped',iter:n}}return{cls:'limit',iter:maxIter}}
|
|
function coord(view,x,y,w,h){const scale=view.span/w;return[view.re+(x+.5-w/2)*scale,view.im+(h/2-y-.5)*scale]}
|
|
function mMul(ar,ai,br,bi){return[ar*br-ai*bi,ar*bi+ai*br]}
|
|
function mFMul(ar,ai,br,bi){return[f(f(ar*br)-f(ai*bi)),f(f(ar*bi)+f(ai*br))]}
|
|
function buildReference(cr,ci,maxIter){const re=new Float64Array(maxIter+1),im=new Float64Array(maxIter+1),hiRe=new Float32Array(maxIter+1),hiIm=new Float32Array(maxIter+1),loRe=new Float32Array(maxIter+1),loIm=new Float32Array(maxIter+1);let zr=0,zi=0,valid=maxIter;for(let n=0;n<=maxIter;n++){re[n]=zr;im[n]=zi;hiRe[n]=f(zr);hiIm[n]=f(zi);loRe[n]=f(zr-hiRe[n]);loIm[n]=f(zi-hiIm[n]);if(n===maxIter)break;const zr2=zr*zr,zi2=zi*zi;zi=2*zr*zi+ci;zr=zr2-zi2+cr;if(!Number.isFinite(zr+zi)||Math.hypot(zr,zi)>1e18){valid=n;break}}return{cr,ci,re,im,hiRe,hiIm,loRe,loIm,valid}}
|
|
function mPerturbEscape(cRe,cIm,ref,maxIter){if(ref.valid<maxIter)return{cls:'reference-end',iter:ref.valid};let wr=f(0),wi=f(0),dr=f(cRe-ref.cr),di=f(cIm-ref.ci);for(let n=0;n<maxIter;n++){const zr=ref.hiRe[n]+ref.loRe[n]+wr,zi=ref.hiIm[n]+ref.loIm[n]+wi,mag=zr*zr+zi*zi;if(mag>4)return{cls:'escaped',iter:n};const [lr,li]=mFMul(ref.hiRe[n],ref.hiIm[n],wr,wi),[lr2,li2]=mFMul(ref.loRe[n],ref.loIm[n],wr,wi),[sr,si]=mFMul(wr,wi,wr,wi);wr=f(f(2*f(lr+lr2))+sr+dr);wi=f(f(2*f(li+li2))+si+di)}const n=maxIter,zr=ref.hiRe[n]+ref.loRe[n]+wr,zi=ref.hiIm[n]+ref.loIm[n]+wi;return zr*zr+zi*zi>4?{cls:'escaped',iter:n}:{cls:'limit',iter:maxIter}}
|
|
function perturbGrid(view,w,h,maxIter,grid){const refs=[],seen=new Set(),add=(x,y)=>{const key=`${x.toFixed(6)}:${y.toFixed(6)}`;if(seen.has(key))return;seen.add(key);const [cr,ci]=coord(view,x,y,w,h);refs.push({...buildReference(cr,ci,maxIter),x,y})};add((w-1)/2,(h-1)/2);if(grid>1)for(let gy=0;gy<grid;gy++)for(let gx=0;gx<grid;gx++)add((gx+.5)*w/grid-.5,(gy+.5)*h/grid-.5);let mismatchClass=0,mismatchIter=0,refEnd=0,total=0;for(let y=0;y<h;y++)for(let x=0;x<w;x++){const [cr,ci]=coord(view,x,y,w,h),truth=iterate(cr,ci,maxIter);if(truth.cls==='analytic')continue;let ref=null,best=Infinity;for(const r of refs){if(r.valid<maxIter)continue;const d=(x-r.x)**2+(y-r.y)**2;if(d<best){best=d;ref=r}}if(!ref){refEnd++;continue}const p=mPerturbEscape(cr,ci,ref,maxIter);total++;const tc=truth.cls==='escaped'?'escaped':'limit';if(p.cls!==tc)mismatchClass++;else if(tc==='escaped'&&Math.abs(p.iter-truth.iter)>1)mismatchIter++}return{grid,references:refs.length,total,refEnd,mismatchClass,mismatchIter,mismatchRate:mismatchClass/Math.max(1,total)}}
|
|
function multiReferenceBench(){const w=64,h=40,maxIter=1200,views=[{name:'seahorse-1e-5',re:-.743643887037151,im:.13182590420533,span:1e-5},{name:'seahorse-1e-7',re:-.743643887037151,im:.13182590420533,span:1e-7}],rows=[];for(const v of views)for(const grid of [1,2,3])rows.push({view:v.name,...perturbGrid(v,w,h,maxIter,grid)});return{maxGuidedPasses:(html.match(/FAILURE_GUIDED_REF_MAX_PASSES=(\d+)/)||[])[1]|0,w,h,maxIter,rows,note:'CPU f32 perturbation locality surrogate. Production selects up to three additional references from failure clusters instead of fixed grids.'}}
|
|
function makeNodeWorkerSource(){return `const {parentPort}=require('node:worker_threads');globalThis.self=globalThis;globalThis.postMessage=(m,t)=>parentPort.postMessage(m,t);${internal.cpuFallbackWorkerSource()}\nparentPort.on('message',d=>self.onmessage({data:d}));`}
|
|
async function cpuFallbackBench(){const source=makeNodeWorkerSource(),w=640,h=360,iter=350,threads=Math.min(4,Math.max(2,os.cpus().length));const payload=(y0,y1,id=1)=>({type:'render',id,w,h,y0,y1,maxIter:iter,re:-.5,im:0,span:3.4,style:{palette:0,cycle:1.0,shift:0}});async function run(count){const workers=Array.from({length:count},()=>new Worker(source,{eval:true}));const once=()=>Promise.all(workers.map((wk,i)=>new Promise((resolve,reject)=>{wk.once('message',resolve);wk.once('error',reject);wk.postMessage(payload(Math.floor(h*i/count),Math.floor(h*(i+1)/count),i+1))})));await once();const times=[];let parts;for(let k=0;k<7;k++){const t0=performance.now();parts=await once();times.push(performance.now()-t0)}await Promise.all(workers.map(w=>w.terminate()));return{medianMs:median(times),times,work:parts.reduce((s,p)=>s+p.work,0),escaped:parts.reduce((s,p)=>s+p.escaped,0),operationLimit:parts.reduce((s,p)=>s+p.operationLimit,0),heuristicInterior:parts.reduce((s,p)=>s+p.heuristicInterior,0)}}const one=await run(1),multi=await run(threads);return{w,h,iter,threads,one,multi,speedup:one.medianMs/multi.medianMs,workMatch:one.work===multi.work,escapedMatch:one.escaped===multi.escaped,operationLimitMatch:one.operationLimit===multi.operationLimit,heuristicMatch:one.heuristicInterior===multi.heuristicInterior,note:'Production CPU fallback Worker source executed under node:worker_threads shim.'}}
|
|
const results={date:new Date().toISOString(),periodicity:periodicityBench(),series:seriesBench(),sparseActiveState:sparseMemoryBench(),multiReference:multiReferenceBench(),cpuFallback:await cpuFallbackBench(),sourceAudit:{heuristicClass:/FIELD_INTERIOR_HEURISTIC:u32=2u/.test(html),strictDisablesPeriodicity:/candPeriod==0u && n>=64u/.test(html)&&!/cycleReady/.test(kernels.FAST_PERTURB_WGSL),seriesBinding:/binding\(5\).*series:SeriesData/s.test(html),compactPixelMap:/deepActivePixel/.test(html)&&/pixelMap/.test(kernels.DEEP_ACTIVE_CONTINUE_WGSL),multiReferencePasses:/FAILURE_GUIDED_REF_MAX_PASSES=3/.test(html),cpuFallback:/class CpuFallbackRenderer/.test(html)},environment:{node:process.version,cpus:os.cpus().length,platform:process.platform,arch:process.arch}};
|
|
console.log(JSON.stringify(results,null,2));
|
|
if(process.argv[2])await fs.writeFile(process.argv[2],JSON.stringify(results,null,2)+'\n');
|