diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2d52e2b6fa1a9..89b2c388e2ac5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -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 diff --git a/Herebyfile.mjs b/Herebyfile.mjs index 8181655add479..0bdfa424cfe63 100644 --- a/Herebyfile.mjs +++ b/Herebyfile.mjs @@ -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."); - } - } } }); diff --git a/scripts/build/utils.mjs b/scripts/build/utils.mjs index be8e862d9adc7..2c8fe8c0c584b 100644 --- a/scripts/build/utils.mjs +++ b/scripts/build/utils.mjs @@ -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 */ diff --git a/scripts/checkPackageSize.mjs b/scripts/checkPackageSize.mjs new file mode 100644 index 0000000000000..9614ca195842a --- /dev/null +++ b/scripts/checkPackageSize.mjs @@ -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} */ +const fileCounts = new Map(); +const inBefore = -1; +const inAfter = 1; + +/** + * @param {Iterable} 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(); +}