Add custom GHA report for package size (#53241)

This commit is contained in:
Jake Bailey 2023-03-14 09:23:51 -07:00 committed by GitHub
parent 9dfd5ea221
commit edc343d733
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 241 additions and 55 deletions

View File

@ -89,38 +89,15 @@ jobs:
smoke:
runs-on: ubuntu-latest
defaults:
run:
working-directory: ./pr
steps:
- uses: actions/checkout@v3
with:
path: pr
- uses: actions/checkout@v3
with:
path: base
ref: ${{ github.base_ref }}
if: github.event_name == 'pull_request'
- uses: actions/setup-node@v3
with:
node-version: "*"
check-latest: true
# Pre-build the base branch so we can check lib folder size changes.
# Note that github.sha points to a merge commit, meaning we're testing
# the base branch versus the base branch with the PR applied.
- name: Build base LKG
if: github.event_name == 'pull_request'
run: |
npm ci
npx hereby lkg
rm -rf $GITHUB_WORKSPACE/pr/lib
mv ./lib $GITHUB_WORKSPACE/pr/
working-directory: ./base
- run: npm ci
- run: npx hereby lkg
@ -177,6 +154,41 @@ jobs:
node ./smoke.js typescript
node ./smoke.js typescript/lib/tsserverlibrary
package-size:
runs-on: ubuntu-latest
if: github.event_name == 'pull_request'
steps:
- uses: actions/checkout@v3
with:
path: pr
- uses: actions/checkout@v3
with:
path: base
ref: ${{ github.base_ref }}
- uses: actions/setup-node@v3
with:
node-version: "*"
check-latest: true
- run: npm ci
working-directory: ./pr
- run: npm ci
working-directory: ./base
- run: npx hereby lkg
working-directory: ./pr
- run: npx hereby lkg
working-directory: ./base
- run: |
echo "See $GITHUB_SERVER_URL/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID for more info."
node ./pr/scripts/checkPackageSize.mjs ./base ./pr >> $GITHUB_STEP_SUMMARY
misc:
runs-on: ubuntu-latest

View File

@ -15,7 +15,7 @@ import { localizationDirectories } from "./scripts/build/localization.mjs";
import cmdLineOptions from "./scripts/build/options.mjs";
import { buildProject, cleanProject, watchProject } from "./scripts/build/projects.mjs";
import { localBaseline, localRwcBaseline, refBaseline, refRwcBaseline, runConsoleTests } from "./scripts/build/tests.mjs";
import { Debouncer, Deferred, exec, getDiffTool, getDirSize, memoize, needsUpdate, readJson } from "./scripts/build/utils.mjs";
import { Debouncer, Deferred, exec, getDiffTool, memoize, needsUpdate, readJson } from "./scripts/build/utils.mjs";
const glob = util.promisify(_glob);
@ -833,19 +833,7 @@ export const produceLKG = task({
throw new Error("Cannot replace the LKG unless all built targets are present in directory 'built/local/'. The following files are missing:\n" + missingFiles.join("\n"));
}
/** @type {number | undefined} */
let sizeBefore;
if (fs.existsSync("lib")) {
sizeBefore = getDirSize("lib");
}
await exec(process.execPath, ["scripts/produceLKG.mjs"]);
if (sizeBefore !== undefined) {
const sizeAfter = getDirSize("lib");
if (sizeAfter > (sizeBefore * 1.10)) {
throw new Error("The lib folder increased by 10% or more. This likely indicates a bug.");
}
}
}
});

View File

@ -4,7 +4,6 @@ import chalk from "chalk";
import { spawn } from "child_process";
import fs from "fs";
import JSONC from "jsonc-parser";
import path from "path";
import which from "which";
/**
@ -140,24 +139,6 @@ export function getDiffTool() {
return program;
}
/**
* Find the size of a directory recursively.
* Symbolic links can cause a loop.
* @param {string} root
* @returns {number} bytes
*/
export function getDirSize(root) {
const stats = fs.lstatSync(root);
if (!stats.isDirectory()) {
return stats.size;
}
return fs.readdirSync(root)
.map(file => getDirSize(path.join(root, file)))
.reduce((acc, num) => acc + num, 0);
}
/**
* @template T
*/

View File

@ -0,0 +1,205 @@
import assert from "assert";
import cp from "child_process";
const baseRepo = process.argv[2];
const headRepo = process.argv[3];
/** @type {Array<{ size: number, unpackedSize: number; files: Array<{ path: string; size: number; }>; }>} */
const [before, after] = JSON.parse(cp.execFileSync("npm", ["pack", "--dry-run", "--json", baseRepo, headRepo], { encoding: "utf8" }));
/** @param {{ path: string; size: number; }[]} files */
function filesToMap(files) {
return new Map(files.map(f => [f.path, f.size]));
}
const beforeFileToSize = filesToMap(before.files);
const afterFileToSize = filesToMap(after.files);
/**
* @param {number} before
* @param {number} after
*/
function failIfTooBig(before, after) {
if (after > (before * 1.1)) {
process.exitCode = 1;
}
}
/**
* @param {number} value
*/
function sign(value) {
return value > 0 ? "+" : "-";
}
const units = ["B", "KiB", "MiB", "GiB"];
/**
* @param {number} size
*/
function prettyPrintSize(size) {
assert(size >= 0);
let i = 0;
while (size > 1024) {
i++;
size /= 1024;
}
return `${size.toFixed(2)} ${units[i]}`;
}
/**
* @param {number} before
* @param {number} after
*/
function prettyPrintSizeDiff(before, after) {
const diff = after - before;
return sign(diff) + prettyPrintSize(Math.abs(diff));
}
/**
* @param {number} before
* @param {number} after
*/
function prettyPercentDiff(before, after) {
const percent = 100 * (after - before) / before;
return `${sign(percent)}${Math.abs(percent).toFixed(2)}%`;
}
/**
* @param {string[]} header
* @param {string[][]} data
*/
function logTable(header, data) {
/** @type {string[]} */
const lines = [];
/**
* @param {string[]} row
*/
function addRow(row) {
lines.push("| " + row.join(" | ") + " |");
}
addRow(header);
addRow(new Array(header.length).fill("-"));
for (const row of data) {
addRow(row);
}
console.log(lines.join("\n"));
}
console.log(`# Package size report`);
console.log();
console.log(`## Overall package size`);
console.log();
if (before.size === after.size && before.unpackedSize === after.unpackedSize) {
console.log("No change.");
}
else {
logTable(
["", "Before", "After", "Diff", "Diff (percent)"],
[
[
"Packed",
prettyPrintSize(before.size),
prettyPrintSize(after.size),
prettyPrintSizeDiff(before.size, after.size),
prettyPercentDiff(before.size, after.size),
],
[
"Unpacked",
prettyPrintSize(before.unpackedSize),
prettyPrintSize(after.unpackedSize),
prettyPrintSizeDiff(before.unpackedSize, after.unpackedSize),
prettyPercentDiff(before.unpackedSize, after.unpackedSize),
],
]
);
}
failIfTooBig(before.size, after.size);
failIfTooBig(before.unpackedSize, after.unpackedSize);
console.log();
/** @type {Map<string, number>} */
const fileCounts = new Map();
const inBefore = -1;
const inAfter = 1;
/**
* @param {Iterable<string>} paths
* @param {-1 | 1} marker
*/
function addFiles(paths, marker) {
for (const p of paths) {
fileCounts.set(p, (fileCounts.get(p) ?? 0) + marker);
}
}
addFiles(beforeFileToSize.keys(), inBefore);
addFiles(afterFileToSize.keys(), inAfter);
const allEntries = [...fileCounts.entries()];
const commonFiles = allEntries.filter(([, count]) => count === 0).map(([path]) => path);
const beforeOnly = allEntries.filter(([, count]) => count === inBefore).map(([path]) => path);
const afterOnly = allEntries.filter(([, count]) => count === inAfter).map(([path]) => path);
const commonData = commonFiles.map(path => {
const beforeSize = beforeFileToSize.get(path) ?? 0;
const afterSize = afterFileToSize.get(path) ?? 0;
return { path, beforeSize, afterSize };
})
.filter(({ beforeSize, afterSize }) => beforeSize !== afterSize)
.map(({ path, beforeSize, afterSize }) => {
return [
"`" + path + "`",
prettyPrintSize(beforeSize),
prettyPrintSize(afterSize),
prettyPrintSizeDiff(beforeSize, afterSize),
prettyPercentDiff(beforeSize, afterSize),
];
});
if (commonData.length > 0) {
console.log(`## Files`);
console.log();
logTable(["", "Before", "After", "Diff", "Diff (percent)"], commonData);
console.log();
}
if (afterOnly.length > 0) {
console.log(`## New files`);
console.log();
logTable(
["", "Size"],
afterOnly.map(path => {
const afterSize = afterFileToSize.get(path) ?? 0;
return { path, afterSize };
})
.map(({ path, afterSize }) => {
return ["`" + path + "`", prettyPrintSize(afterSize)];
}),
);
console.log();
}
if (beforeOnly.length > 0) {
console.log(`## Deleted files`);
console.log();
logTable(
["", "Size"],
beforeOnly.map(path => {
const afterSize = afterFileToSize.get(path) ?? 0;
return { path, afterSize };
})
.map(({ path, afterSize }) => {
return ["`" + path + "`", prettyPrintSize(afterSize)];
}),
);
console.log();
}