import fs from 'node:fs' import path from 'node:path' import vm from 'node:vm' import {pathToFileURL,fileURLToPath} from 'node:url' import assert from 'node:assert/strict' import * as current from '../src/common/ime-engine.js' const app=path.resolve(path.dirname(fileURLToPath(import.meta.url)),'..') const args=process.argv.slice(2),option=name=>args[args.indexOf(name)+1] assert(args.includes('--baseline'),'Use --baseline ') const baseline=await import(pathToFileURL(path.resolve(option('--baseline')))) const cases=[] for(const text of ['shi','shij','shijie','niha','nihao','beij','beijing','zhongguo'])for(const mode of [false,true,'14'])cases.push([mode===false?text:mode==='14'?current.code14Of(text):current.digitsOf(text),mode]) const identity=rows=>rows.map(x=>x.word+'@'+x.pinyin+'@'+x.consume+'@'+!!x.completion).sort() async function sample(module){ let reads=0,readChars=0,largestRead=0,resultCount=0 const dictionary=module.createDictionary(async uri=>{const text=fs.readFileSync(app+'/src'+uri,'utf8');reads++;readChars+=text.length;largestRead=Math.max(largestRead,text.length);return text}) if(global.gc)global.gc() const before=process.memoryUsage().heapUsed,times=[],rows=[] for(let round=0;round<5;round++)for(const [raw,mode]of cases){ const start=performance.now(),result=await dictionary.lookup(raw,mode,{},'你好');times.push(performance.now()-start) if(!round)rows.push(identity(result));resultCount+=result.length } if(global.gc)global.gc() const retainedHeapBytes=process.memoryUsage().heapUsed-before const sorted=times.slice().sort((a,b)=>a-b) const metrics={queries:times.length,resultCount,reads,readChars,largestRead,retainedHeapBytes,medianMs:sorted[Math.floor(sorted.length/2)],p95Ms:sorted[Math.floor(sorted.length*.95)],totalMs:times.reduce((a,b)=>a+b,0),stats:dictionary.stats||null} dictionary.clear();return {metrics,rows} } const old=await sample(baseline),now=await sample(current) assert.deepEqual(now.rows,old.rows,'Exact candidates and completions survive optimization in all three layouts') assert.equal(now.metrics.resultCount,old.metrics.resultCount) const meta=JSON.parse(fs.readFileSync(app+'/src/assets/ime-dict/meta.json','utf8')) const runtime=JSON.parse(fs.readFileSync(app+'/src/assets/ime-dict/runtime.json','utf8')) const masks=[['short','s','23456789'],['long','','23456789'],['dualShort','ds','0123456789abcd'],['dualLong','d','0123456789abcd'],['associations','a','0123456789abcdef'],['completions','c','0123456789abcdef']] const restoredNames=[] for(const [field,prefix,alphabet]of masks)for(let mask=0;mask>3)*2,(i>>3)*2+2),16)&1<<(i&7)){ let code='',value=i;for(let digit=0;digit'补'+i)]},frequencies:{同:999}}} const edge=current.createDictionary(async uri=>{const name=path.basename(uri,'.json');if(!(name in source))throw new Error('absent');return JSON.stringify(source[name])}) const edgeRows=await edge.lookup('abcd',false,{},'') assert.equal(edgeRows.filter(x=>x.completion).length,12);assert.equal(edgeRows.filter(x=>x.word==='同').length,1) assert((await edge.predict('你')).length===0);edge.clear() const candidates=Array.from({length:12000},(_,i)=>({word:i%13===0?'完整词组':String.fromCodePoint(0x4e00+i%1200)})) const plan=current.candidatePageStarts(candidates),restored=[] let maxVisible=0 for(let page=0;pagerow.items.map(cell=>cell.item));restored.push(...visible);maxVisible=Math.max(maxVisible,visible.length) assert(rows.reduce((sum,row)=>sum+row.units,0)<=4) } assert.deepEqual(restored,candidates);assert(maxVisible<=16) // A clear while file I/O is pending must not refill a closed session's cache. let release;const paused=current.createDictionary(uri=>uri.endsWith('runtime.json')?new Promise(resolve=>release=resolve):fs.readFileSync(app+'/src'+uri,'utf8')) const obsolete=paused.lookup('shi',false,{},'',()=>false);paused.clear();release(fs.readFileSync(app+'/src/assets/ime-dict/runtime.json','utf8'));assert.equal((await obsolete).length,0);assert.equal(paused.cacheSize,0) const reactive=text=>{const source=text.match(/