// Tutorial mechanism probe, not the official performance or accuracy gate. // Run from any directory; see README.md for runtime and Playwright setup. const fs = require('node:fs'); const path = require('node:path'); const http = require('node:http'); const {stripTypeScriptTypes} = require('node:module'); const assert = require('node:assert/strict'); const {chromium} = require(process.env.PLAYWRIGHT_MODULE || 'playwright'); const src = path.resolve(__dirname, '../../src'); const outputFlag = process.argv.indexOf('--output'); if (outputFlag !== -1 && !process.argv[outputFlag + 1]) throw new Error('--output requires a file path'); const modules = new Map(); function compile(dir) { for (const entry of fs.readdirSync(dir,{withFileTypes:true})) { const file = path.join(dir,entry.name); if(entry.isDirectory()) compile(file); else if(file.endsWith('.ts') && !file.endsWith('.test.ts')) { modules.set('/'+path.relative(src,file).replace(/\.ts$/,'.js'),stripTypeScriptTypes(fs.readFileSync(file,'utf8'),{mode:'transform'})); } } } compile(src); const server = http.createServer((req,res)=>{ res.setHeader('Content-Type',req.url==='/'?'text/html':'text/javascript'); if(req.url !== '/' && !modules.has(req.url)) { res.statusCode=404; res.end('Missing module'); return; } res.end(req.url==='/'?'':modules.get(req.url)); }); (async()=>{ await new Promise((resolve,reject)=>{server.once('error',reject);server.listen(0,'127.0.0.1',resolve)}); let browser; try { browser = await chromium.launch({headless:true,...(process.env.CHROME_EXECUTABLE ? {executablePath:process.env.CHROME_EXECUTABLE} : {})}); const page = await browser.newPage(); await page.goto(`http://127.0.0.1:${server.address().port}/`); const result = await page.evaluate(async()=>{ const api = await import('/layout.js'); let calls=0,units=0; const original=OffscreenCanvasRenderingContext2D.prototype.measureText; OffscreenCanvasRenderingContext2D.prototype.measureText=function(text){calls++;units+=text.length;return original.call(this,text)}; const font='16px "Helvetica Neue", "PingFang SC", "Geeza Pro"'; const cases=[ {name:'latin',text:'The quick brown fox jumps over the lazy dog. Accurate text layout needs cached measurements and careful line breaking. '.repeat(12)}, {name:'cjk',text:'高性能文本布局的关键,是复用字体测量结果,并正确处理中文标点和换行。'.repeat(12)}, {name:'pre-wrap',text:'A long line with\ttabs\n preserved spaces and a discretionary soft\u00adhyphen.\n'.repeat(12),options:{whiteSpace:'pre-wrap'}}, ]; const out=[]; for(const c of cases){ api.clearCache();calls=0;units=0; const p=api.prepare(c.text,font,c.options); const first={calls,units};calls=0;units=0; api.prepare(c.text,font,c.options); const second={calls,units};calls=0; const widths=[80,121,200,319,320,321,480,800]; const heights=widths.map(w=>api.layout(p,w,24).height); const layoutCalls=calls; const el=document.createElement('div'); Object.assign(el.style,{font,lineHeight:'24px',overflowWrap:'break-word',whiteSpace:c.options?.whiteSpace||'normal',position:'absolute'}); el.textContent=c.text;document.body.appendChild(el); const domHeights=widths.map(w=>{el.style.width=w+'px';return el.getBoundingClientRect().height}); let sink=0; for(let i=0;i<3000;i++)sink+=api.layout(p,widths[i%8],24).height; const pretextMs=[],domMs=[]; for(let r=0;r<5;r++){ let t=performance.now(); for(let i=0;i<3000;i++)sink+=api.layout(p,widths[i%8],24).height; pretextMs.push(performance.now()-t); t=performance.now(); for(let i=0;i<3000;i++){el.style.width=widths[i%8]+'px';sink+=el.getBoundingClientRect().height;} domMs.push(performance.now()-t); } el.remove(); const withSegments=api.prepareWithSegments(c.text,font,c.options); const lineApiCounts=widths.map(w=>api.layoutWithLines(withSegments,w,24).lines.length); const median=a=>a.sort((a,b)=>a-b)[2]; out.push({name:c.name,chars:c.text.length,first,second,layoutCalls,fastPath:p.simpleLineWalkFastPath,widths,heights,domHeights,lineApiCounts,pretextMedianMs:median(pretextMs),domInterleavedMedianMs:median(domMs),sink}); } return {userAgent:navigator.userAgent,devicePixelRatio,language:document.documentElement.lang,font,iterations:3000,rounds:5,out}; }); for(const row of result.out){ assert.ok(row.first.calls > 0, row.name+': first preparation must measure'); assert.equal(row.second.calls,0,row.name+': repeated preparation must reuse Canvas metrics'); assert.equal(row.layoutCalls,0,row.name+': layout must not measure'); assert.deepEqual(row.heights,row.domHeights,row.name+': heights must match DOM for these fixtures'); assert.deepEqual(row.lineApiCounts,row.heights.map(h=>h/24),row.name+': line APIs must agree'); } if(outputFlag !== -1) fs.writeFileSync(path.resolve(process.argv[outputFlag + 1]),JSON.stringify(result,null,2)+'\n'); console.log(JSON.stringify(result,null,2)); } finally {if(browser)await browser.close();server.close();} })().catch(e=>{console.error(e);process.exitCode=1});