diff --git a/.github/workflows/performance.yml b/.github/workflows/performance.yml index e3e3b694..57c521b5 100644 --- a/.github/workflows/performance.yml +++ b/.github/workflows/performance.yml @@ -3,26 +3,51 @@ name: performance on: [pull_request] jobs: - runtime-size: - name: runtime-size + size: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 - - uses: necolas/compressed-size-action@master - with: - name: 'runtime library' - build-script: 'build -w @stylexjs/stylex' - pattern: './packages/stylex/lib/{stylex.js,StyleXSheet.js}' - repo-token: '${{ secrets.GITHUB_TOKEN }}' - - bundle-size: - name: bundle-size - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - uses: necolas/compressed-size-action@master - with: - name: 'e2e bundles' - build-script: 'build' - pattern: './apps/rollup-example/.build/{stylex.css,bundle.js}' - repo-token: '${{ secrets.GITHUB_TOKEN }}' + - uses: actions/checkout@v4 + with: + fetch-depth: 50 + - uses: actions/setup-node@v4 + with: + node-version: '20.x' + - name: 'Setup temporary files' + run: | + echo "BASE_JSON=$(mktemp)" >> $GITHUB_ENV + echo "PATCH_JSON=$(mktemp)" >> $GITHUB_ENV + - name: 'Benchmark base' + run: | + git checkout -f ${{ github.event.pull_request.base.sha }} + npm install --loglevel error + if npm run size -w @stylexjs/scripts -- -o ${{ env.BASE_JSON }}; then + echo "Ran successfully on base branch" + else + echo "{}" > ${{ env.BASE_JSON }} # Empty JSON as default + echo "Benchmark script not found on base branch, using default values" + fi + - name: 'Benchmark patch' + run: | + git checkout -f ${{ github.event.pull_request.head.sha }} + npm install --loglevel error + npm run size -w @stylexjs/scripts -- -o ${{ env.PATCH_JSON }} + echo "Ran successfully on patch branch" + - name: 'Collect results' + id: collect + run: | + echo "table<> $GITHUB_OUTPUT + npm run size:compare -w @stylexjs/scripts -- ${{ env.BASE_JSON }} ${{ env.PATCH_JSON }} >> markdown + cat markdown >> $GITHUB_OUTPUT + echo "EOF" >> $GITHUB_OUTPUT + - name: 'Post comment' + uses: edumserrano/find-create-or-update-comment@v3 + with: + issue-number: ${{ github.event.pull_request.number }} + body-includes: '' + comment-author: 'github-actions[bot]' + body: | + + ### workflow: benchmarks/size + Comparison of minified (terser) and compressed (brotli) size results, measured in bytes. Smaller is better. + ${{ steps.collect.outputs.table }} + edit-mode: replace diff --git a/package-lock.json b/package-lock.json index 7cda55e6..75fa1004 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7,7 +7,6 @@ "": { "name": "stylex-monorepo", "version": "0.7.5", - "hasInstallScript": true, "license": "MIT", "workspaces": [ "packages/shared", @@ -229,18 +228,6 @@ "node": ">=4" } }, - "apps/docs/node_modules/clean-css": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-5.3.2.tgz", - "integrity": "sha512-JVJbM+f3d3Q704rF4bqQ5UUyTtuJ0JRKNbTKVEeujCCBoMdkEi+V+e8oktO9qGQNSvHrFTM6JZRXrUvGR1czww==", - "dev": true, - "dependencies": { - "source-map": "~0.6.0" - }, - "engines": { - "node": ">= 10.0" - } - }, "apps/docs/node_modules/debug": { "version": "3.2.7", "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", @@ -10054,6 +10041,22 @@ "node": ">=8" } }, + "node_modules/brotli-size": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/brotli-size/-/brotli-size-4.0.0.tgz", + "integrity": "sha512-uA9fOtlTRC0iqKfzff1W34DXUA3GyVqbUaeo3Rw3d4gd1eavKVCETXrn3NzO74W+UVkG3UHu8WxUi+XvKI/huA==", + "dependencies": { + "duplexer": "0.1.1" + }, + "engines": { + "node": ">= 10.16.0" + } + }, + "node_modules/brotli-size/node_modules/duplexer": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.1.tgz", + "integrity": "sha512-sxNZ+ljy+RA1maXoUReeqBBpBC6RLKmg5ewzV+x+mSETmWNoKdZN6vcQjpFROemza23hGFskJtFNoUWUaQ+R4Q==" + }, "node_modules/browserslist": { "version": "4.23.0", "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.0.tgz", @@ -10398,9 +10401,9 @@ "dev": true }, "node_modules/clean-css": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-5.3.1.tgz", - "integrity": "sha512-lCr8OHhiWCTw4v8POJovCoh4T7I9U11yVsPjMWWnnMmp9ZowCxyad1Pathle/9HjaDp+fdQKjO9fQydE6RHTZg==", + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-5.3.3.tgz", + "integrity": "sha512-D5J+kHaVb/wKSFcyyV75uCn8fiY4sV38XJoe4CUyGQ+mOU/fMVYUdH1hJC+CJQ5uY3EnW27SbJYS4X8BiLrAFg==", "dependencies": { "source-map": "~0.6.0" }, @@ -29193,9 +29196,9 @@ } }, "node_modules/terser": { - "version": "5.19.2", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.19.2.tgz", - "integrity": "sha512-qC5+dmecKJA4cpYxRa5aVkKehYsQKc+AHeKl0Oe62aYjBL8ZA33tTljktDHJSaxxMnbI5ZYw+o/S2DxxLu8OfA==", + "version": "5.31.6", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.31.6.tgz", + "integrity": "sha512-PQ4DAriWzKj+qgehQ7LK5bQqCFNMmlhjR2PFFLuqGCpuCAauxemVBWwWOxo3UIwWQx8+Pr61Df++r76wDmkQBg==", "dependencies": { "@jridgewell/source-map": "^0.3.3", "acorn": "^8.8.2", @@ -31538,7 +31541,6 @@ "cpu": [ "arm64" ], - "dev": true, "optional": true, "os": [ "darwin" @@ -31554,7 +31556,6 @@ "cpu": [ "x64" ], - "dev": true, "optional": true, "os": [ "darwin" @@ -31570,7 +31571,6 @@ "cpu": [ "arm64" ], - "dev": true, "optional": true, "os": [ "linux" @@ -31586,7 +31586,6 @@ "cpu": [ "arm64" ], - "dev": true, "optional": true, "os": [ "linux" @@ -31602,7 +31601,6 @@ "cpu": [ "x64" ], - "dev": true, "optional": true, "os": [ "linux" @@ -31618,7 +31616,6 @@ "cpu": [ "x64" ], - "dev": true, "optional": true, "os": [ "linux" @@ -31634,7 +31631,6 @@ "cpu": [ "arm64" ], - "dev": true, "optional": true, "os": [ "win32" @@ -31650,7 +31646,6 @@ "cpu": [ "ia32" ], - "dev": true, "optional": true, "os": [ "win32" @@ -31666,7 +31661,6 @@ "cpu": [ "x64" ], - "dev": true, "optional": true, "os": [ "win32" @@ -31868,7 +31862,10 @@ "version": "0.7.5", "license": "MIT", "dependencies": { + "brotli-size": "^4.0.0", + "clean-css": "^5.3.3", "flow-api-translator": "0.19.2", + "terser": "^5.31.0", "yargs": "17.7.2" }, "bin": { @@ -36793,7 +36790,10 @@ "@stylexjs/scripts": { "version": "file:packages/scripts", "requires": { + "brotli-size": "^4.0.0", + "clean-css": "^5.3.3", "flow-api-translator": "0.19.2", + "terser": "^5.31.0", "yargs": "17.7.2" } }, @@ -38333,6 +38333,21 @@ "fill-range": "^7.0.1" } }, + "brotli-size": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/brotli-size/-/brotli-size-4.0.0.tgz", + "integrity": "sha512-uA9fOtlTRC0iqKfzff1W34DXUA3GyVqbUaeo3Rw3d4gd1eavKVCETXrn3NzO74W+UVkG3UHu8WxUi+XvKI/huA==", + "requires": { + "duplexer": "0.1.1" + }, + "dependencies": { + "duplexer": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.1.tgz", + "integrity": "sha512-sxNZ+ljy+RA1maXoUReeqBBpBC6RLKmg5ewzV+x+mSETmWNoKdZN6vcQjpFROemza23hGFskJtFNoUWUaQ+R4Q==" + } + } + }, "browserslist": { "version": "4.23.0", "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.0.tgz", @@ -38540,9 +38555,9 @@ "dev": true }, "clean-css": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-5.3.1.tgz", - "integrity": "sha512-lCr8OHhiWCTw4v8POJovCoh4T7I9U11yVsPjMWWnnMmp9ZowCxyad1Pathle/9HjaDp+fdQKjO9fQydE6RHTZg==", + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-5.3.3.tgz", + "integrity": "sha512-D5J+kHaVb/wKSFcyyV75uCn8fiY4sV38XJoe4CUyGQ+mOU/fMVYUdH1hJC+CJQ5uY3EnW27SbJYS4X8BiLrAFg==", "requires": { "source-map": "~0.6.0" } @@ -39469,15 +39484,6 @@ "integrity": "sha512-M0JtH+hlOL5pLQwHOLNYZaXuhqmvS8oExsqB1SBYgA4Dk7u/xx+YdGHXaK5pyUfed5mYXdlYiphWq3G8cRi5JQ==", "dev": true }, - "clean-css": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-5.3.2.tgz", - "integrity": "sha512-JVJbM+f3d3Q704rF4bqQ5UUyTtuJ0JRKNbTKVEeujCCBoMdkEi+V+e8oktO9qGQNSvHrFTM6JZRXrUvGR1czww==", - "dev": true, - "requires": { - "source-map": "~0.6.0" - } - }, "debug": { "version": "3.2.7", "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", @@ -52381,9 +52387,9 @@ "version": "2.2.1" }, "terser": { - "version": "5.19.2", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.19.2.tgz", - "integrity": "sha512-qC5+dmecKJA4cpYxRa5aVkKehYsQKc+AHeKl0Oe62aYjBL8ZA33tTljktDHJSaxxMnbI5ZYw+o/S2DxxLu8OfA==", + "version": "5.31.6", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.31.6.tgz", + "integrity": "sha512-PQ4DAriWzKj+qgehQ7LK5bQqCFNMmlhjR2PFFLuqGCpuCAauxemVBWwWOxo3UIwWQx8+Pr61Df++r76wDmkQBg==", "requires": { "@jridgewell/source-map": "^0.3.3", "acorn": "^8.8.2", @@ -53869,6 +53875,60 @@ "version": "1.0.5", "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-1.0.5.tgz", "integrity": "sha512-V50KMwwzqJV0NpZIZFwfOD5/lyny3WlSzRiXgA0G7VUnRlqttta1L6UQIHzd6EuBY/cHGfwTIck7w1yH6Q5zUw==" + }, + "@next/swc-darwin-arm64": { + "version": "14.0.1", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-14.0.1.tgz", + "integrity": "sha512-JyxnGCS4qT67hdOKQ0CkgFTp+PXub5W1wsGvIq98TNbF3YEIN7iDekYhYsZzc8Ov0pWEsghQt+tANdidITCLaw==", + "optional": true + }, + "@next/swc-darwin-x64": { + "version": "14.0.1", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-14.0.1.tgz", + "integrity": "sha512-625Z7bb5AyIzswF9hvfZWa+HTwFZw+Jn3lOBNZB87lUS0iuCYDHqk3ujuHCkiyPtSC0xFBtYDLcrZ11mF/ap3w==", + "optional": true + }, + "@next/swc-linux-arm64-gnu": { + "version": "14.0.1", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-14.0.1.tgz", + "integrity": "sha512-iVpn3KG3DprFXzVHM09kvb//4CNNXBQ9NB/pTm8LO+vnnnaObnzFdS5KM+w1okwa32xH0g8EvZIhoB3fI3mS1g==", + "optional": true + }, + "@next/swc-linux-arm64-musl": { + "version": "14.0.1", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-14.0.1.tgz", + "integrity": "sha512-mVsGyMxTLWZXyD5sen6kGOTYVOO67lZjLApIj/JsTEEohDDt1im2nkspzfV5MvhfS7diDw6Rp/xvAQaWZTv1Ww==", + "optional": true + }, + "@next/swc-linux-x64-gnu": { + "version": "14.0.1", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-14.0.1.tgz", + "integrity": "sha512-wMqf90uDWN001NqCM/auRl3+qVVeKfjJdT9XW+RMIOf+rhUzadmYJu++tp2y+hUbb6GTRhT+VjQzcgg/QTD9NQ==", + "optional": true + }, + "@next/swc-linux-x64-musl": { + "version": "14.0.1", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-14.0.1.tgz", + "integrity": "sha512-ol1X1e24w4j4QwdeNjfX0f+Nza25n+ymY0T2frTyalVczUmzkVD7QGgPTZMHfR1aLrO69hBs0G3QBYaj22J5GQ==", + "optional": true + }, + "@next/swc-win32-arm64-msvc": { + "version": "14.0.1", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-14.0.1.tgz", + "integrity": "sha512-WEmTEeWs6yRUEnUlahTgvZteh5RJc4sEjCQIodJlZZ5/VJwVP8p2L7l6VhzQhT4h7KvLx/Ed4UViBdne6zpIsw==", + "optional": true + }, + "@next/swc-win32-ia32-msvc": { + "version": "14.0.1", + "resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-14.0.1.tgz", + "integrity": "sha512-oFpHphN4ygAgZUKjzga7SoH2VGbEJXZa/KL8bHCAwCjDWle6R1SpiGOdUdA8EJ9YsG1TYWpzY6FTbUA+iAJeww==", + "optional": true + }, + "@next/swc-win32-x64-msvc": { + "version": "14.0.1", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-14.0.1.tgz", + "integrity": "sha512-FFp3nOJ/5qSpeWT0BZQ+YE1pSMk4IMpkME/1DwKBwhg4mJLB9L+6EXuJi4JEwaJdl5iN+UUlmUD3IsR1kx5fAg==", + "optional": true } } } diff --git a/packages/scripts/package.json b/packages/scripts/package.json index c4ae06dc..1a38f818 100644 --- a/packages/scripts/package.json +++ b/packages/scripts/package.json @@ -7,8 +7,15 @@ "gen-types": "./gen-types.js", "rewrite-imports": "./rewrite-imports.js" }, + "scripts": { + "size": "./size.js", + "size:compare": "./size-compare.js" + }, "dependencies": { + "brotli-size": "^4.0.0", + "clean-css": "^5.3.3", "flow-api-translator": "0.19.2", + "terser": "^5.31.0", "yargs": "17.7.2" } } diff --git a/packages/scripts/size-compare.js b/packages/scripts/size-compare.js new file mode 100755 index 00000000..1096b485 --- /dev/null +++ b/packages/scripts/size-compare.js @@ -0,0 +1,106 @@ +#!/usr/bin/env node +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * + */ + +'use strict'; + +const fs = require('fs'); + +function readJsonFile(filePath) { + try { + const fileContents = fs.readFileSync(filePath, 'utf8'); + const data = JSON.parse(fileContents); + return data; + } catch (error) { + console.error(`Error reading file ${filePath}:`, error); + return null; + } +} + +function mergeData(base, patch) { + const merged = {}; + function addToMerged(data, fileIndex) { + Object.keys(data).forEach((key) => { + if (merged[key] == null) { + merged[key] = {}; + } + Object.keys(data[key]).forEach((subKey) => { + if (merged[key][subKey] == null) { + merged[key][subKey] = {}; + } + merged[key][subKey][fileIndex] = data[key][subKey]; + }); + }); + } + if (base != null) { + addToMerged(base, 1); + } + if (patch != null) { + addToMerged(patch, 2); + } + return merged; +} + +function generateComparisonData(results) { + const baseResult = parseInt(results[1], 10); + const patchResult = parseInt(results[2], 10); + const isValidBase = !isNaN(baseResult); + const isValidPatch = !isNaN(patchResult); + let icon = '', + ratioFixed = ''; + + if (isValidBase && isValidPatch) { + const ratio = patchResult / baseResult; + ratioFixed = ratio.toFixed(2); + if (ratio < 0.95 || ratio > 1.05) { + icon = '**!!**'; + } else if (ratio < 1) { + icon = '-'; + } else if (ratio > 1) { + icon = '+'; + } + } + + return { + baseResult: isValidBase ? baseResult.toLocaleString() : '', + patchResult: isValidPatch ? patchResult.toLocaleString() : '', + ratio: ratioFixed, + icon, + }; +} + +function generateMarkdownTable(mergedData) { + const rows = []; + rows.push('| **Results** | **Base** | **Patch** | **Ratio** | |'); + rows.push('| :--- | ---: | ---: | ---: | ---: |'); + Object.keys(mergedData).forEach((suiteName) => { + rows.push('| | | | |'); + rows.push(`| **${suiteName}** | | | | |`); + Object.keys(mergedData[suiteName]).forEach((test) => { + const results = mergedData[suiteName][test]; + const { baseResult, patchResult, ratio, icon } = + generateComparisonData(results); + rows.push( + `| · ${test} | ${baseResult} | ${patchResult} | ${ratio} | ${icon} |`, + ); + }); + }); + return rows.join('\n'); +} + +/** + * Compare up to 2 different benchmark runs + */ +const args = process.argv.slice(2); +const baseResults = args[0] ? readJsonFile(args[0]) : null; +const patchResults = args[1] ? readJsonFile(args[1]) : null; +const mergedData = mergeData(baseResults, patchResults); +const markdownTable = generateMarkdownTable(mergedData); + +console.log(markdownTable); diff --git a/packages/scripts/size.js b/packages/scripts/size.js new file mode 100755 index 00000000..94a9dd8a --- /dev/null +++ b/packages/scripts/size.js @@ -0,0 +1,90 @@ +#!/usr/bin/env node +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * + */ + +'use strict'; + +const brotliSizePkg = require('brotli-size'); +const CleanCSS = require('clean-css'); +const fs = require('fs'); +const path = require('path'); +const { minify_sync } = require('terser'); +const yargs = require('yargs/yargs'); +const { hideBin } = require('yargs/helpers'); + +const minifyCSS = new CleanCSS(); + +function getSizes(files) { + const sizes = files.map((file) => { + const code = fs.readFileSync(file, 'utf8'); + let result = ''; + if (file.endsWith('.css')) { + result = minifyCSS.minify(code).styles; + } + if (file.endsWith('.js')) { + result = minify_sync(code).code; + } + const minified = Buffer.byteLength(result, 'utf8'); + const compressed = brotliSizePkg.sync(result); + return { file, compressed, minified }; + }); + return sizes; +} + +// run.js --outfile filename.js +const argv = yargs(hideBin(process.argv)).option('outfile', { + alias: 'o', + type: 'string', + description: 'Output file', + demandOption: false, +}).argv; +const outfile = argv.outfile; + +const files = [ + path.join(__dirname, '../stylex/lib/stylex.js'), + path.join(__dirname, '../stylex/lib/StyleXSheet.js'), + path.join(__dirname, '../../apps/rollup-example/.build/bundle.js'), + path.join(__dirname, '../../apps/rollup-example/.build/stylex.css'), +]; + +console.log('Running "size" benchmark, please wait...'); + +const sizes = getSizes(files); + +const aggregatedResults = {}; +sizes.forEach((entry) => { + const { file, minified, compressed } = entry; + const filename = file.split('apps/')[1] || file.split('packages/')[1]; + aggregatedResults[filename] = { + compressed, + minified, + }; +}); + +const aggregatedResultsString = JSON.stringify(aggregatedResults, null, 2); + +// Print / Write results +const now = new Date(); +const year = now.getFullYear(); +const month = String(now.getMonth() + 1).padStart(2, '0'); // Month is 0-indexed +const day = String(now.getDate()).padStart(2, '0'); +const hours = String(now.getHours()).padStart(2, '0'); +const minutes = String(now.getMinutes()).padStart(2, '0'); +const timestamp = `${year}${month}${day}-${hours}${minutes}`; + +const dirpath = `${process.cwd()}/.logs`; +const filepath = `${dirpath}/size-${timestamp}.json`; +if (!fs.existsSync(dirpath)) { + fs.mkdirSync(dirpath); +} +const outpath = outfile || filepath; +fs.writeFileSync(outpath, `${aggregatedResultsString}\n`); + +console.log(aggregatedResultsString); +console.log('Results written to', outpath);