Budget growing history and import recovery encoding
This commit is contained in:
@@ -226,3 +226,15 @@ test('transport preparation cannot rewrite a committed document change or its id
|
||||
const before=editor.getSnapshot(),result=editor.dispatch({type:'element.data',target:target('a'),patch:{text:'must not publish'}});
|
||||
assert.equal(result.code,'invalid_prepared_intent');assert.equal(editor.getSnapshot(),before);
|
||||
});
|
||||
|
||||
test('growing grouped history can yield during recovery encoding without altering undo data',async()=>{
|
||||
const {sealRecoveryImageAsync}=await import('../../src/lib/canvas-runtime/recovery-encoding.ts');
|
||||
const editor=setup();
|
||||
for(let i=0;i<300;i++)assert.equal(editor.dispatch({type:'element.data',target:target('a'),patch:{title:'step-'+i}},{groupId:'large-history'}).status,'applied');
|
||||
const entry=editor.getSnapshot().history.entries.get(editor.getSnapshot().history.undo.at(-1));
|
||||
assert.equal(entry.changes.length,300);let tick=0,yields=0;
|
||||
const pending=sealRecoveryImageAsync(17,1,1,entry,{now:()=>tick++,frameBudgetMs:1,yieldTask:async()=>{yields++;}});
|
||||
assert.equal(editor.dispatch({type:'element.data',target:target('a'),patch:{title:'later'}},{groupId:'large-history'}).status,'applied');
|
||||
const image=await pending;assert.ok(yields>0);assert.deepEqual(image.payload,JSON.parse(JSON.stringify(entry)));assert.equal(image.payload.changes.length,300);
|
||||
assert.equal(editor.history.undo().status,'applied');assert.equal(editor.getSnapshot().document.elements.get('a').data.title,'a');
|
||||
});
|
||||
|
||||
@@ -162,3 +162,12 @@ test('a cheap runner can use the engine-sized ceiling without dropping records o
|
||||
test('the larger ceiling still backs off when a measured commit exceeds the unchanged time budget',async()=>{
|
||||
let clock=0;const records=Array.from({length:500},(_,index)=>({id:`cost-${index}`})),f=fixture(records,{ports:{maxBatch:128,now:()=>clock}}),commit=f.ports.commit;f.ports.commit=(units,...args)=>{clock+=units.length>40?12:1;return commit(units,...args);};await f.runner.start();const sizes=f.calls.map(batch=>batch.length),wide=sizes.findIndex(size=>size>40);assert.ok(wide>=0);assert.ok(sizes[wide+1]<sizes[wide]);assert.equal(f.root.job.status,'completed');assert.equal(f.root.document.elements.size,500);assert.deepEqual(f.calls.flat(),records.map(row=>row.id));assert.equal(f.errors.length,0);await f.runner.close();
|
||||
});
|
||||
|
||||
test('growing import tables yield when encoded while keeping captured IDs and bindings stable',async()=>{
|
||||
const {sealRecoveryImageAsync,sealRecoveryImage}=await import('../../src/lib/canvas-runtime/recovery-encoding.ts');
|
||||
const f=fixture(Array.from({length:300},(_,i)=>({id:'encode-'+i})));await f.runner.start();
|
||||
const job=f.root.job;assert.equal(job.committedSourceKeys.length,300);let ticks=0,yields=0;
|
||||
const image=await sealRecoveryImageAsync(17,1,1,job,{now:()=>ticks++,frameBudgetMs:1,yieldTask:async()=>{yields++;}});
|
||||
assert.ok(yields>0);assert.deepEqual(image,sealRecoveryImage(17,1,1,job));
|
||||
assert.equal(Object.keys(image.payload.stableIds).length,300);assert.equal(Object.keys(image.payload.bindings).length,300);await f.runner.close();
|
||||
});
|
||||
|
||||
@@ -72,3 +72,27 @@ test('cooperative preparation evaluates every unsafe occurrence and rejects erro
|
||||
assert.ok(await sealRecoveryImageAsync(17,1,1,nested(64,dataRecord({omitted:undefined}))));
|
||||
await assert.rejects(sealRecoveryImageAsync(17,0,1,{}),/corrupt_recovery_image/);
|
||||
});
|
||||
|
||||
test('primitive normalization and UTF-8 sizing occur inside the cooperative slice',async()=>{
|
||||
const {dataArray}=await import('../../src/lib/canvas-core/data-containers.ts');
|
||||
const {sealRecoveryImageAsync,sealedRecoveryByteLength}=await import('../../src/lib/canvas-runtime/recovery-encoding.ts');
|
||||
const input=dataArray(Array.from({length:1000},(_,i)=>'budgeted-中文😀-'+i)),stringify=JSON.stringify;
|
||||
let strings=0,tick=0,atFirstYield;
|
||||
JSON.stringify=function(value,...args){if(typeof value==='string'&&value.startsWith('budgeted-'))strings++;return Reflect.apply(stringify,this,[value,...args]);};
|
||||
try{
|
||||
const image=await sealRecoveryImageAsync(17,1,1,input,{now:()=>tick++,frameBudgetMs:1,yieldTask:async()=>{atFirstYield??=strings;}});
|
||||
assert.ok(atFirstYield>0&&atFirstYield<=128,'all scalar encoding ran before the first yield');
|
||||
assert.equal(strings,1000);assert.deepEqual(image.payload,input);
|
||||
assert.equal(sealedRecoveryByteLength(image),new TextEncoder().encode(stringify(image)).byteLength);
|
||||
}finally{JSON.stringify=stringify;}
|
||||
});
|
||||
|
||||
test('unknown proxies retain the synchronous codec trap order and callback count',async()=>{
|
||||
const {dataRecord}=await import('../../src/lib/canvas-core/data-containers.ts');
|
||||
const {sealRecoveryImageAsync}=await import('../../src/lib/canvas-runtime/recovery-encoding.ts');
|
||||
let events=[];
|
||||
const proxy=new Proxy({text:'external'},Object.fromEntries(['get','getPrototypeOf','getOwnPropertyDescriptor','ownKeys','isExtensible'].map(name=>[name,(...args)=>{events.push([name,String(args[1]??'')]);return Reflect[name](...args);}])));
|
||||
const payload=dataRecord({proxy}),expected=sealRecoveryImage(17,1,1,payload),before=events;events=[];
|
||||
const actual=await sealRecoveryImageAsync(17,1,1,payload);
|
||||
assert.deepEqual(actual,expected);assert.deepEqual(events,before);
|
||||
});
|
||||
|
||||
@@ -22,7 +22,7 @@ export async function editDuringCopy({call,evaluate,wait,count}){
|
||||
await evaluate(`(()=>{
|
||||
const element=document.activeElement,samples=[];
|
||||
const input=()=>{
|
||||
const sample={inputAt:performance.now(),value:element.value,copyActive:window.__canvasPerformance.read().localCompleteMs===null};
|
||||
const sample={inputAt:performance.now(),value:element.value,phase:window.__copyInputProbe.phase,committed:window.__copyInputProbe.progress(),copyActive:window.__canvasPerformance.read().localCompleteMs===null};
|
||||
samples.push(sample);
|
||||
requestAnimationFrame(()=>requestAnimationFrame(()=>{
|
||||
sample.frameAt=performance.now();sample.frameDelayMs=sample.frameAt-sample.inputAt;
|
||||
@@ -31,7 +31,10 @@ export async function editDuringCopy({call,evaluate,wait,count}){
|
||||
}));
|
||||
};
|
||||
element.addEventListener('input',input);
|
||||
window.__copyInputProbe={element,samples,dispose(){element.removeEventListener('input',input);}};
|
||||
window.__copyInputProbe={element,samples,phase:'early',progress(){
|
||||
const status=document.querySelector('[aria-label="导入任务 1"] p[role="status"]')?.textContent??'';
|
||||
return Number(status.match(/·\\s*(\\d+)\\s*项/)?.[1]??0);
|
||||
},dispose(){element.removeEventListener('input',input);}};
|
||||
})()`);
|
||||
await call('Input.imeSetComposition',{text:'复',selectionStart:1,selectionEnd:1});
|
||||
await call('Input.insertText',{text:'复制期间输入'});
|
||||
@@ -39,13 +42,25 @@ export async function editDuringCopy({call,evaluate,wait,count}){
|
||||
await call('Input.insertText',{text});
|
||||
await wait(`window.__copyInputProbe.samples.at(-1)?.frameAt!==undefined`,'native input did not reach its next frame');
|
||||
}
|
||||
// Keep the same native editor alive through later publication and ACK work.
|
||||
// Small scenarios can finish during the early burst; the scale gate uses 10k.
|
||||
if(count>=10000)for(const [phase,fraction,text] of [['middle',.5,' M'],['tail',.9,' T']]){
|
||||
await wait(`window.__copyInputProbe.progress()>=${Math.floor((count+3)*fraction)}`,'copy did not reach '+phase+' publication');
|
||||
assert.equal(await evaluate('window.__canvasPerformance.read().localCompleteMs===null'),true,'copy completed before '+phase+' input');
|
||||
await evaluate(`window.__copyInputProbe.phase=${JSON.stringify(phase)};true`);
|
||||
const started=performance.now();await call('Input.insertText',{text});
|
||||
const dispatchRoundTripMs=performance.now()-started;
|
||||
await wait(`window.__copyInputProbe.samples.at(-1)?.phase===${JSON.stringify(phase)}&&window.__copyInputProbe.samples.at(-1)?.frameAt!==undefined`,phase+' input did not reach a frame');
|
||||
await evaluate(`window.__copyInputProbe.samples.at(-1).dispatchRoundTripMs=${dispatchRoundTripMs};true`);
|
||||
}
|
||||
const result=await evaluate(`(()=>{
|
||||
const probe=window.__copyInputProbe,result={samples:probe.samples,value:probe.element.value,retained:probe.element.isConnected&&document.activeElement===probe.element};
|
||||
probe.dispose();probe.element.blur();delete window.__copyInputProbe;return result;
|
||||
})()`);
|
||||
assert.equal(result.retained,true,'copy discarded native input DOM or focus');
|
||||
assert.ok(result.value.endsWith('复制期间输入 A B C'),'native composition/typing was overwritten');
|
||||
assert.ok(result.value.endsWith(count>=10000?'复制期间输入 A B C M T':'复制期间输入 A B C'),'native composition/typing was overwritten');
|
||||
assert.ok(result.samples.some(sample=>sample.copyActive),'all native input happened after copy completion');
|
||||
if(count>=10000)for(const phase of ['middle','tail'])assert.ok(result.samples.some(sample=>sample.phase===phase&&sample.copyActive),phase+' input was not observed before completion');
|
||||
assert.ok(result.samples.every(sample=>sample.retained),'copy interrupted an input presentation frame');
|
||||
assert.ok(result.samples.every(sample=>sample.mountedNodes<100),'native editing mounted offscreen nodes during copy');
|
||||
return result;
|
||||
|
||||
@@ -94,7 +94,7 @@ export async function measureRenderer({call,evaluate,documents,calls=[],root,par
|
||||
document.querySelector('[aria-label="深复制选中内容"]').click();
|
||||
})()`);
|
||||
copying=true;console.log('PERF copy requested');
|
||||
if(concurrentInput){duringCopyInput=await editDuringCopy({call,evaluate,wait,count});assert.equal(duringCopyInput.value,sourceText+'复制期间输入 A B C','concurrent typing lost pre-existing source text');console.log('PERF concurrent input '+JSON.stringify(duringCopyInput));}
|
||||
if(concurrentInput){duringCopyInput=await editDuringCopy({call,evaluate,wait,count});assert.equal(duringCopyInput.value,sourceText+(count>=10000?'复制期间输入 A B C M T':'复制期间输入 A B C'),'concurrent typing lost pre-existing source text');console.log('PERF concurrent input '+JSON.stringify(duringCopyInput));}
|
||||
await wait(`document.querySelectorAll('.react-flow__node').length>0&&document.querySelector('[aria-label="画布导入"]')?.textContent.includes('已加入画布')&&[...document.querySelectorAll('[role=status]')].some(item=>item.textContent.trim()==='已同步')`,'large copy did not finish rendering and synchronization');
|
||||
await evaluate('new Promise(resolve=>requestAnimationFrame(()=>requestAnimationFrame(resolve)))');
|
||||
const internalsBatches=await evaluate('window.__fixtureInternalsBatches??[]');assert.ok(internalsBatches.some(size=>size>1),'mounted nodes did not share a renderer measurement batch');
|
||||
|
||||
Reference in New Issue
Block a user