perf(canvas): batch progressive import parsing and publication
This commit is contained in:
@@ -118,6 +118,18 @@ export function createImportRunner(ports:ImportRunnerPorts){
|
||||
function commit(units:readonly ImportUnit[],job:ImportJob,value:ImportJob){const start=now(),result=ports.commit(units,value,job.generation),cost=Math.max(.01,(now()-start)/units.length);costPerUnit=costPerUnit===undefined?cost:Math.max(cost,costPerUnit*.8+cost*.2);if(!['applied','rejected'].includes(result?.status)||!equal(current(),result.status==='applied'?value:job))throw fault('invalid_import_commit');return result;}
|
||||
async function pump(signal:AbortSignal){
|
||||
let frame=now(),didWork=false;
|
||||
type ReadResult={kind:'line';line:ImportLine|null}|{kind:'error';error:unknown};
|
||||
let reading:{cursor:number;result:Promise<ReadResult>}|undefined;
|
||||
const decoded=new Map<string,{unit:ImportUnit;digest:string;bytes:number}>();let decodedBytes=0;
|
||||
function cache(key:string,value:{unit:ImportUnit;digest:string;bytes:number}){const previous=decoded.get(key);if(previous)decodedBytes-=previous.bytes;decoded.delete(key);decoded.set(key,value);decodedBytes+=value.bytes;while(decoded.size>maxBatch||decodedBytes>maxBytes){const[key,value]=decoded.entries().next().value!;decoded.delete(key);decodedBytes-=value.bytes;}}
|
||||
function forget(key:string){const value=decoded.get(key);if(value){decodedBytes-=value.bytes;decoded.delete(key);}}
|
||||
async function read(cursor:number,flushReady:boolean):Promise<ReadResult|{kind:'flush'}>{
|
||||
if(reading&&reading.cursor!==cursor)throw fault('invalid_import_cursor');
|
||||
reading??={cursor,result:Promise.resolve().then(()=>ports.source.read(cursor,signal)).then(line=>({kind:'line' as const,line}),error=>({kind:'error' as const,error}))};
|
||||
let timer:ReturnType<typeof setTimeout>|undefined;
|
||||
const result=flushReady?await Promise.race([reading.result,new Promise<{kind:'flush'}>(resolve=>{timer=setTimeout(()=>resolve({kind:'flush'}),0);})]):await reading.result;
|
||||
if(timer!==undefined)clearTimeout(timer);if(result.kind!=='flush')reading=undefined;return result;
|
||||
}
|
||||
if(!ports.optimisticLocal)await ports.persist();
|
||||
while(!closed&&!signal.aborted&&!holds.size){
|
||||
let job=current();if(job.status!=='running')return;
|
||||
@@ -130,6 +142,10 @@ export function createImportRunner(ports:ImportRunnerPorts){
|
||||
if(chosen.length){
|
||||
const units:ImportUnit[]=[];let failed:ImportFailure|undefined;
|
||||
for(const locator of chosen){
|
||||
const cached=decoded.get(locator.key);
|
||||
if(cached&&cached.digest===locator.digest){units.push(cached.unit);continue;}
|
||||
// A source need not support concurrent reads. Finish any speculative read before seeking.
|
||||
if(reading)await reading.result;
|
||||
const line=await ports.source.read(locator.cursor,signal);
|
||||
if(closed||signal.aborted||current().generation!==job.generation)return;
|
||||
if(!line||line.error||line.skip||line.nextCursor!==locator.nextCursor)throw fault('import_source_changed');
|
||||
@@ -152,6 +168,7 @@ export function createImportRunner(ports:ImportRunnerPorts){
|
||||
if(now()-frame>=budget){await(ports.yieldTask??yieldTask)();frame=now();}
|
||||
}
|
||||
}else await ports.persist();
|
||||
for(const unit of units)forget(unit.key);
|
||||
continue;
|
||||
}
|
||||
if(job.eof){
|
||||
@@ -159,23 +176,38 @@ export function createImportRunner(ports:ImportRunnerPorts){
|
||||
publish(job,{status:job.failures.length||unresolved.length?'partial':'completed',failures:[...job.failures,...unresolved]});await ports.persist();return;
|
||||
}
|
||||
if(job.pending.length>=maxPending||new TextEncoder().encode(JSON.stringify(job.pending)).byteLength>=maxBytes){publish(job,{status:'paused',error:'dependency_capacity'});await ports.persist();return;}
|
||||
const line=await ports.source.read(job.cursor,signal);
|
||||
if(closed||signal.aborted||current().generation!==job.generation)return;
|
||||
if(!line){publish(job,{eof:true});await ports.persist();continue;}
|
||||
if(line.cursor!==job.cursor||!Number.isSafeInteger(line.nextCursor)||line.nextCursor<=job.cursor)throw fault('invalid_import_cursor');
|
||||
if(line.skip){publish(job,{cursor:line.nextCursor});await ports.persist();continue;}
|
||||
if(line.error){publish(job,{cursor:line.nextCursor,failures:[...job.failures,{key:line.key,cursor:line.cursor,code:line.error}]});await ports.persist();continue;}
|
||||
let patch:Partial<ImportJob>;
|
||||
try{
|
||||
const prepared=prepare(line,job),key=prepared.unit.key;
|
||||
if(job.committedSourceKeys.includes(key)||job.pending.some(unit=>unit.key===key)||job.failures.some(failure=>failure.key===key))throw fault('duplicate_import_source_key');
|
||||
const locator={key,cursor:line.cursor,nextCursor:line.nextCursor,dependencies:prepared.unit.dependencies,digest:await digest(prepared.unit),byteLength:new TextEncoder().encode(JSON.stringify(prepared.unit)).byteLength,changeCount:prepared.unit.changes.length};
|
||||
patch={cursor:line.nextCursor,stableIds:prepared.ids,pending:[...job.pending,locator]};
|
||||
}catch(error){patch={cursor:line.nextCursor,failures:[...job.failures,{key:line.key,cursor:line.cursor,code:(error as {code?:string}).code??'import_decode_failed'}]};}
|
||||
if(closed||signal.aborted||current().generation!==job.generation)return;
|
||||
publish(job,patch);
|
||||
const staged=patch.pending?.at(-1),immediatelyReady=staged?.dependencies.every(key=>job.committedSourceKeys.includes(key));
|
||||
if(!ports.optimisticLocal||!immediatelyReady)await ports.persist();
|
||||
// Stage a bounded parser slice, then publish its cursor/IDs/locators once. This is
|
||||
// unpublished transaction preparation, never another durable task state store.
|
||||
const base=job;let staged=job,records=0;
|
||||
while(!staged.eof&&records<maxBatch&&staged.pending.length<maxPending&&new TextEncoder().encode(JSON.stringify(staged.pending)).byteLength<maxBytes){
|
||||
const ready=staged.pending.some(unit=>unit.dependencies.every(key=>committed.has(key)));
|
||||
const result=await read(staged.cursor,ready);
|
||||
if(closed||signal.aborted||current().generation!==base.generation)return;
|
||||
if(result.kind==='flush')break;
|
||||
if(result.kind==='error'){
|
||||
if(ready){reading={cursor:staged.cursor,result:Promise.resolve(result)};break;}
|
||||
if(staged!==base){const{generation,...patch}=staged;publish(base,patch);}throw result.error;
|
||||
}
|
||||
const line=result.line;
|
||||
if(!line){staged={...staged,eof:true};break;}
|
||||
if(line.cursor!==staged.cursor||!Number.isSafeInteger(line.nextCursor)||line.nextCursor<=staged.cursor)throw fault('invalid_import_cursor');
|
||||
let patch:Partial<ImportJob>;
|
||||
if(line.skip)patch={cursor:line.nextCursor};
|
||||
else if(line.error)patch={cursor:line.nextCursor,failures:[...staged.failures,{key:line.key,cursor:line.cursor,code:line.error}]};
|
||||
else try{
|
||||
const prepared=prepare(line,staged),key=prepared.unit.key;
|
||||
if(staged.committedSourceKeys.includes(key)||staged.pending.some(unit=>unit.key===key)||staged.failures.some(failure=>failure.key===key))throw fault('duplicate_import_source_key');
|
||||
const locator={key,cursor:line.cursor,nextCursor:line.nextCursor,dependencies:prepared.unit.dependencies,digest:await digest(prepared.unit),byteLength:new TextEncoder().encode(JSON.stringify(prepared.unit)).byteLength,changeCount:prepared.unit.changes.length};
|
||||
cache(key,{unit:prepared.unit,digest:locator.digest,bytes:locator.byteLength});
|
||||
patch={cursor:line.nextCursor,stableIds:prepared.ids,pending:[...staged.pending,locator]};
|
||||
}catch(error){patch={cursor:line.nextCursor,failures:[...staged.failures,{key:line.key,cursor:line.cursor,code:(error as {code?:string}).code??'import_decode_failed'}]};}
|
||||
if(closed||signal.aborted||current().generation!==base.generation)return;
|
||||
staged={...staged,...patch};records++;
|
||||
// First ready record displays immediately; subsequent slices amortize publication.
|
||||
if(costPerUnit===undefined&&staged.pending.some(unit=>unit.dependencies.every(key=>committed.has(key)))||now()-frame>=budget)break;
|
||||
}
|
||||
if(staged!==base){const{generation,...patch}=staged;publish(base,patch);if(!ports.optimisticLocal||!staged.pending.some(unit=>unit.dependencies.every(key=>committed.has(key))))await ports.persist();}
|
||||
|
||||
}
|
||||
}
|
||||
function launch(){
|
||||
|
||||
Reference in New Issue
Block a user