From a95b38a3cbf4c14b53231bc7d2e254f4e3c007ba Mon Sep 17 00:00:00 2001 From: Lawrence Forooghian Date: Wed, 24 Jan 2024 13:59:39 +0000 Subject: [PATCH 1/4] Remove unnecessary Object.values call Not sure why I added this in 63f5524. --- scripts/moduleReport.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/moduleReport.ts b/scripts/moduleReport.ts index 22cfe14e91..4186f2de12 100644 --- a/scripts/moduleReport.ts +++ b/scripts/moduleReport.ts @@ -101,7 +101,7 @@ function printAndCheckModuleSizes() { console.log(`${baseClient}: ${formatBytes(baseClientSize)}`); // Then display the size of each export together with the base client - [...moduleNames, ...Object.values(functions).map((functionData) => functionData.name)].forEach((exportName) => { + [...moduleNames, ...functions.map((functionData) => functionData.name)].forEach((exportName) => { const size = getImportSize([baseClient, exportName]); console.log(`${baseClient} + ${exportName}: ${formatBytes(size)}`); From 698fde2793941b0170af158da5e709616972131c Mon Sep 17 00:00:00 2001 From: Lawrence Forooghian Date: Fri, 19 Jan 2024 15:02:41 +0000 Subject: [PATCH 2/4] Make modulereport print gzipped size too MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit It’s quite likely that some of our users will be serving their bundles with gzip encoding, so this is useful information to have. Resolves #1580. --- scripts/moduleReport.ts | 84 +++++++++++++++++++++++++++-------------- 1 file changed, 55 insertions(+), 29 deletions(-) diff --git a/scripts/moduleReport.ts b/scripts/moduleReport.ts index 4186f2de12..3a525ab97d 100644 --- a/scripts/moduleReport.ts +++ b/scripts/moduleReport.ts @@ -1,9 +1,11 @@ import * as esbuild from 'esbuild'; import * as path from 'path'; import { explore } from 'source-map-explorer'; +import { promisify } from 'util'; +import { gzip } from 'zlib'; // The maximum size we allow for a minimal useful Realtime bundle (i.e. one that can subscribe to a channel) -const minimalUsefulRealtimeBundleSizeThresholdKiB = 94; +const minimalUsefulRealtimeBundleSizeThresholdsKiB = { raw: 94, gzip: 29 }; // List of all modules accepted in ModulesMap const moduleNames = [ @@ -46,6 +48,15 @@ interface BundleInfo { sourceMap: Uint8Array; } +interface ByteSizes { + rawByteSize: number; + gzipEncodedByteSize: number; +} + +function formatByteSizes(sizes: ByteSizes) { + return `raw: ${formatBytes(sizes.rawByteSize)}; gzip: ${formatBytes(sizes.gzipEncodedByteSize)}`; +} + // Uses esbuild to create a bundle containing the named exports from 'ably/modules' function getBundleInfo(modules: string[]): BundleInfo { const outfile = modules.join(''); @@ -79,9 +90,14 @@ function getBundleInfo(modules: string[]): BundleInfo { } // Gets the bundled size in bytes of an array of named exports from 'ably/modules' -function getImportSize(modules: string[]) { +async function getImportSizes(modules: string[]): Promise { const bundleInfo = getBundleInfo(modules); - return bundleInfo.byteSize; + + return { + rawByteSize: bundleInfo.byteSize, + // I’m trusting that the default settings of the `gzip` function (e.g. compression level) are somewhat representative of how gzip compression is normally used when serving files in the real world + gzipEncodedByteSize: (await promisify(gzip)(bundleInfo.code)).byteLength, + }; } async function runSourceMapExplorer(bundleInfo: BundleInfo) { @@ -91,48 +107,48 @@ async function runSourceMapExplorer(bundleInfo: BundleInfo) { }); } -function printAndCheckModuleSizes() { +async function printAndCheckModuleSizes() { const errors: Error[] = []; - ['BaseRest', 'BaseRealtime'].forEach((baseClient) => { - const baseClientSize = getImportSize([baseClient]); + for (const baseClient of ['BaseRest', 'BaseRealtime']) { + const baseClientSizes = await getImportSizes([baseClient]); // First display the size of the base client - console.log(`${baseClient}: ${formatBytes(baseClientSize)}`); + console.log(`${baseClient}: ${formatByteSizes(baseClientSizes)}`); // Then display the size of each export together with the base client - [...moduleNames, ...functions.map((functionData) => functionData.name)].forEach((exportName) => { - const size = getImportSize([baseClient, exportName]); - console.log(`${baseClient} + ${exportName}: ${formatBytes(size)}`); + for (const exportName of [...moduleNames, ...functions.map((functionData) => functionData.name)]) { + const sizes = await getImportSizes([baseClient, exportName]); + console.log(`${baseClient} + ${exportName}: ${formatByteSizes(sizes)}`); - if (!(baseClientSize < size) && !(baseClient === 'BaseRest' && exportName === 'Rest')) { + if (!(baseClientSizes.rawByteSize < sizes.rawByteSize) && !(baseClient === 'BaseRest' && exportName === 'Rest')) { // Emit an error if adding the module does not increase the bundle size // (this means that the module is not being tree-shaken correctly). errors.push(new Error(`Adding ${exportName} to ${baseClient} does not increase the bundle size.`)); } - }); - }); + } + } return errors; } -function printAndCheckFunctionSizes() { +async function printAndCheckFunctionSizes() { const errors: Error[] = []; for (const functionData of functions) { const { name: functionName, transitiveImports } = functionData; // First display the size of the function - const standaloneSize = getImportSize([functionName]); - console.log(`${functionName}: ${formatBytes(standaloneSize)}`); + const standaloneSizes = await getImportSizes([functionName]); + console.log(`${functionName}: ${formatByteSizes(standaloneSizes)}`); // Then display the size of the function together with the modules we expect // it to transitively import if (transitiveImports.length > 0) { - const withTransitiveImportsSize = getImportSize([functionName, ...transitiveImports]); - console.log(`${functionName} + ${transitiveImports.join(' + ')}: ${formatBytes(withTransitiveImportsSize)}`); + const withTransitiveImportsSizes = await getImportSizes([functionName, ...transitiveImports]); + console.log(`${functionName} + ${transitiveImports.join(' + ')}: ${formatByteSizes(withTransitiveImportsSizes)}`); - if (withTransitiveImportsSize > standaloneSize) { + if (withTransitiveImportsSizes.rawByteSize > standaloneSizes.rawByteSize) { // Emit an error if the bundle size is increased by adding the modules // that we expect this function to have transitively imported anyway. // This seemed like a useful sense check, but it might need tweaking in @@ -150,20 +166,30 @@ function printAndCheckFunctionSizes() { return errors; } -function printAndCheckMinimalUsefulRealtimeBundleSize() { +async function printAndCheckMinimalUsefulRealtimeBundleSize() { const errors: Error[] = []; const exports = ['BaseRealtime', 'FetchRequest', 'WebSocketTransport']; - const size = getImportSize(exports); + const sizes = await getImportSizes(exports); - console.log(`Minimal useful Realtime (${exports.join(' + ')}): ${formatBytes(size)}`); + console.log(`Minimal useful Realtime (${exports.join(' + ')}): ${formatByteSizes(sizes)}`); + + if (sizes.rawByteSize > minimalUsefulRealtimeBundleSizeThresholdsKiB.raw * 1024) { + errors.push( + new Error( + `Minimal raw useful Realtime bundle is ${formatBytes( + sizes.rawByteSize + )}, which is greater than allowed maximum of ${minimalUsefulRealtimeBundleSizeThresholdsKiB.raw} KiB.` + ) + ); + } - if (size > minimalUsefulRealtimeBundleSizeThresholdKiB * 1024) { + if (sizes.gzipEncodedByteSize > minimalUsefulRealtimeBundleSizeThresholdsKiB.gzip * 1024) { errors.push( new Error( - `Minimal useful Realtime bundle is ${formatBytes( - size - )}, which is greater than allowed maximum of ${minimalUsefulRealtimeBundleSizeThresholdKiB} KiB.` + `Minimal gzipped useful Realtime bundle is ${formatBytes( + sizes.gzipEncodedByteSize + )}, which is greater than allowed maximum of ${minimalUsefulRealtimeBundleSizeThresholdsKiB.gzip} KiB.` ) ); } @@ -258,9 +284,9 @@ async function checkBaseRealtimeFiles() { (async function run() { const errors: Error[] = []; - errors.push(...printAndCheckMinimalUsefulRealtimeBundleSize()); - errors.push(...printAndCheckModuleSizes()); - errors.push(...printAndCheckFunctionSizes()); + errors.push(...(await printAndCheckMinimalUsefulRealtimeBundleSize())); + errors.push(...(await printAndCheckModuleSizes())); + errors.push(...(await printAndCheckFunctionSizes())); errors.push(...(await checkBaseRealtimeFiles())); if (errors.length > 0) { From 883e262967553d185b1f46a9b359751d7ed3d888 Mon Sep 17 00:00:00 2001 From: Lawrence Forooghian Date: Wed, 24 Jan 2024 09:38:56 +0000 Subject: [PATCH 3/4] Format modulereport output as a table A bit easier to read. --- package-lock.json | 52 +++++++++++++++++++++++ package.json | 4 +- scripts/moduleReport.ts | 91 ++++++++++++++++++++++++++--------------- 3 files changed, 114 insertions(+), 33 deletions(-) diff --git a/package-lock.json b/package-lock.json index 277a60789d..6c90d8cd65 100644 --- a/package-lock.json +++ b/package-lock.json @@ -20,6 +20,7 @@ "@babel/parser": "^7.23.6", "@babel/traverse": "^7.23.7", "@testing-library/react": "^13.3.0", + "@types/cli-table": "^0.3.4", "@types/jmespath": "^0.15.2", "@types/node": "^18.0.0", "@types/request": "^2.48.7", @@ -30,6 +31,7 @@ "async": "ably-forks/async#requirejs", "aws-sdk": "^2.1413.0", "chai": "^4.2.0", + "cli-table": "^0.3.11", "cors": "^2.8.5", "esbuild": "^0.18.10", "esbuild-plugin-umd-wrapper": "^1.0.7", @@ -1436,6 +1438,12 @@ "@types/chai": "*" } }, + "node_modules/@types/cli-table": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/@types/cli-table/-/cli-table-0.3.4.tgz", + "integrity": "sha512-GsALrTL69mlwbAw/MHF1IPTadSLZQnsxe7a80G8l4inN/iEXCOcVeT/S7aRc6hbhqzL9qZ314kHPDQnQ3ev+HA==", + "dev": true + }, "node_modules/@types/eslint": { "version": "8.44.8", "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.44.8.tgz", @@ -2798,6 +2806,27 @@ "node": ">=6.0" } }, + "node_modules/cli-table": { + "version": "0.3.11", + "resolved": "https://registry.npmjs.org/cli-table/-/cli-table-0.3.11.tgz", + "integrity": "sha512-IqLQi4lO0nIB4tcdTpN4LCB9FI3uqrJZK7RC515EnhZ6qBaglkIgICb1wjeAqpdoOabm1+SuQtkXIPdYC93jhQ==", + "dev": true, + "dependencies": { + "colors": "1.0.3" + }, + "engines": { + "node": ">= 0.2.0" + } + }, + "node_modules/cli-table/node_modules/colors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/colors/-/colors-1.0.3.tgz", + "integrity": "sha512-pFGrxThWcWQ2MsAz6RtgeWe4NK2kUE1WfsrvvlctdII745EW9I0yflqhe7++M5LEc7bV2c/9/5zc8sFcpL0Drw==", + "dev": true, + "engines": { + "node": ">=0.1.90" + } + }, "node_modules/cli-table3": { "version": "0.6.3", "resolved": "https://registry.npmjs.org/cli-table3/-/cli-table3-0.6.3.tgz", @@ -11719,6 +11748,12 @@ "@types/chai": "*" } }, + "@types/cli-table": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/@types/cli-table/-/cli-table-0.3.4.tgz", + "integrity": "sha512-GsALrTL69mlwbAw/MHF1IPTadSLZQnsxe7a80G8l4inN/iEXCOcVeT/S7aRc6hbhqzL9qZ314kHPDQnQ3ev+HA==", + "dev": true + }, "@types/eslint": { "version": "8.44.8", "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.44.8.tgz", @@ -12757,6 +12792,23 @@ "integrity": "sha512-p3KULyQg4S7NIHixdwbGX+nFHkoBiA4YQmyWtjb8XngSKV124nJmRysgAeujbUVb15vh+RvFUfCPqU7rXk+hZg==", "dev": true }, + "cli-table": { + "version": "0.3.11", + "resolved": "https://registry.npmjs.org/cli-table/-/cli-table-0.3.11.tgz", + "integrity": "sha512-IqLQi4lO0nIB4tcdTpN4LCB9FI3uqrJZK7RC515EnhZ6qBaglkIgICb1wjeAqpdoOabm1+SuQtkXIPdYC93jhQ==", + "dev": true, + "requires": { + "colors": "1.0.3" + }, + "dependencies": { + "colors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/colors/-/colors-1.0.3.tgz", + "integrity": "sha512-pFGrxThWcWQ2MsAz6RtgeWe4NK2kUE1WfsrvvlctdII745EW9I0yflqhe7++M5LEc7bV2c/9/5zc8sFcpL0Drw==", + "dev": true + } + } + }, "cli-table3": { "version": "0.6.3", "resolved": "https://registry.npmjs.org/cli-table3/-/cli-table3-0.6.3.tgz", diff --git a/package.json b/package.json index 760795238e..ae880a90d5 100644 --- a/package.json +++ b/package.json @@ -52,6 +52,7 @@ "@babel/parser": "^7.23.6", "@babel/traverse": "^7.23.7", "@testing-library/react": "^13.3.0", + "@types/cli-table": "^0.3.4", "@types/jmespath": "^0.15.2", "@types/node": "^18.0.0", "@types/request": "^2.48.7", @@ -62,6 +63,7 @@ "async": "ably-forks/async#requirejs", "aws-sdk": "^2.1413.0", "chai": "^4.2.0", + "cli-table": "^0.3.11", "cors": "^2.8.5", "esbuild": "^0.18.10", "esbuild-plugin-umd-wrapper": "^1.0.7", @@ -138,7 +140,7 @@ "format": "prettier --write --ignore-path .gitignore --ignore-path .prettierignore src test ably.d.ts modules.d.ts webpack.config.js Gruntfile.js scripts/*.[jt]s docs/chrome-mv3.md grunt", "format:check": "prettier --check --ignore-path .gitignore --ignore-path .prettierignore src test ably.d.ts modules.d.ts webpack.config.js Gruntfile.js scripts/*.[jt]s grunt", "sourcemap": "source-map-explorer build/ably.min.js", - "modulereport": "tsc --noEmit scripts/moduleReport.ts && esr scripts/moduleReport.ts", + "modulereport": "tsc --noEmit --esModuleInterop scripts/moduleReport.ts && esr scripts/moduleReport.ts", "docs": "typedoc" } } diff --git a/scripts/moduleReport.ts b/scripts/moduleReport.ts index 3a525ab97d..8df69416e5 100644 --- a/scripts/moduleReport.ts +++ b/scripts/moduleReport.ts @@ -3,6 +3,7 @@ import * as path from 'path'; import { explore } from 'source-map-explorer'; import { promisify } from 'util'; import { gzip } from 'zlib'; +import Table from 'cli-table'; // The maximum size we allow for a minimal useful Realtime bundle (i.e. one that can subscribe to a channel) const minimalUsefulRealtimeBundleSizeThresholdsKiB = { raw: 94, gzip: 29 }; @@ -53,8 +54,14 @@ interface ByteSizes { gzipEncodedByteSize: number; } -function formatByteSizes(sizes: ByteSizes) { - return `raw: ${formatBytes(sizes.rawByteSize)}; gzip: ${formatBytes(sizes.gzipEncodedByteSize)}`; +interface TableRow { + description: string; + sizes: ByteSizes; +} + +interface Output { + tableRows: TableRow[]; + errors: Error[]; } // Uses esbuild to create a bundle containing the named exports from 'ably/modules' @@ -107,46 +114,49 @@ async function runSourceMapExplorer(bundleInfo: BundleInfo) { }); } -async function printAndCheckModuleSizes() { - const errors: Error[] = []; +async function calculateAndCheckModuleSizes(): Promise { + const output: Output = { tableRows: [], errors: [] }; for (const baseClient of ['BaseRest', 'BaseRealtime']) { const baseClientSizes = await getImportSizes([baseClient]); - // First display the size of the base client - console.log(`${baseClient}: ${formatByteSizes(baseClientSizes)}`); + // First output the size of the base client + output.tableRows.push({ description: baseClient, sizes: baseClientSizes }); - // Then display the size of each export together with the base client + // Then output the size of each export together with the base client for (const exportName of [...moduleNames, ...functions.map((functionData) => functionData.name)]) { const sizes = await getImportSizes([baseClient, exportName]); - console.log(`${baseClient} + ${exportName}: ${formatByteSizes(sizes)}`); + output.tableRows.push({ description: `${baseClient} + ${exportName}`, sizes }); if (!(baseClientSizes.rawByteSize < sizes.rawByteSize) && !(baseClient === 'BaseRest' && exportName === 'Rest')) { // Emit an error if adding the module does not increase the bundle size // (this means that the module is not being tree-shaken correctly). - errors.push(new Error(`Adding ${exportName} to ${baseClient} does not increase the bundle size.`)); + output.errors.push(new Error(`Adding ${exportName} to ${baseClient} does not increase the bundle size.`)); } } } - return errors; + return output; } -async function printAndCheckFunctionSizes() { - const errors: Error[] = []; +async function calculateAndCheckFunctionSizes(): Promise { + const output: Output = { tableRows: [], errors: [] }; for (const functionData of functions) { const { name: functionName, transitiveImports } = functionData; - // First display the size of the function + // First output the size of the function const standaloneSizes = await getImportSizes([functionName]); - console.log(`${functionName}: ${formatByteSizes(standaloneSizes)}`); + output.tableRows.push({ description: functionName, sizes: standaloneSizes }); - // Then display the size of the function together with the modules we expect + // Then output the size of the function together with the modules we expect // it to transitively import if (transitiveImports.length > 0) { const withTransitiveImportsSizes = await getImportSizes([functionName, ...transitiveImports]); - console.log(`${functionName} + ${transitiveImports.join(' + ')}: ${formatByteSizes(withTransitiveImportsSizes)}`); + output.tableRows.push({ + description: `${functionName} + ${transitiveImports.join(' + ')}`, + sizes: withTransitiveImportsSizes, + }); if (withTransitiveImportsSizes.rawByteSize > standaloneSizes.rawByteSize) { // Emit an error if the bundle size is increased by adding the modules @@ -154,7 +164,7 @@ async function printAndCheckFunctionSizes() { // This seemed like a useful sense check, but it might need tweaking in // the future if we make future optimisations that mean that the // standalone functions don’t necessarily import the whole module. - errors.push( + output.errors.push( new Error( `Adding ${transitiveImports.join(' + ')} to ${functionName} unexpectedly increases the bundle size.` ) @@ -163,19 +173,19 @@ async function printAndCheckFunctionSizes() { } } - return errors; + return output; } -async function printAndCheckMinimalUsefulRealtimeBundleSize() { - const errors: Error[] = []; +async function calculateAndCheckMinimalUsefulRealtimeBundleSize(): Promise { + const output: Output = { tableRows: [], errors: [] }; const exports = ['BaseRealtime', 'FetchRequest', 'WebSocketTransport']; const sizes = await getImportSizes(exports); - console.log(`Minimal useful Realtime (${exports.join(' + ')}): ${formatByteSizes(sizes)}`); + output.tableRows.push({ description: `Minimal useful Realtime (${exports.join(' + ')})`, sizes }); if (sizes.rawByteSize > minimalUsefulRealtimeBundleSizeThresholdsKiB.raw * 1024) { - errors.push( + output.errors.push( new Error( `Minimal raw useful Realtime bundle is ${formatBytes( sizes.rawByteSize @@ -185,7 +195,7 @@ async function printAndCheckMinimalUsefulRealtimeBundleSize() { } if (sizes.gzipEncodedByteSize > minimalUsefulRealtimeBundleSizeThresholdsKiB.gzip * 1024) { - errors.push( + output.errors.push( new Error( `Minimal gzipped useful Realtime bundle is ${formatBytes( sizes.gzipEncodedByteSize @@ -194,7 +204,7 @@ async function printAndCheckMinimalUsefulRealtimeBundleSize() { ); } - return errors; + return output; } // Performs a sense check that there are no unexpected files making a large contribution to the BaseRealtime bundle size. @@ -282,15 +292,32 @@ async function checkBaseRealtimeFiles() { } (async function run() { - const errors: Error[] = []; - - errors.push(...(await printAndCheckMinimalUsefulRealtimeBundleSize())); - errors.push(...(await printAndCheckModuleSizes())); - errors.push(...(await printAndCheckFunctionSizes())); - errors.push(...(await checkBaseRealtimeFiles())); + const output = ( + await Promise.all([ + calculateAndCheckMinimalUsefulRealtimeBundleSize(), + calculateAndCheckModuleSizes(), + calculateAndCheckFunctionSizes(), + ]) + ).reduce((accum, current) => ({ + tableRows: [...accum.tableRows, ...current.tableRows], + errors: [...accum.errors, ...current.errors], + })); + + output.errors.push(...(await checkBaseRealtimeFiles())); + + const table = new Table({ + style: { head: ['green'] }, + head: ['Modules', 'Size (raw, KiB)', 'Size (gzipped, KiB)'], + rows: output.tableRows.map((row) => [ + row.description, + formatBytes(row.sizes.rawByteSize), + formatBytes(row.sizes.gzipEncodedByteSize), + ]), + }); + console.log(table.toString()); - if (errors.length > 0) { - for (const error of errors) { + if (output.errors.length > 0) { + for (const error of output.errors) { console.log(error.message); } process.exit(1); From 792e10b04dcc7d942f7c2c1b6585865a78f24531 Mon Sep 17 00:00:00 2001 From: Lawrence Forooghian Date: Wed, 24 Jan 2024 10:19:16 +0000 Subject: [PATCH 4/4] Make modulereport print size of bundle with all modules Seems like useful information to have. --- scripts/moduleReport.ts | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/scripts/moduleReport.ts b/scripts/moduleReport.ts index 8df69416e5..9b22c609d0 100644 --- a/scripts/moduleReport.ts +++ b/scripts/moduleReport.ts @@ -8,6 +8,8 @@ import Table from 'cli-table'; // The maximum size we allow for a minimal useful Realtime bundle (i.e. one that can subscribe to a channel) const minimalUsefulRealtimeBundleSizeThresholdsKiB = { raw: 94, gzip: 29 }; +const baseClientNames = ['BaseRest', 'BaseRealtime']; + // List of all modules accepted in ModulesMap const moduleNames = [ 'Rest', @@ -117,7 +119,7 @@ async function runSourceMapExplorer(bundleInfo: BundleInfo) { async function calculateAndCheckModuleSizes(): Promise { const output: Output = { tableRows: [], errors: [] }; - for (const baseClient of ['BaseRest', 'BaseRealtime']) { + for (const baseClient of baseClientNames) { const baseClientSizes = await getImportSizes([baseClient]); // First output the size of the base client @@ -207,6 +209,13 @@ async function calculateAndCheckMinimalUsefulRealtimeBundleSize(): Promise { + const exports = [...baseClientNames, ...moduleNames, ...functions.map((val) => val.name)]; + const sizes = await getImportSizes(exports); + + return { tableRows: [{ description: 'All modules', sizes }], errors: [] }; +} + // Performs a sense check that there are no unexpected files making a large contribution to the BaseRealtime bundle size. async function checkBaseRealtimeFiles() { const baseRealtimeBundleInfo = getBundleInfo(['BaseRealtime']); @@ -295,6 +304,7 @@ async function checkBaseRealtimeFiles() { const output = ( await Promise.all([ calculateAndCheckMinimalUsefulRealtimeBundleSize(), + calculateAllModulesBundleSize(), calculateAndCheckModuleSizes(), calculateAndCheckFunctionSizes(), ])