27 lines
4.0 KiB
JavaScript
27 lines
4.0 KiB
JavaScript
/**
|
||
* 查询快照代次的GC保留回归。独立Node进程开启--expose-gc,用WeakRef区分显式持有的旧前缀与已丢弃代次。
|
||
* 这是受控强制GC场景,不能据此宣称生产内存没有任何泄漏或保证垃圾回收发生时机。
|
||
*/
|
||
import {test} from 'node:test';
|
||
import assert from 'node:assert/strict';
|
||
import {spawnSync} from 'node:child_process';
|
||
// 同时测手动增量链、显式持有链和editor链,不只检查WeakRef消失,还检查旧前缀与最新查询都仍正确。
|
||
test('latest query methods do not keep discarded query/document generations alive; explicitly held prefixes still work',()=>{
|
||
// 转file URL供子进程ESM动态脚本导入;子进程使用当前process.execPath,避免与主测试互相共享GC状态。
|
||
const core=new URL('../../src/lib/canvas-core/index.ts',import.meta.url).href,queries=new URL('../../src/lib/canvas-core/queries.ts',import.meta.url).href;
|
||
// 嵌入脚本的chain(keep)连续推进30代query,held只在keep=true保留初始查询;WeakRef本身不作为强引用。
|
||
// editorChain用真实editor推进同样30次几何,检查当前查询方法是否闭包保留旧document/query。
|
||
// 12次setImmediate+gc跨过任务边界后输出布尔存活状态,同时读取显式旧前缀及最新文档的x证明可用性。
|
||
const script=`
|
||
import {createDocument,createEditor,applyChanges,createRegistry,builtinComponents} from ${JSON.stringify(core)};
|
||
import {createDocumentQueries,advanceDocumentQueries} from ${JSON.stringify(queries)};
|
||
import {setImmediate} from 'node:timers/promises';
|
||
const registry=createRegistry(builtinComponents());
|
||
function chain(keep){let document=createDocument('p',[{id:'p',parent:null}],[{id:'a',incarnation:1,ownerCanvasId:'p',componentId:'text',dataVersion:1,structure:'node',geometry:{x:0,y:0,width:100,height:100,rotation:0},data:{title:'a',text:''}}]),query=createDocumentQueries(document,registry);const weakDocument=new WeakRef(document),weakQuery=new WeakRef(query),held=keep?query:undefined;for(let i=1;i<=30;i++){const changes=[{kind:'field',target:{id:'a',incarnation:1},field:'geometry',before:document.elements.get('a').geometry,after:{x:i,y:0,width:100,height:100,rotation:0}}];document=applyChanges(document,changes);query=advanceDocumentQueries(query,document,registry,i,changes);}return{last:query,weakDocument,weakQuery,held};}
|
||
function editorChain(){const editor=createEditor({registry,document:createDocument('p',[{id:'p',parent:null}],[{id:'a',incarnation:1,ownerCanvasId:'p',componentId:'text',dataVersion:1,structure:'node',geometry:{x:0,y:0,width:100,height:100,rotation:0},data:{title:'a',text:''}}]),queryCoverage:()=>({projectComplete:true,topologyComplete:true,completeCanvasIds:['p']})}),weakDocument=new WeakRef(editor.getSnapshot().document),weakQuery=new WeakRef(editor.getQueries());for(let i=1;i<=30;i++)editor.dispatch({type:'element.geometry',target:{id:'a',incarnation:1},patch:{x:i}});return{editor,weakDocument,weakQuery};}
|
||
const dropped=chain(false),kept=chain(true),owned=editorChain();for(let i=0;i<12;i++){await setImmediate();global.gc();}
|
||
console.log(JSON.stringify({droppedDocument:!!dropped.weakDocument.deref(),droppedQuery:!!dropped.weakQuery.deref(),keptDocument:!!kept.weakDocument.deref(),keptQuery:!!kept.weakQuery.deref(),oldX:kept.held.document.elements.get('a').geometry.x,newX:dropped.last.document.elements.get('a').geometry.x,members:dropped.last.members('p'),editorDocument:!!owned.weakDocument.deref(),editorQuery:!!owned.weakQuery.deref(),editorX:owned.editor.getSnapshot().document.elements.get('a').geometry.x}));`;
|
||
// 子进程限时15秒,先要求正常退出,再比JSON;旧丢弃代次应回收,显式held应存活且oldX仍0,最新x=30。
|
||
const child=spawnSync(process.execPath,['--expose-gc','--input-type=module','-e',script],{encoding:'utf8',timeout:15000});assert.equal(child.status,0,child.stderr||String(child.error));assert.deepEqual(JSON.parse(child.stdout),{droppedDocument:false,droppedQuery:false,keptDocument:true,keptQuery:true,oldX:0,newX:30,members:['a'],editorDocument:false,editorQuery:false,editorX:30});
|
||
});
|