浏览代码

CI: Report tree-shaking size in PR (#25615)

* Add report-size.yml action

* Format

* Report normal bundle size as well

* Wording

* Remove pretty-bytes dependency

* Add clarification about tree-shaking in message
Marco Fugaro 2 年之前
父节点
当前提交
3987bf5701

+ 50 - 0
.github/workflows/read-size.yml

@@ -0,0 +1,50 @@
+name: Read size
+
+on:
+  pull_request:
+    paths:
+      - 'src/**'
+      - 'package.json'
+
+# This workflow runs in a read-only environment. We can safely checkout
+# the PR code here.
+# Reference:
+# https://securitylab.github.com/research/github-actions-preventing-pwn-requests/
+permissions:
+  contents: read
+
+jobs:
+  read-size:
+    name: Tree-shaking
+    runs-on: ubuntu-latest
+    steps:
+      - name: Git checkout
+        uses: actions/checkout@v3
+      - name: Install Node
+        uses: actions/setup-node@v3
+        with:
+          node-version: 18
+          cache: 'npm'
+      - name: Install dependencies
+        run: npm ci
+      - name: Build
+        run: npm run build-module
+      - name: === Test tree-shaking ===
+        run: npm run test-treeshake
+      - name: Read bundle sizes
+        id: read-size
+        run: |
+          FILESIZE=$(stat --format=%s test/treeshake/three.module.min.js)
+          gzip -k test/treeshake/three.module.min.js
+          FILESIZE_GZIP=$(stat --format=%s test/treeshake/three.module.min.js.gz)
+          TREESHAKEN=$(stat --format=%s test/treeshake/index.bundle.min.js)
+          gzip -k test/treeshake/index.bundle.min.js
+          TREESHAKEN_GZIP=$(stat --format=%s test/treeshake/index.bundle.min.js.gz)
+
+          # write the output in a json file to upload it as artifact
+          node -pe "JSON.stringify({ filesize: $FILESIZE, gzip: $FILESIZE_GZIP, treeshaken: $TREESHAKEN, treeshakenGzip: $TREESHAKEN_GZIP })" > sizes.json
+      - name: Upload artifact
+        uses: actions/upload-artifact@v3
+        with:
+          name: sizes
+          path: sizes.json

+ 125 - 0
.github/workflows/report-size.yml

@@ -0,0 +1,125 @@
+name: Report size
+
+on:
+  workflow_run:
+    workflows: ["Read size"]
+    types:
+      - completed
+
+# This workflow needs to be run with "pull-requests: write" permissions to
+# be able to comment on the pull request. We can't checkout the PR code
+# in this workflow.
+# Reference:
+# https://securitylab.github.com/research/github-actions-preventing-pwn-requests/
+permissions:
+  pull-requests: write
+
+jobs:
+  report-size:
+    name: Comment on PR
+    runs-on: ubuntu-latest
+    if: github.event.workflow_run.event == 'pull_request' &&
+      github.event.workflow_run.conclusion == 'success'
+    steps:
+      # Using actions/download-artifact doesn't work here
+      # https://github.com/actions/download-artifact/issues/60
+      - name: Download artifact
+        uses: actions/github-script@v6
+        id: download-artifact
+        with:
+          result-encoding: string
+          script: |
+            const fs = require('fs/promises');
+
+            const artifacts = await github.rest.actions.listWorkflowRunArtifacts({
+               owner: context.repo.owner,
+               repo: context.repo.repo,
+               run_id: context.payload.workflow_run.id,
+            });
+            const matchArtifact = artifacts.data.artifacts.find((artifact) => artifact.name === 'sizes');
+            const download = await github.rest.actions.downloadArtifact({
+               owner: context.repo.owner,
+               repo: context.repo.repo,
+               artifact_id: matchArtifact.id,
+               archive_format: 'zip',
+            });
+
+            await fs.writeFile('sizes.zip', Buffer.from(download.data));
+            await exec.exec('unzip sizes.zip');
+            const json = await fs.readFile('sizes.json', 'utf8');
+            return json;
+
+      # This runs on the base branch of the PR, meaning "dev"
+      - name: Git checkout
+        uses: actions/checkout@v3
+      - name: Install Node
+        uses: actions/setup-node@v3
+        with:
+          node-version: 18
+          cache: 'npm'
+      - name: Install dependencies
+        run: npm ci
+      - name: Build
+        run: npm run build-module
+      - name: === Test tree-shaking ===
+        run: npm run test-treeshake
+      - name: Read sizes
+        id: read-size
+        run: |
+          FILESIZE_BASE=$(stat --format=%s test/treeshake/three.module.min.js)
+          TREESHAKEN_BASE=$(stat --format=%s test/treeshake/index.bundle.min.js)
+          echo "FILESIZE_BASE=$FILESIZE_BASE" >> $GITHUB_OUTPUT
+          echo "TREESHAKEN_BASE=$TREESHAKEN_BASE" >> $GITHUB_OUTPUT
+
+      - name: Format sizes
+        id: format
+        # It's important these are passed as env variables.
+        # https://securitylab.github.com/research/github-actions-untrusted-input/
+        env:
+          FILESIZE: ${{ fromJSON(steps.download-artifact.outputs.result).filesize }}
+          FILESIZE_GZIP: ${{ fromJSON(steps.download-artifact.outputs.result).gzip }}
+          FILESIZE_BASE: ${{ steps.read-size.outputs.FILESIZE_BASE }}
+          TREESHAKEN: ${{ fromJSON(steps.download-artifact.outputs.result).treeshaken }}
+          TREESHAKEN_GZIP: ${{ fromJSON(steps.download-artifact.outputs.result).treeshakenGzip }}
+          TREESHAKEN_BASE: ${{ steps.read-size.outputs.TREESHAKEN_BASE }}
+        run: |
+          FILESIZE_FORM=$(node ./test/treeshake/utils/format-size.js "$FILESIZE")
+          FILESIZE_GZIP_FORM=$(node ./test/treeshake/utils/format-size.js "$FILESIZE_GZIP")
+          FILESIZE_DIFF=$(node ./test/treeshake/utils/format-diff.js "$FILESIZE" "$FILESIZE_BASE")
+          TREESHAKEN_FORM=$(node ./test/treeshake/utils/format-size.js "$TREESHAKEN")
+          TREESHAKEN_GZIP_FORM=$(node ./test/treeshake/utils/format-size.js "$TREESHAKEN_GZIP")
+          TREESHAKEN_DIFF=$(node ./test/treeshake/utils/format-diff.js "$TREESHAKEN" "$TREESHAKEN_BASE")
+
+          echo "FILESIZE=$FILESIZE_FORM" >> $GITHUB_OUTPUT
+          echo "FILESIZE_GZIP=$FILESIZE_GZIP_FORM" >> $GITHUB_OUTPUT
+          echo "FILESIZE_DIFF=$FILESIZE_DIFF" >> $GITHUB_OUTPUT
+          echo "TREESHAKEN=$TREESHAKEN_FORM" >> $GITHUB_OUTPUT
+          echo "TREESHAKEN_GZIP=$TREESHAKEN_GZIP_FORM" >> $GITHUB_OUTPUT
+          echo "TREESHAKEN_DIFF=$TREESHAKEN_DIFF" >> $GITHUB_OUTPUT
+
+      - name: Find existing comment
+        uses: peter-evans/find-comment@v2
+        id: find-comment
+        with:
+          issue-number: ${{ github.event.workflow_run.pull_requests[0].number }}
+          comment-author: 'github-actions[bot]'
+          body-includes: Bundle size
+      - name: Comment on PR
+        uses: peter-evans/create-or-update-comment@v2
+        with:
+          issue-number: ${{ github.event.workflow_run.pull_requests[0].number }}
+          comment-id: ${{ steps.find-comment.outputs.comment-id }}
+          edit-mode: replace
+          body: |
+            ### 📦 Bundle size
+            | Filesize | Gzipped | Diff from `${{ github.ref_name }}` |
+            |----------|---------|------|
+            | ${{ steps.format.outputs.FILESIZE }} | ${{ steps.format.outputs.FILESIZE_GZIP }} | ${{ steps.format.outputs.FILESIZE_DIFF }} |
+
+            ### 🌳 Bundle size after tree-shaking
+
+            _Includes a renderer, camera, and empty scene._
+
+            | Filesize | Gzipped | Diff from `${{ github.ref_name }}` |
+            |----------|---------|------|
+            | ${{ steps.format.outputs.TREESHAKEN }} | ${{ steps.format.outputs.TREESHAKEN_GZIP }} | ${{ steps.format.outputs.TREESHAKEN_DIFF }} |

+ 1 - 0
.gitignore

@@ -11,6 +11,7 @@ test/unit/build
 test/treeshake/index.bundle.js
 test/treeshake/index.bundle.min.js
 test/treeshake/index-src.bundle.min.js
+test/treeshake/three.module.min.js
 test/treeshake/stats.html
 test/e2e/chromium
 test/e2e/output-screenshots

+ 13 - 0
test/rollup.treeshake.config.js

@@ -70,4 +70,17 @@ export default [
 			}
 		]
 	},
+	// esm bundle size minified, used in read-size.yml
+	{
+		input: 'build/three.module.js',
+		plugins: [
+			terser(),
+		],
+		output: [
+			{
+				format: 'esm',
+				file: 'test/treeshake/three.module.min.js'
+			}
+		]
+	},
 ];

+ 10 - 0
test/treeshake/utils/format-diff.js

@@ -0,0 +1,10 @@
+// used in report-size.yml
+
+const filesize = Number( process.argv[ 2 ] );
+const filesizeBase = Number( process.argv[ 3 ] );
+
+const diff = ( filesize - filesizeBase ) * 100 / filesizeBase;
+const diffString = diff.toFixed( 2 ).slice( - 1 ) === '0' ? diff.toFixed( 1 ) : diff.toFixed( 2 );
+const formatted = `${diff >= 0 ? '+' : ''}${diffString}%`;
+
+console.log( formatted );

+ 20 - 0
test/treeshake/utils/format-size.js

@@ -0,0 +1,20 @@
+// used in report-size.yml
+
+export function formatBytes( bytes, decimals = 1 ) {
+
+	if ( bytes === 0 ) return '0 B';
+
+	const k = 1000;
+	const dm = decimals < 0 ? 0 : decimals;
+	const sizes = [ 'B', 'kB', 'MB', 'GB' ];
+
+	const i = Math.floor( Math.log( bytes ) / Math.log( k ) );
+
+	return parseFloat( ( bytes / Math.pow( k, i ) ).toFixed( dm ) ) + ' ' + sizes[ i ];
+
+}
+
+const n = Number( process.argv[ 2 ] );
+const formatted = formatBytes( n );
+
+console.log( formatted );