Skip to content

Commit 6deec80

Browse files
Jasper De Moordevongovett
authored andcommitted
Log bundle metrics (#733)
1 parent d3ae5f6 commit 6deec80

12 files changed

Lines changed: 332 additions & 19 deletions

File tree

package.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,10 @@
2929
"cross-spawn": "^6.0.4",
3030
"cssnano": "^3.10.0",
3131
"dotenv": "^5.0.0",
32+
"filesize": "^3.6.0",
3233
"get-port": "^3.2.0",
3334
"glob": "^7.1.2",
35+
"grapheme-breaker": "^0.3.2",
3436
"htmlnano": "^0.1.6",
3537
"is-url": "^1.2.2",
3638
"js-yaml": "^3.10.0",
@@ -52,6 +54,7 @@
5254
"serialize-to-js": "^1.1.1",
5355
"serve-static": "^1.12.4",
5456
"source-map": "0.6.1",
57+
"strip-ansi": "^4.0.0",
5558
"toml": "^2.3.3",
5659
"tomlify-j0.4": "^3.0.0",
5760
"uglify-es": "^3.2.1",

src/Asset.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,8 @@ class Asset {
3737
this.parentBundle = null;
3838
this.bundles = new Set();
3939
this.cacheData = {};
40+
this.buildTime = 0;
41+
this.bundledSize = 0;
4042
}
4143

4244
shouldInvalidate() {

src/Bundle.js

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ class Bundle {
1818
this.siblingBundles = new Set();
1919
this.siblingBundlesMap = new Map();
2020
this.offsets = new Map();
21+
this.totalSize = 0;
22+
this.bundleTime = 0;
2123
}
2224

2325
static createWithAsset(asset, parentBundle) {
@@ -122,6 +124,7 @@ class Bundle {
122124
let Packager = bundler.packagers.get(this.type);
123125
let packager = new Packager(this, bundler);
124126

127+
let startTime = Date.now();
125128
await packager.start();
126129

127130
let included = new Set();
@@ -130,6 +133,11 @@ class Bundle {
130133
}
131134

132135
await packager.end();
136+
137+
this.bundleTime = Date.now() - startTime;
138+
for (let asset of this.assets) {
139+
this.bundleTime += asset.buildTime;
140+
}
133141
}
134142

135143
async _addDeps(asset, packager, included) {
@@ -144,6 +152,12 @@ class Bundle {
144152
}
145153

146154
await packager.addAsset(asset);
155+
this.addAssetSize(asset, packager.getSize() - this.totalSize);
156+
}
157+
158+
addAssetSize(asset, size) {
159+
asset.bundledSize = size;
160+
this.totalSize += size;
147161
}
148162

149163
getParents() {

src/Bundler.js

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ const config = require('./utils/config');
1616
const emoji = require('./utils/emoji');
1717
const loadEnv = require('./utils/env');
1818
const PromiseQueue = require('./utils/PromiseQueue');
19+
const bundleReport = require('./utils/bundleReport');
20+
const prettifyTime = require('./utils/prettifyTime');
1921

2022
/**
2123
* The Bundler is the main entry point. It resolves and loads assets,
@@ -93,7 +95,8 @@ class Bundler extends EventEmitter {
9395
typeof options.sourceMaps === 'boolean'
9496
? options.sourceMaps
9597
: !isProduction,
96-
hmrHostname: options.hmrHostname || ''
98+
hmrHostname: options.hmrHostname || '',
99+
detailedReport: options.detailedReport || false
97100
};
98101
}
99102

@@ -200,11 +203,11 @@ class Bundler extends EventEmitter {
200203
this.unloadOrphanedAssets();
201204

202205
let buildTime = Date.now() - startTime;
203-
let time =
204-
buildTime < 1000
205-
? `${buildTime}ms`
206-
: `${(buildTime / 1000).toFixed(2)}s`;
206+
let time = prettifyTime(buildTime);
207207
logger.status(emoji.success, `Built in ${time}.`, 'green');
208+
if (!this.watcher) {
209+
bundleReport(bundle, this.options.detailedReport);
210+
}
208211

209212
this.emit('bundled', bundle);
210213
return bundle;
@@ -378,6 +381,7 @@ class Bundler extends EventEmitter {
378381
asset.processed = true;
379382

380383
// First try the cache, otherwise load and compile in the background
384+
let startTime = Date.now();
381385
let processed = this.cache && (await this.cache.read(asset.name));
382386
if (!processed || asset.shouldInvalidate(processed.cacheData)) {
383387
processed = await this.farm.run(asset.name, asset.package, this.options);
@@ -386,6 +390,7 @@ class Bundler extends EventEmitter {
386390
}
387391
}
388392

393+
asset.buildTime = Date.now() - startTime;
389394
asset.generated = processed.generated;
390395
asset.hash = processed.hash;
391396

src/Logger.js

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ const chalk = require('chalk');
22
const readline = require('readline');
33
const prettyError = require('./utils/prettyError');
44
const emoji = require('./utils/emoji');
5+
const {countBreaks} = require('grapheme-breaker');
6+
const stripAnsi = require('strip-ansi');
57

68
class Logger {
79
constructor(options) {
@@ -127,6 +129,46 @@ class Logger {
127129
_log(message) {
128130
console.log(message);
129131
}
132+
133+
table(columns, table) {
134+
// Measure column widths
135+
let colWidths = [];
136+
for (let row of table) {
137+
let i = 0;
138+
for (let item of row) {
139+
colWidths[i] = Math.max(colWidths[i] || 0, stringWidth(item));
140+
i++;
141+
}
142+
}
143+
144+
// Render rows
145+
for (let row of table) {
146+
let items = row.map((item, i) => {
147+
// Add padding between columns unless the alignment is the opposite to the
148+
// next column and pad to the column width.
149+
let padding =
150+
!columns[i + 1] || columns[i + 1].align === columns[i].align ? 4 : 0;
151+
return pad(item, colWidths[i] + padding, columns[i].align);
152+
});
153+
154+
this.log(items.join(''));
155+
}
156+
}
157+
}
158+
159+
// Pad a string with spaces on either side
160+
function pad(text, length, align = 'left') {
161+
let pad = ' '.repeat(length - stringWidth(text));
162+
if (align === 'right') {
163+
return pad + text;
164+
}
165+
166+
return text + pad;
167+
}
168+
169+
// Count visible characters in a string
170+
function stringWidth(string) {
171+
return countBreaks(stripAnsi('' + string));
130172
}
131173

132174
// If we are in a worker, make a proxy class which will

src/cli.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,10 @@ program
9494
'set the runtime environment, either "node", "browser" or "electron". defaults to "browser"',
9595
/^(node|browser|electron)$/
9696
)
97+
.option(
98+
'--detailed-report',
99+
'print a detailed build report after a completed build'
100+
)
97101
.action(bundle);
98102

99103
program

src/packagers/Packager.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,10 @@ class Packager {
2222
throw new Error('Must be implemented by subclasses');
2323
}
2424

25+
getSize() {
26+
return this.dest.bytesWritten;
27+
}
28+
2529
async end() {
2630
await this.dest.end();
2731
}

src/packagers/RawPackager.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,14 @@ class RawPackager extends Packager {
2323
contents = await fs.readFile(contents ? contents.path : asset.name);
2424
}
2525

26+
this.size = contents.length;
2627
await fs.writeFile(name, contents);
2728
}
2829

30+
getSize() {
31+
return this.size || 0;
32+
}
33+
2934
end() {}
3035
}
3136

src/transforms/uglify.js

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
const {minify} = require('uglify-es');
2-
const logger = require('../Logger');
32

43
module.exports = async function(asset) {
54
await asset.parseIfNeeded();
@@ -24,13 +23,6 @@ module.exports = async function(asset) {
2423
throw result.error;
2524
}
2625

27-
// Log all warnings
28-
if (result.warnings) {
29-
result.warnings.forEach(warning => {
30-
logger.warn('[uglify] ' + warning);
31-
});
32-
}
33-
3426
// babel-generator did our code generation for us, so remove the old AST
3527
asset.ast = null;
3628
asset.outputCode = result.code;

src/utils/bundleReport.js

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
const path = require('path');
2+
const prettifyTime = require('./prettifyTime');
3+
const logger = require('../Logger');
4+
const emoji = require('./emoji');
5+
const filesize = require('filesize');
6+
7+
const LARGE_BUNDLE_SIZE = 1024 * 1024;
8+
const NUM_LARGE_ASSETS = 10;
9+
const COLUMNS = [
10+
{align: 'left'}, // name
11+
{align: 'right'}, // size
12+
{align: 'right'} // time
13+
];
14+
15+
function bundleReport(mainBundle, detailed = false) {
16+
// Get a list of bundles sorted by size
17+
let bundles = Array.from(iterateBundles(mainBundle)).sort(
18+
(a, b) => b.totalSize - a.totalSize
19+
);
20+
let rows = [];
21+
22+
for (let bundle of bundles) {
23+
// Add a row for the bundle
24+
rows.push([
25+
formatFilename(bundle.name, logger.chalk.cyan.bold),
26+
logger.chalk.bold(
27+
prettifySize(bundle.totalSize, bundle.totalSize > LARGE_BUNDLE_SIZE)
28+
),
29+
logger.chalk.green.bold(prettifyTime(bundle.bundleTime))
30+
]);
31+
32+
// If detailed, generate a list of the top 10 largest assets in the bundle
33+
if (detailed && bundle.assets.size > 1) {
34+
let assets = Array.from(bundle.assets)
35+
.filter(a => a.type === bundle.type)
36+
.sort((a, b) => b.bundledSize - a.bundledSize);
37+
38+
let largestAssets = assets.slice(0, NUM_LARGE_ASSETS);
39+
for (let asset of largestAssets) {
40+
// Add a row for the asset.
41+
rows.push([
42+
(asset == assets[assets.length - 1] ? '└── ' : '├── ') +
43+
formatFilename(asset.name, logger.chalk.reset),
44+
logger.chalk.dim(prettifySize(asset.bundledSize)),
45+
logger.chalk.dim(logger.chalk.green(prettifyTime(asset.buildTime)))
46+
]);
47+
}
48+
49+
// Show how many more assets there are
50+
if (assets.length > largestAssets.length) {
51+
rows.push([
52+
'└── ' +
53+
logger.chalk.dim(
54+
`+ ${assets.length - largestAssets.length} more assets`
55+
)
56+
]);
57+
}
58+
59+
// If this isn't the last bundle, add an empty row before the next one
60+
if (bundle !== bundles[bundles.length - 1]) {
61+
rows.push([]);
62+
}
63+
}
64+
}
65+
66+
// Render table
67+
logger.log('');
68+
logger.table(COLUMNS, rows);
69+
}
70+
71+
module.exports = bundleReport;
72+
73+
function* iterateBundles(bundle) {
74+
yield bundle;
75+
for (let child of bundle.childBundles) {
76+
yield* iterateBundles(child);
77+
}
78+
}
79+
80+
function prettifySize(size, isLarge) {
81+
let res = filesize(size);
82+
if (isLarge) {
83+
res = logger.chalk.yellow(emoji.warning + ' ' + res);
84+
} else {
85+
res = logger.chalk.magenta(res);
86+
}
87+
88+
return res;
89+
}
90+
91+
function formatFilename(filename, color = logger.chalk.reset) {
92+
let dir = path.relative(process.cwd(), path.dirname(filename));
93+
return (
94+
logger.chalk.dim(dir + (dir ? path.sep : '')) +
95+
color(path.basename(filename))
96+
);
97+
}

0 commit comments

Comments
 (0)