main
js 114 lines 3.85 KB
Raw
1 /**
2 * Copyright (c) Meta Platforms, Inc. and affiliates.
3 *
4 * This source code is licensed under the MIT license found in the
5 * LICENSE file in the root directory of this source tree.
6 */
7
8 'use strict';
9
10 /* eslint-disable no-for-of-loops/no-for-of-loops */
11
12 // Measures every build artifact in `build` against the base revision's artifacts
13 // in `base-build` and writes the raw numbers to `sizebot-results.json`.
14 //
15 // This runs in the `pull_request` half of CI, where the GitHub token is
16 // read-only, so it never talks to the API and never renders anything
17 // user-facing. `render-comment.js` turns this JSON into the pull request comment
18 // from a trusted checkout. See `.github/workflows/runtime_sizebot_comment.yml`
19 // for why the two halves are separate.
20
21 const {promisify} = require('util');
22 const glob = promisify(require('glob'));
23 const gzipSize = require('gzip-size');
24 const {readFileSync, statSync, writeFileSync} = require('fs');
25
26 // Bump on any incompatible change to the JSON below: added required fields,
27 // renamed or removed fields, or a changed meaning for an existing one. Purely
28 // additive optional fields do not need a bump. The reader lives on the default
29 // branch while the writer lives on the pull request branch, so the two can
30 // legitimately disagree and `render-comment.js` needs to be able to tell.
31 const RESULTS_VERSION = 1;
32
33 const RESULTS_PATH = 'sizebot-results.json';
34 const BASE_DIR = 'base-build';
35 const HEAD_DIR = 'build';
36
37 function measure(dir, artifactPath) {
38 const file = dir + '/' + artifactPath;
39 return {
40 size: statSync(file).size,
41 sizeGzip: gzipSize.fileSync(file),
42 };
43 }
44
45 function writeResults(results) {
46 writeFileSync(RESULTS_PATH, JSON.stringify(results, null, 2) + '\n');
47 }
48
49 (async function () {
50 let headSha;
51 let baseSha;
52 try {
53 headSha = String(readFileSync(HEAD_DIR + '/COMMIT_SHA')).trim();
54 baseSha = String(readFileSync(BASE_DIR + '/COMMIT_SHA')).trim();
55 } catch {
56 // Let the renderer explain this one. It is expected to happen whenever the
57 // build configuration changes upstream, which is not a CI failure.
58 writeResults({
59 version: RESULTS_VERSION,
60 status: 'base-artifacts-unavailable',
61 });
62 return;
63 }
64
65 // A missing size is recorded as null rather than 0, so the renderer can tell
66 // "this artifact does not exist on that side" apart from "this artifact is
67 // empty". It derives the new-file and deleted-file cases from those nulls.
68 const artifactsByPath = new Map();
69
70 const headArtifactPaths = await glob('**/*.js', {cwd: HEAD_DIR});
71 for (const artifactPath of headArtifactPaths) {
72 let base;
73 try {
74 base = measure(BASE_DIR, artifactPath);
75 } catch {
76 // There's no matching base artifact. This is a new file.
77 base = null;
78 }
79 const head = measure(HEAD_DIR, artifactPath);
80 artifactsByPath.set(artifactPath, {
81 path: artifactPath,
82 baseSize: base === null ? null : base.size,
83 baseSizeGzip: base === null ? null : base.sizeGzip,
84 headSize: head.size,
85 headSizeGzip: head.sizeGzip,
86 });
87 }
88
89 // Check for base artifacts that were deleted in the head.
90 const baseArtifactPaths = await glob('**/*.js', {cwd: BASE_DIR});
91 for (const artifactPath of baseArtifactPaths) {
92 if (!artifactsByPath.has(artifactPath)) {
93 const base = measure(BASE_DIR, artifactPath);
94 artifactsByPath.set(artifactPath, {
95 path: artifactPath,
96 baseSize: base.size,
97 baseSizeGzip: base.sizeGzip,
98 headSize: null,
99 headSizeGzip: null,
100 });
101 }
102 }
103
104 // Every artifact is reported, with no threshold filtering. The thresholds and
105 // the critical bundle list belong to the renderer, so that a pull request
106 // cannot quietly widen them to hide a regression.
107 writeResults({
108 version: RESULTS_VERSION,
109 status: 'ok',
110 baseSha,
111 headSha,
112 artifacts: Array.from(artifactsByPath.values()),
113 });
114 })();