66 行
6.0 KiB
JavaScript
66 行
6.0 KiB
JavaScript
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 <v1.2.4 ime-engine.js>')
|
|
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<runtime[field].length;mask++){
|
|
const length=field==='short'||field==='dualShort'?mask+1:field==='long'?4:field==='dualLong'?3:2,bits=runtime[field][mask]
|
|
for(let i=0;i<alphabet.length**length;i++)if(parseInt(bits.slice((i>>3)*2,(i>>3)*2+2),16)&1<<(i&7)){
|
|
let code='',value=i;for(let digit=0;digit<length;digit++){code=alphabet[value%alphabet.length]+code;value=Math.floor(value/alphabet.length)}restoredNames.push(prefix+code)
|
|
}
|
|
}
|
|
assert.deepEqual(restoredNames.sort(),[...meta.shards,...meta.association_shards,...meta.completion_shards].sort(),'Every runtime presence bit matches the audit manifest, including absent buckets')
|
|
// Duplicate exact words must not consume the bounded completion budget. Legacy
|
|
// manifests without predictive lists remain compatible; zero-frequency rows survive.
|
|
const source={meta:{shards:['2223']},'2223':{entries:{abcd:['同'],abcde:['同',...Array.from({length:15},(_,i)=>'补'+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;page<plan.length;page++){
|
|
const rows=current.candidatePageRows(candidates,plan,page),visible=rows.flatMap(row=>row.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(/<script>([\s\S]*?)<\/script>/)[1].replace(/^\s*import .+$/gm,'').replace('export default','module.exports=');const module={exports:{}};vm.runInNewContext(source,{module,stylesForTheme:()=>({})});return Object.keys(module.exports.data).length}
|
|
const report={method:'Node V8 comparison with identical dictionary; heap figures are host measurements, not MCU or watch RAM',exactCandidateCases:cases.length,completionCases:cases.length,runtimePresenceShards:restoredNames.length,baseline:old.metrics,optimized:now.metrics,pagePlan:{candidates:candidates.length,pages:plan.length,maxVisibleCells:maxVisible},runtimeIndexBytes:fs.statSync(app+'/src/assets/ime-dict/runtime.json').size,auditMetadataBytes:fs.statSync(app+'/src/assets/ime-dict/meta.json').size,reactiveProperties:{baseline:args.includes('--baseline-component')?reactive(fs.readFileSync(option('--baseline-component'),'utf8')):null,optimized:reactive(fs.readFileSync(app+'/src/components/CrabInput/index.ux','utf8'))},passed:true}
|
|
if(args.includes('--output'))fs.writeFileSync(option('--output'),JSON.stringify(report,null,2)+'\n')
|
|
console.log(JSON.stringify(report))
|