90 lines
5.0 KiB
JavaScript
90 lines
5.0 KiB
JavaScript
// 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==='/'?'<html lang="en"><body></body></html>':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});
|