This commit is contained in:
33333-33333 2026-09-29 23:45:12 +09:00
commit f5fc9b76ce
73 changed files with 32628 additions and 376 deletions

View 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');