Skip to content

Commit 717be90

Browse files
authored
chore: improve comparison comments (#923)
* chore: improve comparison comments * fixup!
1 parent 587b8f9 commit 717be90

6 files changed

Lines changed: 128 additions & 58 deletions

File tree

scripts/comparators/file-size.mjs

Lines changed: 27 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
1-
import { stat, readdir } from 'node:fs/promises';
1+
import { stat } from 'node:fs/promises';
22
import path from 'node:path';
33

4-
import { BASE, BENCHMARK_FILE, HEAD, TITLE } from '../constants.mjs';
4+
import { BASE, HEAD, TITLE } from '../constants.mjs';
5+
import { listOutputFiles } from './files.mjs';
56
import { comparePerformance } from './performance.mjs';
67

78
const UNITS = ['B', 'KB', 'MB', 'GB'];
@@ -26,7 +27,7 @@ const formatBytes = bytes => {
2627
* @returns {Promise<Map<string, number>>} Map of filename to size in bytes
2728
*/
2829
const getStats = async dir => {
29-
const files = (await readdir(dir)).filter(file => file !== BENCHMARK_FILE);
30+
const files = await listOutputFiles(dir);
3031
return new Map(
3132
await Promise.all(
3233
files.map(async f => [f, (await stat(path.join(dir, f))).size])
@@ -37,18 +38,16 @@ const getStats = async dir => {
3738
// Fetch stats for both directories in parallel
3839
const [baseStats, headStats] = await Promise.all([BASE, HEAD].map(getStats));
3940

40-
const didChange = f =>
41-
baseStats.has(f) && headStats.has(f) && baseStats.get(f) !== headStats.get(f);
41+
const didChange = f => baseStats.get(f) !== headStats.get(f);
4242

4343
const toDiffObject = f => ({
4444
file: f,
45-
base: baseStats.get(f),
46-
head: headStats.get(f),
47-
diff: headStats.get(f) - baseStats.get(f),
45+
base: baseStats.get(f) ?? 0,
46+
head: headStats.get(f) ?? 0,
47+
diff: (headStats.get(f) ?? 0) - (baseStats.get(f) ?? 0),
4848
});
4949

50-
// Find files that exist in both directories but have different sizes,
51-
// then sort by absolute diff (largest changes first)
50+
// Find files whose presence or size changed, then show the largest changes first.
5251
const changed = [...new Set([...baseStats.keys(), ...headStats.keys()])]
5352
.filter(didChange)
5453
.map(toDiffObject)
@@ -58,19 +57,30 @@ const sections = [];
5857

5958
// Output markdown table if there are changes
6059
if (changed.length) {
60+
const totalDiff = changed.reduce((total, { diff }) => total + diff, 0);
61+
const totalSign = totalDiff > 0 ? '+' : '';
6162
const rows = changed.map(({ file, base, head, diff }) => {
6263
const sign = diff > 0 ? '+' : '';
63-
const percent = `${sign}${((diff / base) * 100).toFixed(2)}%`;
64-
const diffFormatted = `${sign}${formatBytes(diff)} (${percent})`;
64+
const percent =
65+
base === 0 ? '' : ` (${sign}${((diff / base) * 100).toFixed(1)}%)`;
66+
const diffFormatted = `${sign}${formatBytes(diff)}${percent}`;
6567

66-
return `| \`${file}\` | ${formatBytes(base)} | ${formatBytes(head)} | ${diffFormatted} |`;
68+
return `| \`${file}\` | ${baseStats.has(file) ? formatBytes(base) : '—'} | ${headStats.has(file) ? formatBytes(head) : '—'} | ${diffFormatted} |`;
6769
});
6870

6971
sections.push(
70-
'### Output size',
71-
'| File | Base | Head | Diff |',
72-
'|-|-|-|-|',
73-
rows.join('\n')
72+
[
73+
`**Output size:** ${changed.length} ${changed.length === 1 ? 'file' : 'files'} changed · net ${totalSign}${formatBytes(totalDiff)}`,
74+
'',
75+
'<details>',
76+
'<summary>File size details</summary>',
77+
'',
78+
'| File | Main | PR | Change |',
79+
'| --- | ---: | ---: | ---: |',
80+
rows.join('\n'),
81+
'',
82+
'</details>',
83+
].join('\n')
7484
);
7585
}
7686

scripts/comparators/files.mjs

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import { glob } from 'node:fs/promises';
2+
import path from 'node:path';
3+
4+
import { BENCHMARK_FILE, COMPARISON_FILE } from '../constants.mjs';
5+
6+
const METADATA_FILES = new Set([BENCHMARK_FILE, COMPARISON_FILE]);
7+
8+
export const listOutputFiles = async directory => {
9+
const entries = glob('**/*', {
10+
cwd: directory,
11+
withFileTypes: true,
12+
exclude: entry => METADATA_FILES.has(entry.name),
13+
});
14+
15+
return (await Array.fromAsync(entries))
16+
.filter(entry => entry.isFile())
17+
.map(entry =>
18+
path.relative(directory, path.join(entry.parentPath, entry.name))
19+
)
20+
.sort();
21+
};

scripts/comparators/object-assertion.mjs

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,30 @@
11
import assert from 'node:assert';
2-
import { readdir, readFile } from 'node:fs/promises';
2+
import { readFile } from 'node:fs/promises';
33
import { join } from 'node:path';
44

5-
import { BASE, BENCHMARK_FILE, HEAD, TITLE } from '../constants.mjs';
5+
import { BASE, HEAD, TITLE } from '../constants.mjs';
6+
import { listOutputFiles } from './files.mjs';
67
import { comparePerformance } from './performance.mjs';
78

8-
const files = (await readdir(BASE)).filter(file => file !== BENCHMARK_FILE);
9+
const [baseFiles, headFiles] = await Promise.all(
10+
[BASE, HEAD].map(directory => listOutputFiles(directory))
11+
);
12+
const baseFileSet = new Set(baseFiles);
13+
const headFileSet = new Set(headFiles);
14+
const files = [...new Set([...baseFiles, ...headFiles])];
915

1016
export const details = (summary, diff) =>
1117
`<details>\n<summary>${summary}</summary>\n\n\`\`\`diff\n${diff}\n\`\`\`\n\n</details>`;
1218

1319
const getFileDiff = async file => {
20+
if (!baseFileSet.has(file)) {
21+
return `- \`${file}\` added`;
22+
}
23+
24+
if (!headFileSet.has(file)) {
25+
return `- \`${file}\` removed`;
26+
}
27+
1428
const basePath = join(BASE, file);
1529
const headPath = join(HEAD, file);
1630

@@ -31,7 +45,10 @@ const filteredResults = results.filter(Boolean);
3145

3246
const sections = [];
3347
if (filteredResults.length) {
34-
sections.push('### Output', filteredResults.join('\n'));
48+
sections.push(
49+
`**Output:** ${filteredResults.length} ${filteredResults.length === 1 ? 'file differs' : 'files differ'}`,
50+
filteredResults.join('\n')
51+
);
3552
}
3653

3754
const performance = await comparePerformance();

scripts/comparators/performance.mjs

Lines changed: 17 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -27,14 +27,17 @@ const formatPercent = (base, diff) => {
2727
return 'n/a';
2828
}
2929

30-
const percent = (diff / base) * 100;
31-
return `${percent > 0 ? '+' : ''}${percent.toFixed(2)}%`;
30+
return `${Math.abs((diff / base) * 100).toFixed(1)}%`;
3231
};
3332

34-
const formatDiff = (base, head, formatter) => {
33+
const formatChange = (base, head, { increase, decrease }) => {
3534
const diff = head - base;
36-
const value = `${diff > 0 ? '+' : ''}${formatter(diff)}`;
37-
return `${value} (${formatPercent(base, diff)})`;
35+
36+
if (diff === 0) {
37+
return 'unchanged';
38+
}
39+
40+
return `${formatPercent(base, diff)} ${diff > 0 ? increase : decrease}`;
3841
};
3942

4043
const readBenchmark = async directory => {
@@ -53,24 +56,16 @@ const readBenchmark = async directory => {
5356

5457
const METRICS = [
5558
{
56-
label: 'Elapsed time',
59+
label: 'Generation time',
5760
value: benchmark => benchmark.elapsedSeconds,
5861
format: formatSeconds,
62+
change: { increase: 'slower', decrease: 'faster' },
5963
},
6064
{
61-
label: 'User CPU time',
62-
value: benchmark => benchmark.userCpuSeconds,
63-
format: formatSeconds,
64-
},
65-
{
66-
label: 'System CPU time',
67-
value: benchmark => benchmark.systemCpuSeconds,
68-
format: formatSeconds,
69-
},
70-
{
71-
label: 'Peak resident memory',
65+
label: 'Peak memory',
7266
value: benchmark => benchmark.maxRssKiB * 1024,
7367
format: formatBytes,
68+
change: { increase: 'higher', decrease: 'lower' },
7469
},
7570
];
7671

@@ -96,21 +91,18 @@ export const comparePerformance = async (
9691
return '';
9792
}
9893

99-
const rows = METRICS.map(({ label, value, format }) => {
94+
const rows = METRICS.map(({ label, value, format, change }) => {
10095
const baseValue = value(base);
10196
const headValue = value(head);
10297

10398
if (!Number.isFinite(baseValue) || !Number.isFinite(headValue)) {
10499
throw new TypeError(`Invalid ${label.toLowerCase()} benchmark value`);
105100
}
106101

107-
return `| ${label} | ${format(baseValue)} | ${format(headValue)} | ${formatDiff(baseValue, headValue, format)} |`;
102+
return `- **${label}:** ${formatChange(baseValue, headValue, change)} (${format(baseValue)} ${format(headValue)})`;
108103
});
109104

110-
return [
111-
'### Performance',
112-
'| Metric | Base | Head | Diff |',
113-
'|-|-|-|-|',
114-
...rows,
115-
].join('\n');
105+
return ['**Performance estimate** <sub>(single CI run)</sub>', ...rows].join(
106+
'\n'
107+
);
116108
};

scripts/constants.mjs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ export const TITLE =
1111
process.env.TITLE || `## \`${process.env.GENERATOR ?? '...'}\` Generator`;
1212

1313
export const BENCHMARK_FILE = 'benchmark.json';
14+
export const COMPARISON_FILE = 'comparison.txt';
1415

1516
// MDN Constants
1617
export const MDN_COMPAT_URL =

src/__tests__/comparators.test.mjs

Lines changed: 41 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ const runComparator = async (name, base, head) => {
5454
return stdout;
5555
};
5656

57-
test('comparePerformance formats benchmark differences', async t => {
57+
test('comparePerformance summarizes benchmark differences', async t => {
5858
const { base, head } = await createDirectories(t);
5959

6060
await Promise.all([
@@ -71,16 +71,14 @@ test('comparePerformance formats benchmark differences', async t => {
7171

7272
assert.match(
7373
result,
74-
/Elapsed time \| 2\.00 s \| 3\.00 s \| \+1\.00 s \(\+50\.00%\)/
74+
/\*\*Generation time:\*\* 50\.0% slower \(2\.00 s 3\.00 s\)/
7575
);
7676
assert.match(
7777
result,
78-
/User CPU time \| 1\.00 s \| 750\.00 ms \| -250\.00 ms \(-25\.00%\)/
79-
);
80-
assert.match(
81-
result,
82-
/Peak resident memory \| 1\.00 MB \| 1\.50 MB \| \+512\.00 KB \(\+50\.00%\)/
78+
/\*\*Peak memory:\*\* 50\.0% higher \(1\.00 MB 1\.50 MB\)/
8379
);
80+
assert.match(result, /single CI run/);
81+
assert.doesNotMatch(result, /CPU time/);
8482
});
8583

8684
test('comparePerformance omits results when an artifact has no benchmark', async t => {
@@ -101,7 +99,7 @@ test('comparePerformance rejects invalid benchmark values', async t => {
10199

102100
await assert.rejects(
103101
comparePerformance(base, head),
104-
/Invalid peak resident memory benchmark value/
102+
/Invalid peak memory benchmark value/
105103
);
106104
});
107105

@@ -118,9 +116,14 @@ test('file-size comparator combines output and performance results', async t =>
118116
const result = await runComparator('file-size', base, head);
119117

120118
assert.equal(result.match(/## `test` Generator/g)?.length, 1);
121-
assert.match(result, /### Output size/);
119+
assert.match(result, /Output size:.*1 file changed · net \+11\.00 B/);
120+
assert.match(result, /<summary>File size details<\/summary>/);
121+
assert.match(
122+
result,
123+
/\| File \| Main \| PR \| Change \|\n\| --- \| ---: \| ---: \| ---: \|/
124+
);
122125
assert.match(result, /`result\.txt`/);
123-
assert.match(result, /### Performance/);
126+
assert.match(result, /Performance estimate/);
124127
assert.doesNotMatch(result, /benchmark\.json/);
125128
});
126129

@@ -137,7 +140,33 @@ test('object comparator treats benchmark data as metadata', async t => {
137140
const result = await runComparator('object-assertion', base, head);
138141

139142
assert.equal(result.match(/## `test` Generator/g)?.length, 1);
140-
assert.doesNotMatch(result, /### Output\n/);
141-
assert.match(result, /### Performance/);
143+
assert.doesNotMatch(result, /\*\*Output:/);
144+
assert.match(result, /Performance estimate/);
142145
assert.doesNotMatch(result, /benchmark\.json/);
143146
});
147+
148+
test('comparators report added and removed output files', async t => {
149+
const { base, head } = await createDirectories(t);
150+
const baseOutput = path.join(base, 'generator');
151+
const headOutput = path.join(head, 'generator');
152+
153+
await Promise.all([mkdir(baseOutput), mkdir(headOutput)]);
154+
await Promise.all([
155+
writeFile(path.join(baseOutput, 'removed.json'), '{"old":true}', 'utf8'),
156+
writeFile(path.join(headOutput, 'added.json'), '{"new":true}', 'utf8'),
157+
writeFile(path.join(head, 'comparison.txt'), '', 'utf8'),
158+
]);
159+
160+
const [sizes, objects] = await Promise.all([
161+
runComparator('file-size', base, head),
162+
runComparator('object-assertion', base, head),
163+
]);
164+
165+
assert.match(sizes, /2 files changed/);
166+
assert.match(sizes, /`generator\/added\.json` \| \| 12\.00 B/);
167+
assert.match(sizes, /`generator\/removed\.json` \| 12\.00 B \| /);
168+
assert.match(objects, /`generator\/added\.json` added/);
169+
assert.match(objects, /`generator\/removed\.json` removed/);
170+
assert.doesNotMatch(sizes, /comparison\.txt|4\.00 KB/);
171+
assert.doesNotMatch(objects, /comparison\.txt/);
172+
});

0 commit comments

Comments
 (0)