sizebot: Combine stable and experimental results (#20753)

Because we have access to the artifacts in CI, we can read bundle sizes
directly from the filesystem, instead of the JSON files emitted by our
custom Rollup plugin.

This gives us some flexibility if we ever have artifacts that aren't
generated by Rollup, or if we rewrite our build script.

Personally, I also prefer to see the whole file path, instead of just
the name, because some of our names are repeated.

My immediate motivation, though, is because it gives us a way to merge
the separate "experimental" and "stable" size results. Instead
everything is reported in a single table and disambiguated by path.

I also added a section at the top that always displays the size impact
to certain critical bundles — right now, that's the React DOM production
bundles for each release channel. This section will also include any
size changes larger than 2%.

Below that is a section that is collapsed by default and includes all
size changes larger than 0.2%.
This commit is contained in:
Andrew Clark 2021-02-06 14:57:11 -06:00 committed by GitHub
parent 903384ab0c
commit bbb2ba8c8d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

View File

@ -7,6 +7,8 @@
'use strict'; 'use strict';
/* eslint-disable no-for-of-loops/no-for-of-loops */
// Hi, if this is your first time editing/reading a Dangerfile, here's a summary: // Hi, if this is your first time editing/reading a Dangerfile, here's a summary:
// It's a JS runtime which helps you provide continuous feedback inside GitHub. // It's a JS runtime which helps you provide continuous feedback inside GitHub.
// //
@ -26,169 +28,65 @@
// `DANGER_GITHUB_API_TOKEN=[ENV_ABOVE] yarn danger pr https://github.com/facebook/react/pull/11865 // `DANGER_GITHUB_API_TOKEN=[ENV_ABOVE] yarn danger pr https://github.com/facebook/react/pull/11865
const {markdown, danger, warn} = require('danger'); const {markdown, danger, warn} = require('danger');
const {promisify} = require('util');
const glob = promisify(require('glob'));
const gzipSize = require('gzip-size');
const {generateResultsArray} = require('./scripts/rollup/stats'); const {readFileSync, statSync} = require('fs');
const {readFileSync, readdirSync} = require('fs');
const path = require('path');
/** const BASE_DIR = 'base-build';
* Generates a Markdown table const HEAD_DIR = 'build2';
* @param {string[]} headers
* @param {string[][]} body
*/
function generateMDTable(headers, body) {
const tableHeaders = [
headers.join(' | '),
headers.map(() => ' --- ').join(' | '),
];
const tablebody = body.map(r => r.join(' | ')); const CRITICAL_THRESHOLD = 0.02;
return tableHeaders.join('\n') + '\n' + tablebody.join('\n'); const SIGNIFICANCE_THRESHOLD = 0.002;
const CRITICAL_ARTIFACT_PATHS = new Set([
// We always report changes to these bundles, even if the change is
// insiginificant or non-existent.
'oss-stable/react-dom/cjs/react-dom.production.min.js',
'oss-experimental/react-dom/cjs/react-dom.production.min.js',
'facebook-www/ReactDOM-prod.classic.js',
'facebook-www/ReactDOM-prod.modern.js',
'facebook-www/ReactDOMForked-prod.classic.js',
]);
const kilobyteFormatter = new Intl.NumberFormat('en', {
style: 'unit',
unit: 'kilobyte',
minimumFractionDigits: 2,
maximumFractionDigits: 2,
});
function kbs(bytes) {
return kilobyteFormatter.format(bytes / 1000);
} }
/** const percentFormatter = new Intl.NumberFormat('en', {
* Generates a user-readable string from a percentage change style: 'percent',
* @param {number} change signDisplay: 'exceptZero',
* @param {boolean} includeEmoji minimumFractionDigits: 2,
*/ maximumFractionDigits: 2,
function addPercent(change, includeEmoji) { });
if (!isFinite(change)) {
// When a new package is created function change(decimal) {
return 'n/a'; if (Number === Infinity) {
return 'New file';
} }
const formatted = (change * 100).toFixed(1); if (decimal === -1) {
if (/^-|^0(?:\.0+)$/.test(formatted)) { return 'Deleted';
return `${formatted}%`;
} else {
if (includeEmoji) {
return `:small_red_triangle:+${formatted}%`;
} else {
return `+${formatted}%`;
}
} }
if (decimal < 0.0001) {
return '=';
}
return percentFormatter.format(decimal);
} }
function setBoldness(row, isBold) { const header = `
if (isBold) { | Name | +/- | Base | Current | +/- gzip | Base gzip | Current gzip |
return row.map(element => `**${element}**`); | ---- | --- | ---- | ------- | -------- | --------- | ------------ |`;
} else {
return row;
}
}
function getBundleSizes(pathToSizesDir) { function row(result) {
const filenames = readdirSync(pathToSizesDir); // prettier-ignore
let bundleSizes = []; return `| ${result.path} | **${change(result.change)}** | ${kbs(result.baseSize)} | ${kbs(result.headSize)} | ${change(result.changeGzip)} | ${kbs(result.baseSizeGzip)} | ${kbs(result.headSizeGzip)}`;
for (let i = 0; i < filenames.length; i++) {
const filename = filenames[i];
if (filename.endsWith('.json')) {
const json = readFileSync(path.join(pathToSizesDir, filename));
bundleSizes.push(...JSON.parse(json).bundleSizes);
}
}
return {bundleSizes};
}
async function printResultsForChannel(baseResults, headResults) {
// Take the JSON of the build response and
// make an array comparing the results for printing
const results = generateResultsArray(headResults, baseResults);
const packagesToShow = results
.filter(
r =>
Math.abs(r.prevFileSizeAbsoluteChange) >= 300 || // bytes
Math.abs(r.prevGzipSizeAbsoluteChange) >= 100 // bytes
)
.map(r => r.packageName);
if (packagesToShow.length) {
let allTables = [];
// Highlight React and React DOM changes inline
// e.g. react: `react.production.min.js`: -3%, `react.development.js`: +4%
if (packagesToShow.includes('react')) {
const reactProd = results.find(
r => r.bundleType === 'UMD_PROD' && r.packageName === 'react'
);
if (
reactProd.prevFileSizeChange !== 0 ||
reactProd.prevGzipSizeChange !== 0
) {
const changeSize = addPercent(reactProd.prevFileSizeChange, true);
const changeGzip = addPercent(reactProd.prevGzipSizeChange, true);
markdown(`React: size: ${changeSize}, gzip: ${changeGzip}`);
}
}
if (packagesToShow.includes('react-dom')) {
const reactDOMProd = results.find(
r => r.bundleType === 'UMD_PROD' && r.packageName === 'react-dom'
);
if (
reactDOMProd.prevFileSizeChange !== 0 ||
reactDOMProd.prevGzipSizeChange !== 0
) {
const changeSize = addPercent(reactDOMProd.prevFileSizeChange, true);
const changeGzip = addPercent(reactDOMProd.prevGzipSizeChange, true);
markdown(`ReactDOM: size: ${changeSize}, gzip: ${changeGzip}`);
}
}
// Show a hidden summary table for all diffs
// eslint-disable-next-line no-var,no-for-of-loops/no-for-of-loops
for (var name of new Set(packagesToShow)) {
const thisBundleResults = results.filter(r => r.packageName === name);
const changedFiles = thisBundleResults.filter(
r => r.prevFileSizeChange !== 0 || r.prevGzipSizeChange !== 0
);
const mdHeaders = [
'File',
'Filesize Diff',
'Gzip Diff',
'Prev Size',
'Current Size',
'Prev Gzip',
'Current Gzip',
'ENV',
];
const mdRows = changedFiles.map(r => {
const isProd = r.bundleType.includes('PROD');
return setBoldness(
[
r.filename,
addPercent(r.prevFileSizeChange, isProd),
addPercent(r.prevGzipSizeChange, isProd),
r.prevSize,
r.prevFileSize,
r.prevGzip,
r.prevGzipSize,
r.bundleType,
],
isProd
);
});
allTables.push(`\n## ${name}`);
allTables.push(generateMDTable(mdHeaders, mdRows));
}
const summary = `
<details>
<summary>Details of bundled changes.</summary>
${allTables.join('\n')}
</details>
`;
return summary;
} else {
return 'No significant bundle size changes to report.';
}
} }
(async function() { (async function() {
@ -202,21 +100,10 @@ async function printResultsForChannel(baseResults, headResults) {
} }
let headSha; let headSha;
let headSizesStable;
let headSizesExperimental;
let baseSha; let baseSha;
let baseSizesStable;
let baseSizesExperimental;
try { try {
headSha = (readFileSync('./build2/COMMIT_SHA') + '').trim(); headSha = (readFileSync(HEAD_DIR + '/COMMIT_SHA') + '').trim();
headSizesStable = getBundleSizes('./build2/sizes-stable'); baseSha = (readFileSync(BASE_DIR + '/COMMIT_SHA') + '').trim();
headSizesExperimental = getBundleSizes('./build2/sizes-experimental');
baseSha = (readFileSync('./base-build/COMMIT_SHA') + '').trim();
baseSizesStable = getBundleSizes('./base-build/sizes-stable');
baseSizesExperimental = getBundleSizes('./base-build/sizes-experimental');
} catch { } catch {
warn( warn(
"Failed to read build artifacts. It's possible a build configuration " + "Failed to read build artifacts. It's possible a build configuration " +
@ -226,17 +113,135 @@ async function printResultsForChannel(baseResults, headResults) {
return; return;
} }
const resultsMap = new Map();
// Find all the head (current) artifacts paths.
const headArtifactPaths = await glob('**/*.js', {cwd: 'build2'});
for (const artifactPath of headArtifactPaths) {
try {
// This will throw if there's no matching base artifact
const baseSize = statSync(BASE_DIR + '/' + artifactPath).size;
const baseSizeGzip = gzipSize.fileSync(BASE_DIR + '/' + artifactPath);
const headSize = statSync(HEAD_DIR + '/' + artifactPath).size;
const headSizeGzip = gzipSize.fileSync(HEAD_DIR + '/' + artifactPath);
resultsMap.set(artifactPath, {
path: artifactPath,
headSize,
headSizeGzip,
baseSize,
baseSizeGzip,
change: (headSize - baseSize) / baseSize,
changeGzip: (headSizeGzip - baseSizeGzip) / baseSizeGzip,
});
} catch {
// There's no matching base artifact. This is a new file.
const baseSize = 0;
const baseSizeGzip = 0;
const headSize = statSync(HEAD_DIR + '/' + artifactPath).size;
const headSizeGzip = gzipSize.fileSync(HEAD_DIR + '/' + artifactPath);
resultsMap.set(artifactPath, {
path: artifactPath,
headSize,
headSizeGzip,
baseSize,
baseSizeGzip,
change: Infinity,
changeGzip: Infinity,
});
}
}
// Check for base artifacts that were deleted in the head.
const baseArtifactPaths = await glob('**/*.js', {cwd: 'base-build'});
for (const artifactPath of baseArtifactPaths) {
if (!resultsMap.has(artifactPath)) {
const baseSize = statSync(BASE_DIR + '/' + artifactPath).size;
const baseSizeGzip = gzipSize.fileSync(BASE_DIR + '/' + artifactPath);
const headSize = 0;
const headSizeGzip = 0;
resultsMap.set(artifactPath, {
path: artifactPath,
headSize,
headSizeGzip,
baseSize,
baseSizeGzip,
change: -1,
changeGzip: -1,
});
}
}
const results = Array.from(resultsMap.values());
results.sort((a, b) => b.change - a.change);
let criticalResults = [];
for (const artifactPath of CRITICAL_ARTIFACT_PATHS) {
const result = resultsMap.get(artifactPath);
if (result === undefined) {
throw new Error(
'Missing expected bundle. If this was an intentional change to the ' +
'build configuration, update Dangerfile.js accordingly: ' +
artifactPath
);
}
criticalResults.push(row(result));
}
let significantResults = [];
for (const result of results) {
// If result exceeds critical threshold, add to top section.
if (
(result.change > CRITICAL_THRESHOLD ||
0 - result.change > CRITICAL_THRESHOLD ||
// New file
result.change === Infinity ||
// Deleted file
result.change === -1) &&
// Skip critical artifacts. We added those earlier, in a fixed order.
!CRITICAL_ARTIFACT_PATHS.has(result.path)
) {
criticalResults.push(row(result));
}
// Do the same for results that exceed the significant threshold. These
// will go into the bottom, collapsed section. Intentionally including
// critical artifacts in this section, too.
if (
result.change > SIGNIFICANCE_THRESHOLD ||
0 - result.change > SIGNIFICANCE_THRESHOLD ||
result.change === Infinity ||
result.change === -1
) {
significantResults.push(row(result));
}
}
markdown(` markdown(`
## Size changes Comparing: ${baseSha}...${headSha}
<p>Comparing: ${baseSha}...${headSha}</p> ## Critical size changes
### Stable channel Includes critical production bundles, as well as any change greater than ${CRITICAL_THRESHOLD *
100}%:
${await printResultsForChannel(baseSizesStable, headSizesStable)} ${header}
${criticalResults.join('\n')}
### Experimental channel ## Significant size changes
${await printResultsForChannel(baseSizesExperimental, headSizesExperimental)} Includes any change greater than ${SIGNIFICANCE_THRESHOLD * 100}%:
${
significantResults.length > 0
? `
<details>
<summary>Expand to show</summary>
${header}
${significantResults.join('\n')}
</details>
`
: '(No significant changes)'
}
`); `);
})(); })();