Skip to content

Commit

Permalink
[docs] Track bundle size of pages (#19978)
Browse files Browse the repository at this point in the history
  • Loading branch information
eps1lon authored Mar 4, 2020
1 parent 7124101 commit 743e26d
Show file tree
Hide file tree
Showing 4 changed files with 181 additions and 80 deletions.
15 changes: 9 additions & 6 deletions azure-pipelines.yml
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
trigger:
branches:
include:
- '*'
- '*'
exclude:
- l10n
- dependabot/*
- l10n
- dependabot/*

pool:
vmImage: 'ubuntu-latest'
Expand Down Expand Up @@ -37,7 +37,7 @@ steps:
globExpressions: '*.tgz'
targetFolder: 'artifacts/$(Build.SourceBranchName)/$(Build.SourceVersion)'
filesAcl: 'public-read'
displayName: "Upload distributables to S3"
displayName: 'Upload distributables to S3'
condition: and(succeeded(), ne(variables['Build.Reason'], 'PullRequest'))
env:
AWS_ACCESS_KEY_ID: $(AWS_ACCESS_KEY_ID)
Expand All @@ -49,8 +49,11 @@ steps:
targetPath: 'material-ui-core.tgz'

- script: |
yarn docs:build
displayName: 'build docs'
set -o pipefail
mkdir -p scripts/sizeSnapshot/build
yarn docs:build | tee scripts/sizeSnapshot/build/docs.next
set +o pipefail
displayName: 'build docs for size snapshot'
- script: |
yarn size:snapshot
Expand Down
155 changes: 105 additions & 50 deletions dangerfile.js
Original file line number Diff line number Diff line change
Expand Up @@ -90,16 +90,6 @@ function formatDiff(absoluteChange, relativeChange) {
)})`;
}

function computeBundleLabel(bundleId) {
if (bundleId === 'packages/material-ui/build/umd/material-ui.production.min.js') {
return '@material-ui/core[umd]';
}
if (bundleId === '@material-ui/core/Textarea') {
return 'TextareaAutosize';
}
return bundleId.replace(/^@material-ui\/core\//, '').replace(/\.esm$/, '');
}

/**
* Generates a Markdown table
* @param {{ label: string, align: 'left' | 'center' | 'right'}[]} headers
Expand Down Expand Up @@ -129,6 +119,74 @@ function generateEmphasizedChange([bundle, { parsed, gzip }]) {
return `**${bundle}**: parsed: ${changeParsed}, gzip: ${changeGzip}`;
}

/**
*
* @param {[string, object][]} entries
* @param {object} options
* @param {function (string): string} options.computeBundleLabel
*/
function createComparisonTable(entries, options) {
const { computeBundleLabel } = options;

return generateMDTable(
[
{ label: 'bundle' },
{ label: 'Size Change', align: 'right' },
{ label: 'Size', align: 'right' },
{ label: 'Gzip Change', align: 'right' },
{ label: 'Gzip', align: 'right' },
],
entries
.map(([bundleId, size]) => [computeBundleLabel(bundleId), size])
// orderBy(|parsedDiff| DESC, |gzipDiff| DESC, name ASC)
.sort(([labelA, statsA], [labelB, statsB]) => {
const compareParsedDiff =
Math.abs(statsB.parsed.absoluteDiff) - Math.abs(statsA.parsed.absoluteDiff);
const compareGzipDiff =
Math.abs(statsB.gzip.absoluteDiff) - Math.abs(statsA.gzip.absoluteDiff);
const compareName = labelA.localeCompare(labelB);

if (compareParsedDiff === 0 && compareGzipDiff === 0) {
return compareName;
}
if (compareParsedDiff === 0) {
return compareGzipDiff;
}
return compareParsedDiff;
})
.map(([label, { parsed, gzip }]) => {
return [
label,
formatDiff(parsed.absoluteDiff, parsed.relativeDiff),
prettyBytes(parsed.current),
formatDiff(gzip.absoluteDiff, gzip.relativeDiff),
prettyBytes(gzip.current),
];
}),
);
}

/**
* Puts results in different buckets wh
* @param {*} results
*/
function sieveResults(results) {
const main = [];
const pages = [];

results.forEach(entry => {
const [bundleId] = entry;

if (bundleId.startsWith('docs:')) {
pages.push(entry);
} else {
main.push(entry);
}
});

return { all: results, main, pages };
}

async function run() {
// Use git locally to grab the commit which represents the place
// where the branches differ
Expand All @@ -145,11 +203,14 @@ async function run() {
const commitRange = `${mergeBaseCommit}...${danger.github.pr.head.sha}`;

const comparison = await loadComparison(mergeBaseCommit, upstreamRef);
const results = Object.entries(comparison.bundles);
const anyResultsChanges = results.filter(createComparisonFilter(1, 1));

const { all: allResults, main: mainResults, pages: pageResults } = sieveResults(
Object.entries(comparison.bundles),
);
const anyResultsChanges = allResults.filter(createComparisonFilter(1, 1));

if (anyResultsChanges.length > 0) {
const importantChanges = results
const importantChanges = mainResults
.filter(createComparisonFilter(parsedSizeChangeThreshold, gzipSizeChangeThreshold))
.filter(isPackageComparison)
.map(generateEmphasizedChange);
Expand All @@ -159,50 +220,44 @@ async function run() {
markdown(importantChanges.join('\n'));
}

const detailsTable = generateMDTable(
[
{ label: 'bundle' },
{ label: 'Size Change', align: 'right' },
{ label: 'Size', align: 'right' },
{ label: 'Gzip Change', align: 'right' },
{ label: 'Gzip', align: 'right' },
],
results
.map(([bundleId, size]) => [computeBundleLabel(bundleId), size])
// orderBy(|parsedDiff| DESC, |gzipDiff| DESC, name ASC)
.sort(([labelA, statsA], [labelB, statsB]) => {
const compareParsedDiff =
Math.abs(statsB.parsed.absoluteDiff) - Math.abs(statsA.parsed.absoluteDiff);
const compareGzipDiff =
Math.abs(statsB.gzip.absoluteDiff) - Math.abs(statsA.gzip.absoluteDiff);
const compareName = labelA.localeCompare(labelB);

if (compareParsedDiff === 0 && compareGzipDiff === 0) {
return compareName;
}
if (compareParsedDiff === 0) {
return compareGzipDiff;
}
return compareParsedDiff;
})
.map(([label, { parsed, gzip }]) => {
return [
label,
formatDiff(parsed.absoluteDiff, parsed.relativeDiff),
prettyBytes(parsed.current),
formatDiff(gzip.absoluteDiff, gzip.relativeDiff),
prettyBytes(gzip.current),
];
}),
);
const mainDetailsTable = createComparisonTable(mainResults, {
computeBundleLabel: bundleId => {
if (bundleId === 'packages/material-ui/build/umd/material-ui.production.min.js') {
return '@material-ui/core[umd]';
}
if (bundleId === '@material-ui/core/Textarea') {
return 'TextareaAutosize';
}
if (bundleId === 'docs.main') {
return 'docs:/_app';
}
if (bundleId === 'docs.landing') {
return 'docs:/';
}
return bundleId.replace(/^@material-ui\/core\//, '').replace(/\.esm$/, '');
},
});
const pageDetailsTable = createComparisonTable(pageResults, {
computeBundleLabel: bundleId => {
const host = `https://deploy-preview-${danger.github.pr.number}--material-ui.netlify.com`;
const page = bundleId.replace(/^docs:/, '');
return `[${page}](${host}${page})`;
},
});

const details = `
<details>
<summary>Details of bundle changes.</summary>
<p>Comparing: ${commitRange}</p>
${detailsTable}
<details>
<summary>Details of page changes</summary>
${pageDetailsTable}
</details>
${mainDetailsTable}
</details>`;

Expand Down
68 changes: 67 additions & 1 deletion scripts/sizeSnapshot/create.js
Original file line number Diff line number Diff line change
Expand Up @@ -49,12 +49,78 @@ async function getWebpackSizes() {
});
}

// waiting for String.prototype.matchAll in node 10
function* matchAll(string, regex) {
let match = null;
do {
match = regex.exec(string);
if (match !== null) {
yield match;
}
} while (match !== null);
}

/**
* Inverse to `pretty-bytes`
*
* @param {string} n
* @param {'B', 'kB' | 'MB' | 'GB' | 'TB' | 'PB'} unit
* @returns {number}
*/

function prettyBytesInverse(n, unit) {
const metrixPrefix = unit.length < 2 ? '' : unit[0];
const metricPrefixes = ['', 'k', 'M', 'G', 'T', 'P'];
const metrixPrefixIndex = metricPrefixes.indexOf(metrixPrefix);
if (metrixPrefixIndex === -1) {
throw new TypeError(
`unrecognized metric prefix '${metrixPrefix}' in unit '${unit}'. only '${metricPrefixes.join(
"', '",
)}' are allowed`,
);
}

const power = metrixPrefixIndex * 3;
return n * 10 ** power;
}

/**
* parses output from next build to size snapshot format
* @returns {[string, { gzip: number, files: number, packages: number }][]}
*/

async function getNextPagesSize() {
const consoleOutput = await fse.readFile(path.join(__dirname, 'build/docs.next'), {
encoding: 'utf8',
});
const pageRegex = /^(?<treeViewPresentation>┌|├|└)\s+(?<fileType>σ|⚡|)\s+(?<pageUrl>[^\s]+)\s+(?<sizeFormatted>[0-9.]+)\s+(?<sizeUnit>\w+)/gm;

return Array.from(matchAll(consoleOutput, pageRegex), match => {
const { pageUrl, sizeFormatted, sizeUnit } = match.groups;

let snapshotId = `docs:${pageUrl}`;
// used to be tracked with custom logic hence the different ids
if (pageUrl === '/') {
snapshotId = 'docs.main';
} else if (pageUrl === '/_app') {
snapshotId = 'docs.main';
}
return [
snapshotId,
{
parsed: prettyBytesInverse(sizeFormatted, sizeUnit),
gzip: -1,
},
];
});
}

async function run() {
const rollupBundles = [path.join(workspaceRoot, 'packages/material-ui/size-snapshot.json')];

const bundleSizes = lodash.fromPairs([
...(await getWebpackSizes()),
...lodash.flatten(await Promise.all(rollupBundles.map(getRollupSize))),
...(await getNextPagesSize()),
]);

await fse.writeJSON(snapshotDestPath, bundleSizes, { spaces: 2 });
Expand Down
23 changes: 0 additions & 23 deletions scripts/sizeSnapshot/webpack.config.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
const fse = require('fs-extra');
const globCallback = require('glob');
const path = require('path');
const CompressionPlugin = require('compression-webpack-plugin');
Expand All @@ -9,18 +8,6 @@ const glob = promisify(globCallback);
const workspaceRoot = path.join(__dirname, '..', '..');

async function getSizeLimitBundles() {
const nextDir = path.join(workspaceRoot, 'docs/.next');
const buildId = await fse.readFile(path.join(nextDir, 'BUILD_ID'), 'utf8');

const dirname = path.join(nextDir, 'static/chunks');
const [main] = (await fse.readdir(dirname)).reduce((result, filename) => {
if (filename.length === 31) {
return [...result, { path: `${dirname}/${filename}` }];
}

return result;
}, []);

const corePackagePath = path.join(workspaceRoot, 'packages/material-ui/build/esm');
const coreComponents = (await glob(path.join(corePackagePath, '[A-Z]*'))).map(componentPath => {
const componentName = path.basename(componentPath);
Expand Down Expand Up @@ -95,16 +82,6 @@ async function getSizeLimitBundles() {
webpack: true,
path: 'packages/material-ui/build/esm/useMediaQuery/index.js',
},
{
name: 'docs.main',
webpack: false,
path: path.relative(workspaceRoot, main.path),
},
{
name: 'docs.landing',
webpack: false,
path: path.relative(workspaceRoot, path.join(nextDir, `static/${buildId}/pages/index.js`)),
},
];
}

Expand Down

0 comments on commit 743e26d

Please sign in to comment.