Skip to content

Commit

Permalink
Merge pull request #1586 from ably/1580-modulereport-gzip-too
Browse files Browse the repository at this point in the history
[SDK-4039] Make `modulereport` print gzipped size too
  • Loading branch information
lawrence-forooghian authored Jan 24, 2024
2 parents 182f53d + 792e10b commit 49b9f40
Show file tree
Hide file tree
Showing 3 changed files with 165 additions and 48 deletions.
52 changes: 52 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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",
Expand Down Expand Up @@ -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"
}
}
157 changes: 110 additions & 47 deletions scripts/moduleReport.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
import * as esbuild from 'esbuild';
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 minimalUsefulRealtimeBundleSizeThresholdKiB = 94;
const minimalUsefulRealtimeBundleSizeThresholdsKiB = { raw: 94, gzip: 29 };

const baseClientNames = ['BaseRest', 'BaseRealtime'];

// List of all modules accepted in ModulesMap
const moduleNames = [
Expand Down Expand Up @@ -46,6 +51,21 @@ interface BundleInfo {
sourceMap: Uint8Array;
}

interface ByteSizes {
rawByteSize: number;
gzipEncodedByteSize: number;
}

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'
function getBundleInfo(modules: string[]): BundleInfo {
const outfile = modules.join('');
Expand Down Expand Up @@ -79,9 +99,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<ByteSizes> {
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) {
Expand All @@ -91,54 +116,57 @@ async function runSourceMapExplorer(bundleInfo: BundleInfo) {
});
}

function printAndCheckModuleSizes() {
const errors: Error[] = [];
async function calculateAndCheckModuleSizes(): Promise<Output> {
const output: Output = { tableRows: [], errors: [] };

['BaseRest', 'BaseRealtime'].forEach((baseClient) => {
const baseClientSize = getImportSize([baseClient]);
for (const baseClient of baseClientNames) {
const baseClientSizes = await getImportSizes([baseClient]);

// First display the size of the base client
console.log(`${baseClient}: ${formatBytes(baseClientSize)}`);
// 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
[...moduleNames, ...Object.values(functions).map((functionData) => functionData.name)].forEach((exportName) => {
const size = getImportSize([baseClient, exportName]);
console.log(`${baseClient} + ${exportName}: ${formatBytes(size)}`);
// 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]);
output.tableRows.push({ description: `${baseClient} + ${exportName}`, 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.`));
output.errors.push(new Error(`Adding ${exportName} to ${baseClient} does not increase the bundle size.`));
}
});
});
}
}

return errors;
return output;
}

function printAndCheckFunctionSizes() {
const errors: Error[] = [];
async function calculateAndCheckFunctionSizes(): Promise<Output> {
const output: Output = { tableRows: [], errors: [] };

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)}`);
// First output the size of the function
const standaloneSizes = await getImportSizes([functionName]);
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 withTransitiveImportsSize = getImportSize([functionName, ...transitiveImports]);
console.log(`${functionName} + ${transitiveImports.join(' + ')}: ${formatBytes(withTransitiveImportsSize)}`);
const withTransitiveImportsSizes = await getImportSizes([functionName, ...transitiveImports]);
output.tableRows.push({
description: `${functionName} + ${transitiveImports.join(' + ')}`,
sizes: 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
// 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.`
)
Expand All @@ -147,28 +175,45 @@ function printAndCheckFunctionSizes() {
}
}

return errors;
return output;
}

function printAndCheckMinimalUsefulRealtimeBundleSize() {
const errors: Error[] = [];
async function calculateAndCheckMinimalUsefulRealtimeBundleSize(): Promise<Output> {
const output: Output = { tableRows: [], errors: [] };

const exports = ['BaseRealtime', 'FetchRequest', 'WebSocketTransport'];
const size = getImportSize(exports);
const sizes = await getImportSizes(exports);

console.log(`Minimal useful Realtime (${exports.join(' + ')}): ${formatBytes(size)}`);
output.tableRows.push({ description: `Minimal useful Realtime (${exports.join(' + ')})`, sizes });

if (size > minimalUsefulRealtimeBundleSizeThresholdKiB * 1024) {
errors.push(
if (sizes.rawByteSize > minimalUsefulRealtimeBundleSizeThresholdsKiB.raw * 1024) {
output.errors.push(
new Error(
`Minimal useful Realtime bundle is ${formatBytes(
size
)}, which is greater than allowed maximum of ${minimalUsefulRealtimeBundleSizeThresholdKiB} KiB.`
`Minimal raw useful Realtime bundle is ${formatBytes(
sizes.rawByteSize
)}, which is greater than allowed maximum of ${minimalUsefulRealtimeBundleSizeThresholdsKiB.raw} KiB.`
)
);
}

return errors;
if (sizes.gzipEncodedByteSize > minimalUsefulRealtimeBundleSizeThresholdsKiB.gzip * 1024) {
output.errors.push(
new Error(
`Minimal gzipped useful Realtime bundle is ${formatBytes(
sizes.gzipEncodedByteSize
)}, which is greater than allowed maximum of ${minimalUsefulRealtimeBundleSizeThresholdsKiB.gzip} KiB.`
)
);
}

return output;
}

async function calculateAllModulesBundleSize(): Promise<Output> {
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.
Expand Down Expand Up @@ -256,15 +301,33 @@ async function checkBaseRealtimeFiles() {
}

(async function run() {
const errors: Error[] = [];

errors.push(...printAndCheckMinimalUsefulRealtimeBundleSize());
errors.push(...printAndCheckModuleSizes());
errors.push(...printAndCheckFunctionSizes());
errors.push(...(await checkBaseRealtimeFiles()));
const output = (
await Promise.all([
calculateAndCheckMinimalUsefulRealtimeBundleSize(),
calculateAllModulesBundleSize(),
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);
Expand Down

0 comments on commit 49b9f40

Please sign in to comment.