Repository navigation
Expand file tree
/
Copy pathrender-benchmark-readme.js
More file actions
146 lines (139 loc) · 9.44 KB
/
Copy pathrender-benchmark-readme.js
File metadata and controls
146 lines (139 loc) · 9.44 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
// Writes the benchmark section of README.md and README_ko.md from one checked
// result file, so both languages publish the same run and the same numbers.
// Every row is the median of the same number of cold samples, shown with its
// range. `benchmark-check.js --write-results` refreshes the file from a run.
//
// bun render-benchmark-readme.js rewrite the sections
// bun render-benchmark-readme.js --check fail when a README differs from them
import { readFileSync, writeFileSync } from 'node:fs'
const START = '<!-- benchmark:start -->'
const END = '<!-- benchmark:end -->'
const results = JSON.parse(readFileSync('benchmark-results.json', 'utf8'))
const rowsOf = (table) => results.rows.filter((row) => row.table === table)
const bytes = (value) => `${value.toLocaleString('en-US')} bytes`
const seconds = (value) => `${value.toFixed(2)}s`
const range = (row) => `${seconds(row.min)} - ${seconds(row.max)}`
const list = (samples) => samples.map(seconds).join(', ')
const TEXT = {
en: {
file: 'README.md',
intro: ({ run, samples }) =>
`[Latest CI benchmark](${run.url}) (commit \`${run.commit}\`, Devup UI ${run.devupUi}) on \`${run.runner}\` with Next.js ${run.next}. Every row is the median of ${samples} cold builds that ran in rotating order, shown with the range of the samples. All Next.js builds use the native TypeScript 7 CLI for type checking. The English and Korean tables are generated from the same checked result file, \`benchmark-results.json\`.`,
sizes:
"Build Size is every byte of the build output directory (`.next`, or `dist` for vinext), including caches and server output. The checked table retains its original historical CSS measurements, which recursively counted `.css` files throughout that output. New runs count only browser-shipped CSS: `.next/static/**/*.css` (Webpack `static/css` and Turbopack `static/chunks`) or vinext `dist/client/**/*.css`, excluding caches and server CSS. A missing browser-assets directory is an error; an existing directory with no CSS counts zero. Approved CSS measurement corrections are limited to explicitly named `devup-ui`, `devup-ui-single` and `vinext-devup-ui` rows. `bun benchmark-check.js --write-css-baseline devup-ui` retains the per-file mode; `bun benchmark-check.js --write-css-baseline devup-ui-single,vinext-devup-ui` selects only the other two rows in one comma-separated argument. Only selected existing rows' CSS bytes change; unlike `--write-baseline`, this preserves sample count, build bytes, ratios, tolerances, all unselected rows and calibration history. Missing or unsupported names and conflicting writer modes are rejected.",
webpack: 'Webpack:',
turbopack: 'Turbopack:',
columns: {
library: 'Library',
version: 'Version',
time: 'Median Build Time',
range: 'Range',
size: 'Build Size',
css: 'CSS Size',
},
note: ({ samples }, [tailwind, direct, stylesheet]) => {
const spread = (row) => [row.min, row.max]
const overlap =
spread(tailwind)[0] <= spread(direct)[1] &&
spread(direct)[0] <= spread(tailwind)[1]
const gap = tailwind.median - direct.median
return `${overlap ? 'The Turbopack ranges overlap' : 'The Turbopack ranges do not overlap'}, so the direct-API median is ${Math.abs(gap).toFixed(2)}s (${Math.abs((gap / tailwind.median) * 100).toFixed(1)}%) ${gap >= 0 ? 'ahead of' : 'behind'} Tailwind on this fixture${overlap ? ' and the two are effectively at parity' : ''}. The ${samples} cold samples were Tailwind \`${list(tailwind.samples)}\`, direct Devup UI \`${list(direct.samples)}\`, and static \`.css.ts\` \`${list(stylesheet.samples)}\`. The fixtures have comparable app shapes, not pixel-identical styling: Tailwind styles the leading paragraph and button more heavily, while Devup UI exercises typed component/style props. Treat these as build-pipeline results rather than a per-rule microbenchmark. Every \`.css.ts\` module runs on the full Boa evaluator, which adds about 20 ms for the first module and under 1 ms for each further one. CI compares the Devup UI rows with a checked baseline (see \`benchmark-gate.js\` for the rule) and fails on a regression beyond the measured noise.`
},
},
ko: {
file: 'README_ko.md',
intro: ({ run, samples }) =>
`[최신 CI 벤치마크](${run.url}) (커밋 \`${run.commit}\`, Devup UI ${run.devupUi})는 \`${run.runner}\`에서 Next.js ${run.next}로 실행했습니다. 모든 행은 순서를 돌려 가며 측정한 cold build ${samples}회의 중앙값이며 샘플의 범위를 함께 보여줍니다. 모든 Next.js 빌드는 네이티브 TypeScript 7 CLI로 타입 검사를 수행합니다. 영어와 한국어 표는 같은 결과 파일 \`benchmark-results.json\`에서 생성합니다.`,
sizes:
'빌드 사이즈는 캐시와 서버 출력을 포함한 빌드 출력 디렉터리(`.next`, vinext는 `dist`)의 모든 바이트입니다. 체크인된 표는 출력 전체의 `.css` 파일을 재귀적으로 합산한 기존 CSS 측정값을 그대로 보존합니다. 새 실행은 브라우저에 제공되는 CSS만 측정합니다: `.next/static/**/*.css`(Webpack `static/css`, Turbopack `static/chunks`) 또는 vinext `dist/client/**/*.css`. 캐시와 서버 CSS는 제외합니다. 브라우저 에셋 디렉터리가 없으면 오류이며, 존재하지만 CSS가 없으면 0입니다. 승인된 CSS 측정 보정은 명시적으로 지정한 `devup-ui`, `devup-ui-single`, `vinext-devup-ui` 행으로 제한됩니다. `bun benchmark-check.js --write-css-baseline devup-ui`는 기존 per-file 모드를 유지하며, `bun benchmark-check.js --write-css-baseline devup-ui-single,vinext-devup-ui`는 쉼표로 구분한 인자 하나로 나머지 두 행만 선택합니다. 선택한 기존 행의 CSS 바이트만 바꾸고, `--write-baseline`과 달리 샘플 수, 빌드 바이트, 비율, 허용 오차, 선택하지 않은 모든 행과 보정 이력을 보존합니다. 이름 누락, 지원하지 않는 이름, 다른 쓰기 모드와의 혼용은 오류입니다.',
webpack: 'Webpack:',
turbopack: 'Turbopack:',
columns: {
library: '라이브러리',
version: '버전',
time: '빌드 시간 중앙값',
range: '범위',
size: '빌드 사이즈',
css: 'CSS 사이즈',
},
note: ({ samples }, [tailwind, direct, stylesheet]) => {
const spread = (row) => [row.min, row.max]
const overlap =
spread(tailwind)[0] <= spread(direct)[1] &&
spread(direct)[0] <= spread(tailwind)[1]
const gap = tailwind.median - direct.median
return `Turbopack 측정 범위는 ${overlap ? '서로 겹치므로' : '겹치지 않으므로'} direct API 중앙값은 이 fixture에서 Tailwind보다 ${Math.abs(gap).toFixed(2)}s(${Math.abs((gap / tailwind.median) * 100).toFixed(1)}%) ${gap >= 0 ? '빠르며' : '느리며'}${overlap ? ' 사실상 동률입니다' : ''}. cold 샘플 ${samples}개는 Tailwind \`${list(tailwind.samples)}\`, direct Devup UI \`${list(direct.samples)}\`, static \`.css.ts\` \`${list(stylesheet.samples)}\`입니다. fixture는 앱 구조만 비슷하고 스타일이 픽셀 단위로 같지는 않습니다. Tailwind는 첫 문단과 버튼에 더 많은 스타일을 주고, Devup UI는 타입이 있는 컴포넌트/스타일 props를 사용합니다. 규칙 단위 마이크로벤치마크가 아니라 빌드 파이프라인 결과로 보세요. 모든 \`.css.ts\`는 full Boa evaluator로 처리하며, 첫 모듈에 약 20ms, 이후 모듈마다 1ms 미만이 더 듭니다. CI는 Devup UI 행을 체크인된 기준값과 비교해 측정된 노이즈를 넘는 회귀에서 실패합니다(규칙은 \`benchmark-gate.js\`).`
},
},
}
function table(text, rows) {
const columns = text.columns
const header = [
columns.library,
columns.version,
columns.time,
columns.range,
columns.size,
columns.css,
]
const body = rows.map((row) => {
const bold = (value) => (row.devup ? `**${value}**` : value)
return [
bold(row.label),
row.version,
bold(seconds(row.median)),
range(row),
bytes(row.bytes),
bytes(row.cssBytes),
]
})
const widths = header.map((_, column) =>
Math.max(...[header, ...body].map((line) => [...line[column]].length)),
)
const line = (cells) =>
`| ${cells.map((cell, column) => cell + ' '.repeat(widths[column] - [...cell].length)).join(' | ')} |`
return [
line(header),
`| ${widths.map((width) => '-'.repeat(width)).join(' | ')} |`,
...body.map(line),
].join('\n')
}
function section(text) {
return [
START,
text.intro(results),
'',
text.sizes,
'',
text.webpack,
'',
table(text, rowsOf('webpack')),
'',
text.turbopack,
'',
table(text, rowsOf('turbopack')),
'',
text.note(results, rowsOf('turbopack')),
END,
].join('\n')
}
let stale = false
for (const text of Object.values(TEXT)) {
const current = readFileSync(text.file, 'utf8').replaceAll('\r\n', '\n')
const start = current.indexOf(START)
const end = current.indexOf(END)
if (start === -1 || end === -1)
throw new Error(`${text.file} has no ${START} ... ${END} block`)
const next =
current.slice(0, start) + section(text) + current.slice(end + END.length)
if (next === current) continue
if (process.argv.includes('--check')) {
console.error(
`${text.file} differs from benchmark-results.json: run \`bun render-benchmark-readme.js\``,
)
stale = true
} else {
writeFileSync(text.file, next)
}
}
if (stale) process.exit(1)