diff --git a/.github/workflows/read-size.yml b/.github/workflows/read-size.yml new file mode 100644 index 00000000000000..9d8a8cc10778d5 --- /dev/null +++ b/.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 diff --git a/.github/workflows/report-size.yml b/.github/workflows/report-size.yml new file mode 100644 index 00000000000000..a563453bc0bf51 --- /dev/null +++ b/.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 }} | diff --git a/.gitignore b/.gitignore index 50df59a8212b6c..f3a967d1af95e1 100644 --- a/.gitignore +++ b/.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 diff --git a/test/rollup.treeshake.config.js b/test/rollup.treeshake.config.js index 6adcb33eae0ed2..b68165fdba1e8e 100644 --- a/test/rollup.treeshake.config.js +++ b/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' + } + ] + }, ]; diff --git a/test/treeshake/utils/format-diff.js b/test/treeshake/utils/format-diff.js new file mode 100644 index 00000000000000..0864f3e5ce658f --- /dev/null +++ b/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 ); diff --git a/test/treeshake/utils/format-size.js b/test/treeshake/utils/format-size.js new file mode 100644 index 00000000000000..1ae96b73044ed2 --- /dev/null +++ b/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 );