publish
This commit is contained in:
parent
01204dfa2f
commit
f5fc9b76ce
73 changed files with 32628 additions and 376 deletions
57
experiments/ab/ab_test.mjs
Normal file
57
experiments/ab/ab_test.mjs
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
import fs from 'node:fs/promises';
|
||||
import vm from 'node:vm';
|
||||
import {performance} from 'node:perf_hooks';
|
||||
import {createHash} from 'node:crypto';
|
||||
|
||||
const root=new URL('../../',import.meta.url);
|
||||
const html=await fs.readFile(new URL('index.html',root),'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:1024,height:768,clientWidth:1024,clientHeight:768,style:{},hidden:false,value:'',textContent:'',disabled:false,addEventListener(){},classList:{toggle(){}},setAttribute(){},removeAttribute(){},showModal(){},close(){},toBlob(){},getBoundingClientRect(){return{left:0,top:0,width:this.clientWidth,height:this.clientHeight}}};
|
||||
const app={MANDEL_WEBGPU_KERNELS:kernels,document:{querySelector:()=>element,getElementById:()=>element,documentElement:{classList:{toggle(){}}},body:{classList:{toggle(){}},appendChild(){}}},navigator:{hardwareConcurrency:8,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:1024,innerHeight:768,devicePixelRatio:1,localStorage:{getItem(){return null},setItem(){},removeItem(){}}};
|
||||
const cut=scripts[1].indexOf('// ── boot / teardown');
|
||||
vm.runInNewContext(scripts[1].slice(0,cut)+`\nglobalThis.abInternal={state,canvas,snapshot,maxIter,pixelPrecisionBits,adaptiveProbeSources,adaptiveProbeChoice,referenceWorkerSource,certifiedInteriorTiles,integerGridReuseShift,WebGpuRenderer,fixedNum};})();`,app);
|
||||
const I=app.abInternal;
|
||||
const corpus=JSON.parse(await fs.readFile(new URL('experiments/benchmark-corpus.json',root),'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 mean=a=>a.reduce((s,x)=>s+x,0)/Math.max(1,a.length);
|
||||
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 snapOf(v,bits=256){return{bits,re:fromDec(v.re,bits),im:fromDec(v.im,bits),span:fromDec(v.span,bits)}}
|
||||
function setView(v,w=1024,h=768){I.canvas.width=w;I.canvas.height=h;I.canvas.clientWidth=w;I.canvas.clientHeight=h;const s=snapOf(v);Object.assign(I.state,s);I.state.adaptive=true;I.state.baseIter=350;I.state.continuationBudget=16384;return s}
|
||||
|
||||
// A/B 1: strict direct path: current A maintains Brent cycle state even though strict disables classification.
|
||||
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));return[f(f(view.re)+f(f(f(x+.5)-f(.5*w))*scale)),f(f(view.im)+f(f(f(.5*h)-f(y+.5))*scale))]}
|
||||
function strictA(cr,ci,maxIter){if(analyticF32(cr,ci))return 0;let zr=f(0),zi=f(0),n=0,cycleR=f(0),cycleI=f(0),cyclePower=1,cycleLam=0;while(n<maxIter){const zr2=f(zr*zr),zi2=f(zi*zi);zi=f(f(2*zr*zi)+ci);zr=f(f(zr2-zi2)+cr);n++;if(f(f(zr*zr)+f(zi*zi))>4)return n;cycleLam++;if(cycleLam>=cyclePower){cycleR=zr;cycleI=zi;cyclePower=Math.min(65536,cyclePower*2);cycleLam=0}}return maxIter}
|
||||
function strictB(cr,ci,maxIter){if(analyticF32(cr,ci))return 0;let zr=f(0),zi=f(0),n=0;while(n<maxIter){const zr2=f(zr*zr),zi2=f(zi*zi);zi=f(f(2*zr*zi)+ci);zr=f(f(zr2-zi2)+cr);n++;if(f(f(zr*zr)+f(zi*zi))>4)return n}return maxIter}
|
||||
function benchStrict(){const w=64,h=40,maxIter=4096,rows=[];for(const v of corpus){const pts=[];for(let y=0;y<h;y++)for(let x=0;x<w;x++)pts.push(coordF32(v,x,y,w,h));const run=fn=>{let sum=0;const t0=performance.now();for(const p of pts)sum+=fn(p[0],p[1],maxIter);return{ms:performance.now()-t0,sum}};run(strictA);run(strictB);const A=[],B=[];for(let k=0;k<5;k++){A.push(run(strictA));B.push(run(strictB))}if(A.at(-1).sum!==B.at(-1).sum)throw new Error('strict A/B result mismatch '+v.name);const a=median(A.map(x=>x.ms)),b=median(B.map(x=>x.ms));rows.push({view:v.name,aMs:a,bMs:b,speedup:a/b,changePct:(b/a-1)*100,iterationChecksum:A.at(-1).sum})}return rows}
|
||||
|
||||
// A/B 2: exact interior proof with a conservative cheap viewport bounding-box gate.
|
||||
// Main cardioid is inside x[-3/4,1/4], |y|<3sqrt(3)/8<2/3. Period-2 bulb is inside x[-5/4,-3/4], |y|<=1/4.
|
||||
function maybeOverlapsKnownInterior(v,w,h){const re=Number(v.re),im=Number(v.im),span=Number(v.span),halfX=span/2,halfY=span*h/(2*w),xl=re-halfX,xh=re+halfX,yl=im-halfY,yh=im+halfY;const hit=(ax,bx,ay,by)=>!(xh<ax||xl>bx||yh<ay||yl>by);return hit(-.75,.375,-2/3,2/3)||hit(-1.25,-.75,-.25,.25)}
|
||||
function benchInteriorGate(){const sizes=[[1024,768],[2048,2048]],rows=[];for(const [w,h] of sizes){for(const v of corpus){const snap=setView(v,w,h);const runsA=[],runsB=[];let aRes,bRes;for(let k=0;k<7;k++){let t=performance.now();aRes=I.certifiedInteriorTiles(snap,w,h,32,null);runsA.push(performance.now()-t);t=performance.now();bRes=maybeOverlapsKnownInterior(v,w,h)?I.certifiedInteriorTiles(snap,w,h,32,null):{pixels:0};runsB.push(performance.now()-t)}if(aRes.pixels!==bRes.pixels)throw new Error('interior gate false negative '+v.name);rows.push({view:v.name,w,h,gate:maybeOverlapsKnownInterior(v,w,h),certified:aRes.pixels,aMs:median(runsA),bMs:median(runsB),savedMs:median(runsA)-median(runsB)})}}return rows}
|
||||
|
||||
// A/B 3: adaptive probe vs always base iteration. Fixed corpus + deterministic exploratory survey.
|
||||
function scorerVM(){let result=null;const c={performance,postMessage:m=>{result=m},BigInt,Float32Array,Float64Array,ArrayBuffer,DataView,Math,Number,String,Map,Set,Uint32Array};c.self=c;vm.runInNewContext(I.referenceWorkerSource(),c);return{score(snap,iter,w,h,candidates){result=null;const targetBits=Math.max(I.pixelPrecisionBits(snap,w,40)+32,...candidates.map(x=>x.precisionBits||0));c.self.onmessage({data:{type:'score',id:1,bits:snap.bits,targetBits,re:snap.re.toString(),im:snap.im.toString(),span:snap.span.toString(),width:w,height:h,iter,candidates:candidates.map(x=>({re:x.re.toString(),im:x.im.toString()}))}});if(result?.type==='error')throw new Error(result.error);return result}}}
|
||||
function probeOne(v,sc,w=1024,h=768){const snap=setView(v,w,h),iter=I.maxIter(),sources=I.adaptiveProbeSources(snap,w,h),t0=performance.now(),res=sc.score(snap,iter,w,h,sources),wall=performance.now()-t0,choice=I.adaptiveProbeChoice(res.escapes,iter,I.state.baseIter),baseWork=choice.work.find(x=>x.initial===I.state.baseIter)?.total??null,chosenWork=choice.work.find(x=>x.initial===choice.iter)?.total??null;return{iter,chosen:choice.iter,probeMs:wall,baseWork,chosenWork,workPenaltyAlwaysBase:chosenWork?baseWork/chosenWork:1,escapes:res.escapes}}
|
||||
function exploratoryViews(){let seed=0x12345678;const rnd=()=>{seed=(Math.imul(seed,1664525)+1013904223)>>>0;return seed/4294967296};const anchors=[[-.75,0],[-.743643887, .131825904],[-.1011,.9563],[-1.25066,.02012],[-.16,1.0405],[-.1225611669,.7448617666],[-.5,0]];const spans=[2,1,.4,.1,.03,.01,.003,.001];const out=[];for(let i=0;i<48;i++){const a=anchors[i%anchors.length],sp=spans[(i*3)%spans.length],j=sp*.35;out.push({name:'survey-'+i,re:a[0]+(rnd()-.5)*j,im:a[1]+(rnd()-.5)*j,span:sp})}return out}
|
||||
function benchProbe(){const sc=scorerVM(),fixed=corpus.map(v=>({view:v.name,...probeOne(v,sc)})),survey=[];for(const v of exploratoryViews())survey.push({view:v.name,...v,...probeOne(v,sc,640,480)});return{fixed,survey,nonBase:survey.filter(x=>x.chosen!==350)}}
|
||||
|
||||
// A/B 4: FAST full-screen repair. Tiled post-stats pass covers all compute pixels; symmetry covers the other half.
|
||||
function coverageCase(w,h,iter,mirror){const r=Object.create(I.WebGpuRenderer.prototype),shape=r.numericTileShape(w,iter),tileW=shape.width,baseRows=r.initialNumericRows(w,iter),seen=new Uint8Array(w*h),computeH=mirror?Math.ceil(h/2):h;let rows=baseRows,y=0,step=0;while(y<computeH){const th=Math.min(rows,computeH-y);for(let x=0;x<w;x+=tileW){const tw=Math.min(tileW,w-x);for(let yy=0;yy<th;yy++)seen.fill(1,(y+yy)*w+x,(y+yy)*w+x+tw)}y+=th;step++;if(step%3===0)rows=Math.max(1,Math.min(computeH-y||1,Math.ceil(rows*.73)));else if(step%4===0)rows=Math.max(1,Math.min(computeH-y||1,rows+1))}if(mirror){for(let yy=0;yy<Math.floor(h/2);yy++){const my=h-1-yy;for(let x=0;x<w;x++)if(seen[yy*w+x])seen[my*w+x]=1}}let holes=0;for(const v of seen)if(!v)holes++;return{w,h,iter,mirror,tileW,initialRows:baseRows,holes,repairThreads:Math.ceil(w/8)*Math.ceil(h/8)*64,minimumMetaReadBytes:w*h*4}}
|
||||
function benchCoverage(){const cases=[];for(const [w,h] of [[965,543],[1365,768],[1024,768],[2731,1536],[2048,2048],[513,511]])for(const iter of [350,4096,16384])for(const mirror of [false,true])cases.push(coverageCase(w,h,iter,mirror));if(cases.some(x=>x.holes))throw new Error('coverage hole found');return cases}
|
||||
|
||||
// A/B 5: numeric history exact-grid reuse hit survey and conditional-residency policy.
|
||||
function historyPanSurvey(){const configs=[{name:'1920x1080-fast',cssW:1920,cssH:1080,w:965,h:543},{name:'1920x1080-standard',cssW:1920,cssH:1080,w:1365,h:768},{name:'1920x1080-high',cssW:1920,cssH:1080,w:2731,h:1536},{name:'1024x768-standard',cssW:1024,cssH:768,w:1024,h:768},{name:'1024x768-dpr2',cssW:1024,cssH:768,w:2048,h:1536}],rows=[];const base=snapOf(corpus[0]);for(const c of configs){let hits=0,total=0,reused=0;const old={...base,w:c.w,h:c.h};for(let dx=-500;dx<=500;dx++){if(dx===0)continue;const re=old.re-old.span*BigInt(Math.round(dx*1e6))/BigInt(Math.round(c.cssW*1e6)),next={...old,re};const sh=I.integerGridReuseShift(old,next,c.w,c.h);total++;if(sh){hits++;reused+=sh.pixels}}const g=(a,b)=>b?g(b,a%b):Math.abs(a),den=c.cssW/g(c.w,c.cssW),policyEligible=den<=8;rows.push({...c,hits,total,hitRate:hits/total,meanReuseRatio:hits?reused/hits/(c.w*c.h):0,reducedScaleDenominator:den,policyEligible,aResidentBytes:8*c.w*c.h,bResidentBytes:policyEligible?8*c.w*c.h:0})}return rows}
|
||||
|
||||
// A/B 6: continuation replay surrogate (direct arithmetic): A restarts survivors from zero; B resumes saved z.
|
||||
function analytic64(cr,ci){const y2=ci*ci,x=cr-.25,q=x*x+y2;if(q*(q+x)<.25*y2)return true;const x2=cr+1;return x2*x2+y2<.0625}
|
||||
function continueFrom(cr,ci,maxIter,zr=0,zi=0,n=0){let work=0;for(;n<maxIter;n++){const zr2=zr*zr,zi2=zi*zi;zi=2*zr*zi+ci;zr=zr2-zi2+cr;work++;if(zr*zr+zi*zi>4)return{escaped:true,n:n+1,zr,zi,work}}return{escaped:false,n,zr,zi,work}}
|
||||
function continuationReplay(){const w=64,h=40,base=350,target=4096,rows=[];for(const v of corpus){let workA=0,workB=0,survivors=0,mismatch=0;for(let y=0;y<h;y++)for(let x=0;x<w;x++){const scale=v.span/w,cr=v.re+(x+.5-w/2)*scale,ci=v.im+(h/2-y-.5)*scale;if(analytic64(cr,ci))continue;const p=continueFrom(cr,ci,base);workA+=p.work;workB+=p.work;if(!p.escaped){survivors++;const a=continueFrom(cr,ci,target);const b=continueFrom(cr,ci,target,p.zr,p.zi,p.n);workA+=a.work;workB+=b.work;if(a.escaped!==b.escaped||a.n!==b.n)mismatch++}}rows.push({view:v.name,survivors,workA,workB,workReduction:1-workB/Math.max(1,workA),mismatch})}return rows}
|
||||
|
||||
function memoryTraffic(){const presets={fast:524288,standard:1048576,high:4194304};const rows=[];for(const [preset,n] of Object.entries(presets)){rows.push({preset,pixels:n,recolorNumericCopyA:8*n,recolorNumericCopyB:0,cpuRgbaRetainedA:4*n,cpuRgbaRetainedB:0,idleColorSnapshotA:12*n,idleColorSnapshotB:0,unknownPrepassMetaReadA:4*n,unknownPrepassMetaReadB:0,fastRepairMetaReadA:4*n,fastRepairMetaReadB:0})}const export512PerSlot=(512*512)*(4+4+4*4+4+4),activeStdFull=1048576*(32+4+4+4)+Math.ceil(1048576/32)*4+24,activeHighFull=4194304*(32+4+4+4)+Math.ceil(4194304/32)*4+24;return{rows,export:{retainedA:3*export512PerSlot,retainedB:0,reallocationSlotsB:3},deep:{standardFullA:activeStdFull,highFullA:activeHighFull,releaseAfterEasyB:0}}}
|
||||
|
||||
const results={date:new Date().toISOString(),source:{sha256:createHash('sha256').update(html).digest('hex'),webGpuTiming:'unavailable in container; Chromium exposes no usable WebGPU adapter'},strictCycle:benchStrict(),interiorGate:benchInteriorGate(),adaptiveProbe:benchProbe(),fastCoverage:benchCoverage(),numericHistory:historyPanSurvey(),continuationReplay:continuationReplay(),memoryTraffic:memoryTraffic()};
|
||||
console.log(JSON.stringify(results,null,2));
|
||||
if(process.argv[2])await fs.writeFile(process.argv[2],JSON.stringify(results,null,2)+'\n');
|
||||
8
experiments/ab/supplement.mjs
Normal file
8
experiments/ab/supplement.mjs
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
import fs from 'node:fs/promises';import vm from 'node:vm';import {performance} from 'node:perf_hooks';
|
||||
const html=await fs.readFile(new URL('../../index.html',import.meta.url),'utf8'),scripts=[...html.matchAll(/<script[^>]*>([\s\S]*?)<\/script>/g)].map(m=>m[1]),kc={};vm.runInNewContext(scripts[0],kc);const element={width:256,height:192,clientWidth:256,clientHeight:192,style:{},hidden:false,value:'',textContent:'',disabled:false,addEventListener(){},classList:{toggle(){}},setAttribute(){},removeAttribute(){},showModal(){},close(){},getBoundingClientRect(){return{left:0,top:0,width:256,height:192}}};const app={MANDEL_WEBGPU_KERNELS:kc.MANDEL_WEBGPU_KERNELS,document:{querySelector:()=>element,getElementById:()=>element,documentElement:{classList:{toggle(){}}},body:{classList:{toggle(){}},appendChild(){}}},navigator:{hardwareConcurrency:8,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:256,innerHeight:192,devicePixelRatio:1,localStorage:{getItem(){return null},setItem(){},removeItem(){}}};const cut=scripts[1].indexOf('// ── boot / teardown');vm.runInNewContext(scripts[1].slice(0,cut)+`\nglobalThis.x={certifiedInteriorTiles};})();`,app);const C=app.x;
|
||||
function fromDec(s,bits=256){s=String(s);let neg=s[0]=='-';if(neg)s=s.slice(1);const [m,e0='0']=s.toLowerCase().split('e'),e=+e0||0,[i='0',f='']=m.split('.');let d=(i+f).replace(/^0+(?=\d)/,'')||'0',p=f.length-e;if(p<0){d+='0'.repeat(-p);p=0}let den=10n**BigInt(p),v=(BigInt(d)*(1n<<BigInt(bits))+den/2n)/den;return neg?-v:v}
|
||||
function gate(v,w,h){const re=v.re,im=v.im,span=v.span,unit=1n<<BigInt(v.bits),bw=BigInt(w),bh=BigInt(h),xLo=2n*re-span,xHi=2n*re+span,xDen=2n*unit,yLo=2n*bw*im-span*bh,yHi=2n*bw*im+span*bh,yDen=2n*bw*unit,axis=(lo,hi,den,lp,lq,hp,hq)=>hi*BigInt(lq)>=BigInt(lp)*den&&lo*BigInt(hq)<=BigInt(hp)*den;return axis(xLo,xHi,xDen,-3,4,3,8)&&axis(yLo,yHi,yDen,-2,3,2,3)||axis(xLo,xHi,xDen,-5,4,-3,4)&&axis(yLo,yHi,yDen,-1,4,1,4)}
|
||||
let seed=0x9e3779b9;const rnd=()=>{seed=(Math.imul(seed,1664525)+1013904223)>>>0;return seed/2**32};let gateFalse=0,falseNeg=0,totalProof=0;for(let i=0;i<240;i++){const span=10**(-4+rnd()*4.7),v={bits:256,re:fromDec(-2+rnd()*3),im:fromDec(-1.2+rnd()*2.4),span:fromDec(span)},g=gate(v,256,192),p=C.certifiedInteriorTiles(v,256,192,32,null).pixels;if(!g)gateFalse++;if(!g&&p){falseNeg++;totalProof+=p}}
|
||||
// FAST semantic coverage with random seeded/proven/unknown metadata. 0 means untouched hole; any invocation writes nonzero unless a nonzero seed/proof is intentionally retained.
|
||||
let fastFalse=0,fastCases=0;for(let c=0;c<200;c++){const w=1+Math.floor(rnd()*700),h=1+Math.floor(rnd()*500),mirror=rnd()<.5,computeH=mirror?Math.ceil(h/2):h,meta=new Uint32Array(w*h);for(let i=0;i<meta.length;i++){const q=rnd();meta[i]=q<.1?(3<<28):q<.2?(1<<28)|5:q<.3?123:0}const tileW=1+Math.floor(rnd()*Math.min(w,200)),rows=1+Math.floor(rnd()*Math.min(computeH,80));for(let y=0;y<computeH;y+=rows)for(let x=0;x<w;x+=tileW){const tw=Math.min(tileW,w-x),th=Math.min(rows,computeH-y);for(let yy=0;yy<th;yy++)for(let xx=0;xx<tw;xx++){const i=(y+yy)*w+x+xx,cls=meta[i]>>>28;if(cls===3)continue;if(cls!==0)continue;meta[i]=(1<<28)|1}}if(mirror)for(let y=0;y<Math.floor(h/2);y++)for(let x=0;x<w;x++)meta[(h-1-y)*w+x]=meta[y*w+x]||1;let holes=0;for(const m of meta)if(m===0)holes++;if(holes)fastFalse++;fastCases++}
|
||||
console.log(JSON.stringify({interiorGateRandom:{cases:240,gateFalse,falseNeg,totalProofInFalseNeg:totalProof},fastCoverageSemantic:{cases:fastCases,casesWithHoles:fastFalse}},null,2));
|
||||
13
experiments/ab/variant_check.mjs
Normal file
13
experiments/ab/variant_check.mjs
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
import fs from 'node:fs/promises';
|
||||
import vm from 'node:vm';
|
||||
for (const file of process.argv.slice(2)) {
|
||||
const html=await fs.readFile(file,'utf8');
|
||||
const scripts=[...html.matchAll(/<script[^>]*>([\s\S]*?)<\/script>/g)].map(m=>m[1]);
|
||||
if(scripts.length!==2) throw new Error(file+': expected 2 scripts, got '+scripts.length);
|
||||
for(const s of scripts)new vm.Script(s);
|
||||
const kc={}; vm.runInNewContext(scripts[0],kc);
|
||||
const kernels=kc.MANDEL_WEBGPU_KERNELS;
|
||||
if(!kernels||Object.keys(kernels).length!==20)throw new Error(file+': kernel bundle malformed');
|
||||
for(const k of ['DIRECT_F32_WGSL','FAST_PERTURB_WGSL','UNKNOWN_STATS_WGSL','COLOR_WGSL'])if(typeof kernels[k]!=='string')throw new Error(file+': missing '+k);
|
||||
console.log('PASS',file.split('/').at(-1),'JS syntax, 19 WGSL kernels + version');
|
||||
}
|
||||
1956
experiments/ab/variants/b-confirmed-combined.html
Normal file
1956
experiments/ab/variants/b-confirmed-combined.html
Normal file
File diff suppressed because it is too large
Load diff
1957
experiments/ab/variants/b01-recolor-no-numeric-copy.html
Normal file
1957
experiments/ab/variants/b01-recolor-no-numeric-copy.html
Normal file
File diff suppressed because it is too large
Load diff
1957
experiments/ab/variants/b02-cpu-no-rgba-retention.html
Normal file
1957
experiments/ab/variants/b02-cpu-no-rgba-retention.html
Normal file
File diff suppressed because it is too large
Load diff
1956
experiments/ab/variants/b03-certify-no-preprimary-stats.html
Normal file
1956
experiments/ab/variants/b03-certify-no-preprimary-stats.html
Normal file
File diff suppressed because it is too large
Load diff
1956
experiments/ab/variants/b04-strict-no-cycle-state.html
Normal file
1956
experiments/ab/variants/b04-strict-no-cycle-state.html
Normal file
File diff suppressed because it is too large
Load diff
1957
experiments/ab/variants/b05-fast-no-coverage-repair.html
Normal file
1957
experiments/ab/variants/b05-fast-no-coverage-repair.html
Normal file
File diff suppressed because it is too large
Load diff
1957
experiments/ab/variants/b06-idle-colorauto-no-snapshot.html
Normal file
1957
experiments/ab/variants/b06-idle-colorauto-no-snapshot.html
Normal file
File diff suppressed because it is too large
Load diff
1957
experiments/ab/variants/b07-export-release-workspace.html
Normal file
1957
experiments/ab/variants/b07-export-release-workspace.html
Normal file
File diff suppressed because it is too large
Load diff
1968
experiments/ab/variants/b08-exact-interior-spatial-gate.html
Normal file
1968
experiments/ab/variants/b08-exact-interior-spatial-gate.html
Normal file
File diff suppressed because it is too large
Load diff
1964
experiments/ab/variants/b09-conditional-numeric-history.html
Normal file
1964
experiments/ab/variants/b09-conditional-numeric-history.html
Normal file
File diff suppressed because it is too large
Load diff
1958
experiments/ab/variants/b10-export-aa-samples-lazy.html
Normal file
1958
experiments/ab/variants/b10-export-aa-samples-lazy.html
Normal file
File diff suppressed because it is too large
Load diff
16
experiments/benchmark-corpus.json
Normal file
16
experiments/benchmark-corpus.json
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
{
|
||||
"purpose": "Fixed surrogate corpus for evaluating proposed Mandelbrot viewer improvements without changing production index.html.",
|
||||
"views": [
|
||||
{"name":"overview","re":-0.5,"im":0,"span":3.4},
|
||||
{"name":"period3-bulb","re":-0.1225611668766536,"im":0.7448617666197442,"span":0.10},
|
||||
{"name":"period3-core","re":-0.1225611668766536,"im":0.7448617666197442,"span":0.025},
|
||||
{"name":"seahorse-boundary","re":-0.743643887037151,"im":0.13182590420533,"span":0.004},
|
||||
{"name":"seahorse-deep","re":-0.743643887037151,"im":0.13182590420533,"span":0.000002},
|
||||
{"name":"exterior","re":0.7,"im":0.2,"span":1.2}
|
||||
],
|
||||
"notes": [
|
||||
"CPU tests are surrogates for algorithmic work and classification behavior, not WebGPU frame-time benchmarks.",
|
||||
"Deep-zoom tests that depend on arbitrary precision are represented by perturbation/series locality tests rather than full production-equivalent rendering.",
|
||||
"Do not change these views when comparing future revisions; add new named views instead."
|
||||
]
|
||||
}
|
||||
40
experiments/cost_audit.mjs
Normal file
40
experiments/cost_audit.mjs
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
import fs from 'node:fs/promises';
|
||||
import vm from 'node:vm';
|
||||
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:1024,height:768,clientWidth:1024,clientHeight:768,style:{},hidden:false,value:'',textContent:'',disabled:false,addEventListener(){},classList:{toggle(){}},setAttribute(){},removeAttribute(){},showModal(){},close(){},toBlob(){},getBoundingClientRect(){return{left:0,top:0,width:this.clientWidth,height:this.clientHeight}}};
|
||||
const app={MANDEL_WEBGPU_KERNELS:kernels,document:{querySelector:()=>element,getElementById:()=>element,documentElement:{classList:{toggle(){}}},body:{classList:{toggle(){}},appendChild(){}}},navigator:{hardwareConcurrency:8,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:1024,innerHeight:768,devicePixelRatio:1,localStorage:{getItem(){return null},setItem(){},removeItem(){}}};
|
||||
const cut=scripts[1].indexOf('// ── boot / teardown');
|
||||
vm.runInNewContext(scripts[1].slice(0,cut)+`\nglobalThis.auditInternal={state,canvas,snapshot,maxIter,zoomExp,pixelPrecisionBits,adaptiveProbeSources,adaptiveProbeChoice,referenceWorkerSource,certifiedInteriorTiles,integerGridReuseShift,fmtFixed,updateStats};})();`,app);
|
||||
const I=app.auditInternal;
|
||||
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};
|
||||
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 snapOf(v,bits=256){return{bits,re:fromDec(v.re,bits),im:fromDec(v.im,bits),span:fromDec(v.span,bits)}}
|
||||
function setView(v,w=1024,h=768){I.canvas.width=w;I.canvas.height=h;I.canvas.clientWidth=w;I.canvas.clientHeight=h;const s=snapOf(v);Object.assign(I.state,s);return s}
|
||||
|
||||
function benchInteriorProof(){const sizes=[[1024,768],[2048,2048]],rows=[];for(const [w,h] of sizes){for(const v of corpus){const snap=setView(v,w,h);const times=[];let r;for(let k=0;k<7;k++){const t0=performance.now();r=I.certifiedInteriorTiles(snap,w,h,32,null);times.push(performance.now()-t0)}rows.push({view:v.name,w,h,pixels:w*h,certified:r.pixels,certifiedRatio:r.pixels/(w*h),medianMs:median(times),times})}}return rows}
|
||||
|
||||
function runScoreWorker(snap,iter,w,h,candidates){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(I.referenceWorkerSource(),c);const targetBits=Math.max(I.pixelPrecisionBits(snap,w,40)+32,...candidates.map(x=>x.precisionBits||0));c.self.onmessage({data:{type:'score',id:1,bits:snap.bits,targetBits,re:snap.re.toString(),im:snap.im.toString(),span:snap.span.toString(),width:w,height:h,iter,candidates:candidates.map(x=>({re:x.re.toString(),im:x.im.toString()}))}});if(result?.type==='error')throw new Error(result.error);return result}
|
||||
function benchAdaptiveProbe(){const w=1024,h=768,rows=[];for(const v of corpus){const snap=setView(v,w,h),adaptiveIter=I.maxIter(),candidates=I.adaptiveProbeSources(snap,w,h);const times=[];let res;for(let k=0;k<3;k++){const t0=performance.now();res=runScoreWorker(snap,adaptiveIter,w,h,candidates);times.push(performance.now()-t0)}const choice=I.adaptiveProbeChoice(res.escapes,adaptiveIter,I.state.baseIter);rows.push({view:v.name,adaptiveIter,chosenInitialIter:choice.iter,targetBits:I.pixelPrecisionBits(snap,w,40)+32,samples:candidates.length,scoreReportedMs:res.scoreMs,wallMedianMs:median(times),escapeSamples:res.escapes,workChoices:choice.work})}return rows}
|
||||
|
||||
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 cmul(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 directBase(cr,ci,maxIter){if(analyticF32(cr,ci))return 0;let zr=f(0),zi=f(0);for(let n=1;n<=maxIter;n++){const zr2=f(zr*zr),zi2=f(zi*zi);zi=f(f(2*zr*zi)+ci);zr=f(f(zr2-zi2)+cr);if(f(f(zr*zr)+f(zi*zi))>4)return n}return maxIter}
|
||||
function directPeriodic(cr,ci,maxIter){if(analyticF32(cr,ci))return 0;let zr=f(0),zi=f(0),n=0,cycleR=f(0),cycleI=f(0),cyclePower=1,cycleLam=0,candPeriod=0,candAge=0,candR=f(0),candI=f(0),candMul=[f(1),f(0)],candRepeats=0;while(n<maxIter){const prev=[zr,zi],zr2=f(zr*zr),zi2=f(zi*zi);zi=f(f(2*zr*zi)+ci);zr=f(f(zr2-zi2)+cr);n++;if(f(f(zr*zr)+f(zi*zi))>4)return n;if(candPeriod>0){candMul=cmul(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 n;candR=zr;candI=zi;candAge=0;candMul=[f(1),f(0)]}else{candPeriod=0;candRepeats=0}}}if(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=cycleLam+1;if(period<=2048){candPeriod=period;candAge=0;candR=zr;candI=zi;candMul=[f(1),f(0)];candRepeats=0}}}cycleLam++;if(cycleLam>=cyclePower){cycleR=zr;cycleI=zi;cyclePower=Math.min(65536,cyclePower*2);cycleLam=0}}return maxIter}
|
||||
function benchPeriodicityRuntime(){const w=64,h=40,maxIter=4096,rows=[];for(const v of corpus){const pts=[];for(let y=0;y<h;y++)for(let x=0;x<w;x++)pts.push(coordF32(v,x,y,w,h));const run=fn=>{let sum=0;const t0=performance.now();for(const [cr,ci] of pts)sum+=fn(cr,ci,maxIter);return{ms:performance.now()-t0,sum}};run(directBase);run(directPeriodic);const b=[],p=[];for(let k=0;k<4;k++){b.push(run(directBase));p.push(run(directPeriodic))}rows.push({view:v.name,baseMedianMs:median(b.map(x=>x.ms)),periodicMedianMs:median(p.map(x=>x.ms)),runtimeRatio:median(p.map(x=>x.ms))/median(b.map(x=>x.ms)),baseIterationSum:b.at(-1).sum,periodicIterationSum:p.at(-1).sum,iterationReduction:1-p.at(-1).sum/Math.max(1,b.at(-1).sum)})}return rows}
|
||||
|
||||
function memoryAudit(){const presets={fast:524288,standard:1048576,high:4194304},rows=[];for(const [preset,n] of Object.entries(presets)){rows.push({preset,pixels:n,baseFrameBytes:28*n,numericHistoryBytes:8*n,colorSourceBytes:12*n,deepCorrectionQueueBytes:4*n,cpuFallbackRgbaBytes:4*n,colorAutoNumericHistoryCopyBytesPerSecond:8*n*20,provisionalSnapshotCopyBytesPerSecondAt8Hz:8*n*8})}const export512PerSlot=(512*512)*(4+4+4*4+4+4); // meta,smooth,4 samples,resolve,read
|
||||
const export256PerSlot=(256*256)*(4+4+4*4+4+4);
|
||||
return{rows,export:{tile512PerSlotBytes:export512PerSlot,tile512Depth3Bytes:3*export512PerSlot,tile256PerSlotBytes:export256PerSlot,tile256Depth2Bytes:2*export256PerSlot,nonAaUnusedSampleTextureBytesPer512Slot:4*512*512*4}}}
|
||||
|
||||
function gridReusePanAudit(){const rows=[];for(const dpr of [1,1.25,1.5,2])for(const cssW of [800,1024,1365]){const w=Math.round(cssW*dpr),h=Math.round(w*.75),old={...snapOf(corpus[0]),w,h};for(const dx of [1,3,10,37,100]){const denom=BigInt(Math.round(cssW*1e6)),re=old.re-old.span*BigInt(Math.round(dx*1e6))/denom,newv={...old,re};const sh=I.integerGridReuseShift(old,newv,w,h);rows.push({dpr,cssW,w,dx,reused:!!sh,sx:sh?.sx??null,pixels:sh?.pixels??0,ratio:sh?sh.pixels/(w*h):0})}}return rows}
|
||||
|
||||
const results={date:new Date().toISOString(),sourceSha256:null,interiorProof:benchInteriorProof(),adaptiveProbe:benchAdaptiveProbe(),periodicityRuntimeSurrogate:benchPeriodicityRuntime(),memory:memoryAudit(),gridReusePan:gridReusePanAudit(),staticFindings:{certifyInteriorRunsUnusedUnknownStats:/encodeUnknownStats\(e,\{meta:f\.meta,unresolved:f\.unresolved,n:f\.n\}\);\s*return await this\.submitStage\(e,historySeed\?/.test(html),numericHistoryAlwaysAllocated:/historyMeta:buf\(d,n\*4/.test(html)&&/historySmooth:buf\(d,n\*4/.test(html),commitHistoryAlwaysCommitsNumeric:/this\.commitNumericHistory\(view,state\.fieldView\?\.iter/.test(html),colorAuto20Hz:/state\.rendering\?100:50/.test(html),colorSourceSnapshotCopiesNumeric:/copyBufferToBuffer\(f\.meta,0,source\.meta,0,f\.n\*4\)/.test(html)&&/copyBufferToBuffer\(f\.smooth,0,source\.smooth,0,f\.n\*4\)/.test(html),cpuFallbackRgbaUnused:(html.match(/cpuFrameRgba/g)||[]).length===2,exportWorkspacePersists:!/exportWorkspaceDestroy\(\)/.test(html.slice(html.indexOf('async function runExport'),html.indexOf("$('png').onclick"))),fastCoverageRepair:/fast-coverage-repair/.test(html)&&/this\.encodeFastNumeric\(e,\{pbuf:f\.numericParams/.test(html)}};
|
||||
console.log(JSON.stringify(results,null,2));
|
||||
if(process.argv[2])await fs.writeFile(process.argv[2],JSON.stringify(results,null,2)+'\n');
|
||||
46
experiments/integrated_bench.mjs
Normal file
46
experiments/integrated_bench.mjs
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
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');
|
||||
47
experiments/reported-view/analyze.mjs
Normal file
47
experiments/reported-view/analyze.mjs
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
import fs from 'node:fs/promises';
|
||||
import vm from 'node:vm';
|
||||
import {createHash} from 'node:crypto';
|
||||
|
||||
const beforeUrl=new URL('../../docs/reported-view-bugfix/index.before.html',import.meta.url);
|
||||
const afterUrl=new URL('../../index.html',import.meta.url);
|
||||
const view={
|
||||
bits:273,
|
||||
re:'-15958919693715511752069626580195447581643016686947107473855747766272418724092005269',
|
||||
im:'3884297345146583343999140717464554120497735725078805729405733239132420430333375719',
|
||||
span:'35679807704506851074208413824041445358585910697267232113437882789120793',
|
||||
width:1108,height:1582,iter:1536,
|
||||
};
|
||||
|
||||
function extract(html){return [...html.matchAll(/<script[^>]*>([\s\S]*?)<\/script>/g)].map(m=>m[1])}
|
||||
function appInternal(html){
|
||||
const scripts=extract(html);const kc={};vm.runInNewContext(scripts[0],kc);const kernels=kc.MANDEL_WEBGPU_KERNELS;
|
||||
const element={width:view.width,height:view.height,clientWidth:view.width,clientHeight:view.height,style:{},hidden:false,value:'',textContent:'',disabled:false,addEventListener(){},classList:{toggle(){}},setAttribute(){},removeAttribute(){},showModal(){},close(){},toBlob(){},getBoundingClientRect(){return{left:0,top:0,width:view.width,height:view.height}}};
|
||||
const context={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:()=>`blob:test`,revokeObjectURL(){}},TextEncoder,Blob:class{},console,addEventListener(){},requestAnimationFrame:()=>1,cancelAnimationFrame(){},setTimeout,clearTimeout,setInterval,clearInterval,innerWidth:view.width,innerHeight:view.height,devicePixelRatio:1,localStorage:{getItem(){return null},setItem(){},removeItem(){}}};
|
||||
const cut=scripts[1].indexOf('// ── boot / teardown');
|
||||
const code=scripts[1].slice(0,cut)+`\nglobalThis.internal={fastReferenceWorkerSource,pixelPrecisionBits};})();`;
|
||||
vm.runInNewContext(code,context);return context.internal;
|
||||
}
|
||||
function runWorker(source,message){let result;const wc={self:{},postMessage:r=>{result=r},performance};vm.runInNewContext(source,wc);wc.self.onmessage({data:message});if(!result)throw new Error('worker returned nothing');if(result.type==='error')throw new Error(result.error);return result}
|
||||
function bitLen(v){v=v<0n?-v:v;return v===0n?0:v.toString(2).length}
|
||||
function fixedLog2Abs(v,bits){v=v<0n?-v:v;if(v===0n)return-Infinity;const bl=bitLen(v),take=Math.min(53,bl),sh=bl-take,top=Number(v>>BigInt(sh));return Math.log2(top)+sh-bits}
|
||||
function fixedRatio(a,b){if(!b||!a)return 0;let neg=a<0n;if(neg)a=-a;const q=(a<<52n)/b,v=Number(q)/4503599627370496;return neg?-v:v}
|
||||
function spanMantExp(span,bits){const l=fixedLog2Abs(span,bits),exp=Math.floor(l);return{mant:Math.pow(2,l-exp),exp}}
|
||||
function complexMul(ar,ai,br,bi){return [Math.fround(Math.fround(ar*br)-Math.fround(ai*bi)),Math.fround(Math.fround(ar*bi)+Math.fround(ai*br))]}
|
||||
function nonFiniteSeriesStarts(series,refX,refY,spanMant,width,height,cols=32,rows=32){
|
||||
if(!series?.jump)return 0;let bad=0;const c=series.coeffs.map(Math.fround);
|
||||
for(let jy=0;jy<rows;jy++)for(let ix=0;ix<cols;ix++){
|
||||
const gx=(ix+.5)*width/cols,gy=(jy+.5)*height/rows,dx=Math.fround((gx-refX)/width),dy=Math.fround((refY-gy)/width),d=[Math.fround(spanMant*dx),Math.fround(spanMant*dy)];
|
||||
const d2=complexMul(d[0],d[1],d[0],d[1]),d4=complexMul(d2[0],d2[1],d2[0],d2[1]);
|
||||
const t4=complexMul(c[6],c[7],d4[0],d4[1]);
|
||||
if(!Number.isFinite(t4[0])||!Number.isFinite(t4[1]))bad++;
|
||||
}
|
||||
return bad;
|
||||
}
|
||||
async function analyze(path){
|
||||
const html=await fs.readFile(path,'utf8'),a=appInternal(html);const snap={bits:view.bits,re:BigInt(view.re),im:BigInt(view.im),span:BigInt(view.span)};const targetBits=a.pixelPrecisionBits(snap,view.width,40);
|
||||
const result=runWorker(a.fastReferenceWorkerSource(),{type:'build',id:1,key:'reported-view',sourceBits:view.bits,targetBits,re:view.re,im:view.im,span:view.span,width:view.width,height:view.height,iter:view.iter});
|
||||
const refRe=BigInt(result.referenceRe),refIm=BigInt(result.referenceIm),span=BigInt(view.span),refX=view.width*.5+fixedRatio(refRe-BigInt(view.re),span)*view.width,refY=view.height*.5-fixedRatio(refIm-BigInt(view.im),span)*view.width,se=spanMantExp(span,view.bits);
|
||||
return {sha256:createHash('sha256').update(html).digest('hex'),targetBits,refLen:result.refLen,selectionEscape:result.selectionEscape,referencePixel:{x:refX,y:refY},series:result.series,nonFiniteSeriesStartSamples:nonFiniteSeriesStarts(result.series,refX,refY,se.mant,view.width,view.height),seriesSampleCount:32*32};
|
||||
}
|
||||
const before=await analyze(beforeUrl),after=await analyze(afterUrl);const output={view,before,after,checks:{baselineSeriesActive:before.series.jump>0,baselineHasNonFiniteSeriesStarts:before.nonFiniteSeriesStartSamples>0,patchedSeriesDisabled:after.series.jump===0&&after.nonFiniteSeriesStartSamples===0}};
|
||||
console.log(JSON.stringify(output,null,2));
|
||||
Loading…
Add table
Add a link
Reference in a new issue