Skip to content

Commit 0c05d65

Browse files
author
Pooya Parsa
committed
feat: build profiler
1 parent de75c9d commit 0c05d65

File tree

9 files changed

+256
-43
lines changed

9 files changed

+256
-43
lines changed

README.md

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,18 +13,31 @@
1313
<p>Elegant Progressbar for Webpack</p>
1414
</div>
1515

16-
✔ Display elegant progress bar while building or watch
17-
✔ Support of multiply concurrent builds (useful for SSR)
18-
✔ Pretty print filename and loaders
19-
✔ Windows compatible
20-
✔ Customizable
16+
✔ Display elegant progress bar while building or watch
17+
18+
✔ Support of multiply concurrent builds (useful for SSR)
19+
20+
✔ Pretty print filename and loaders
21+
22+
✔ Windows compatible
23+
24+
✔ Customizable
25+
26+
✔ Advanced build profiler
2127

2228
<div align="center">
2329
<br>
2430
<img src="./assets/screen1.png" width="70%">
31+
<p>Multi progress bars</p>
2532
<br>
2633
</div>
2734

35+
<div align="center">
36+
<br>
37+
<img src="./assets/screen2.png" width="50%">
38+
<p>Build Profiler</p>
39+
<br>
40+
</div>
2841

2942
<h2 align="center">Getting Started</h2>
3043

@@ -42,7 +55,7 @@ Using yarn:
4255
yarn add webpackbar
4356
```
4457

45-
Then add the reporter as a plugin to your webpack config.
58+
Then add the reporter as a plugin to your webpack config.
4659

4760
**webpack.config.js**
4861

@@ -76,6 +89,11 @@ Display name
7689

7790
Display color
7891

92+
### `profile`
93+
- Default: `false`
94+
95+
Enable profiler
96+
7997
<h2 align="center">Maintainers</h2>
8098

8199
<table>

assets/screen1.png

-829 Bytes
Loading

assets/screen2.png

134 KB
Loading

package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,9 @@
4242
"chalk": "^2.3.2",
4343
"figures": "^2.0.0",
4444
"lodash": "^4.17.5",
45-
"log-update": "^2.3.0"
45+
"log-update": "^2.3.0",
46+
"pretty-time": "^1.0.0",
47+
"table": "^4.0.3"
4648
},
4749
"devDependencies": {
4850
"babel-cli": "^6.26.0",

src/description.js

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import _ from 'lodash';
2+
3+
const DB = {
4+
loader: {
5+
get: loader => _.startCase(loader),
6+
},
7+
ext: {
8+
get: ext => `${ext} files`,
9+
vue: 'Vue Signle File components',
10+
js: 'JavaScript files',
11+
sass: 'SASS files',
12+
scss: 'SASS files',
13+
unknown: 'Unknown files',
14+
},
15+
};
16+
17+
export default function getDescription(category, keyword) {
18+
if (!DB[category]) {
19+
return _.startCase(keyword);
20+
}
21+
22+
if (DB[category][keyword]) {
23+
return DB[category][keyword];
24+
}
25+
26+
if (DB[category].get) {
27+
return DB[category].get(keyword);
28+
}
29+
30+
return '-';
31+
}

src/index.js

Lines changed: 44 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -2,44 +2,64 @@ import webpack from 'webpack';
22
import chalk from 'chalk';
33
import _ from 'lodash';
44
import logUpdate from 'log-update';
5-
import figures from 'figures';
6-
import { formatModule, str, renderBar } from './utils';
5+
import Profile from './profile';
6+
import { BULLET, parseRequst, formatRequest, renderBar, printStats } from './utils';
77

88
const sharedState = {};
99

10-
const B1 = figures('●');
10+
const defaults = { name: 'webpack', color: 'green', profile: false };
1111

1212
export default class WebpackBarPlugin extends webpack.ProgressPlugin {
13-
constructor(options = { name: 'webpack', color: 'green' }) {
14-
super(options);
15-
this.options = options;
13+
constructor(options) {
14+
super();
15+
16+
this.options = Object.assign({}, defaults, options);
1617

1718
this.handler = (percent, msg, ...details) => this.updateProgress(percent, msg, details);
18-
this.handler = _.throttle(this.handler, 25, { leading: true, trailing: true });
1919

20-
this.logUpdate = options.logUpdate || logUpdate;
20+
// Don't throttle when profiling
21+
if (!this.options.profile) {
22+
this.handler = _.throttle(this.handler, 25, { leading: true, trailing: true });
23+
}
24+
25+
this.logUpdate = this.options.logUpdate || logUpdate;
26+
27+
if (!sharedState[this.options.name]) {
28+
sharedState[this.options.name] = {
29+
color: this.options.color,
30+
profile: this.options.profile ? new Profile(this.options.name) : null,
31+
};
32+
}
2133
}
2234

2335
apply(compiler) {
2436
super.apply(compiler);
2537

26-
compiler.hooks.done.tap('progress', () => logUpdate.clear());
38+
compiler.hooks.done.tap('webpackbar', () => {
39+
logUpdate.clear();
40+
41+
if (this.options.profile) {
42+
const stats = sharedState[this.options.name].profile.getStats();
43+
printStats(stats);
44+
}
45+
});
2746
}
2847

2948
updateProgress(percent, msg, details) {
3049
const progress = Math.floor(percent * 100);
3150

32-
if (!sharedState[this.options.name]) {
33-
sharedState[this.options.name] = {
34-
color: this.options.color,
35-
};
36-
}
51+
Object.assign(sharedState[this.options.name], {
52+
progress,
53+
msg,
54+
details: details || [],
55+
request: parseRequst(details[2]),
56+
isRunning: (progress && progress !== 100) && (msg && msg.length),
57+
});
3758

38-
const thisState = sharedState[this.options.name];
39-
thisState.progress = progress;
40-
thisState.msg = msg;
41-
thisState.details = details || [];
42-
thisState.isRunning = (progress && progress !== 100) && (msg && msg.length);
59+
if (this.options.profile) {
60+
sharedState[this.options.name].profile
61+
.onRequest(sharedState[this.options.name].request);
62+
}
4363

4464
// Process all states
4565
let isRunning = false;
@@ -56,16 +76,16 @@ export default class WebpackBarPlugin extends webpack.ProgressPlugin {
5676
}
5777

5878
const lColor = chalk.keyword(state.color);
59-
const lIcon = lColor(B1);
79+
const lIcon = lColor(BULLET);
6080
const lName = lColor(_.startCase(name));
6181
const lBar = renderBar(state.progress, state.color);
6282
const lMsg = _.startCase(state.msg);
6383
const lProgress = `(${state.progress}%)`;
64-
const lDetail1 = chalk.grey(str(state.details[0]));
65-
const lDetail2 = chalk.grey(str(state.details[1]));
66-
const lModule = state.details[2] ? chalk.grey(` ${formatModule(state.details[2])}`) : '';
84+
const lDetail1 = chalk.grey(state.details[0] || '');
85+
const lDetail2 = chalk.grey(state.details[1] || '');
86+
const lRequest = formatRequest(state.request);
6787

68-
lines.push(`${[lIcon, lName, lBar, lMsg, lProgress, lDetail1, lDetail2].join(' ')}\n${lModule}`);
88+
lines.push(`${[lIcon, lName, lBar, lMsg, lProgress, lDetail1, lDetail2].join(' ')}\n ${lRequest}`);
6989
});
7090

7191
if (!isRunning) {

src/profile.js

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import path from 'path';
2+
3+
export default class Profile {
4+
constructor(name) {
5+
this.name = name;
6+
this.requests = [];
7+
}
8+
9+
onRequest(request) {
10+
if (this.requests.length) {
11+
this.requests[this.requests.length - 1].time = process.hrtime(this.requests[this.requests.length - 1].start);
12+
delete this.requests[this.requests.length - 1].start;
13+
}
14+
15+
this.requests.push({
16+
request,
17+
start: process.hrtime(),
18+
});
19+
}
20+
21+
getStats() {
22+
const loaderStats = {};
23+
const extStats = {};
24+
25+
const getStat = (stats, name) => {
26+
if (!stats[name]) {
27+
// eslint-disable-next-line no-param-reassign
28+
stats[name] = {
29+
count: 0,
30+
time: [0, 0],
31+
};
32+
}
33+
return stats[name];
34+
};
35+
36+
const addToStat = (stats, name, count, time) => {
37+
const stat = getStat(stats, name);
38+
stat.count += count;
39+
stat.time[0] += time[0];
40+
stat.time[1] += time[1];
41+
};
42+
43+
this.requests.forEach(({ request, time = [0, 0] }) => {
44+
request.loaders.forEach((loader) => {
45+
addToStat(loaderStats, loader, 1, time);
46+
});
47+
48+
const ext = request.file && path.extname(request.file).substr(1);
49+
addToStat(extStats, ext && ext.length ? ext : 'unknown', 1, time);
50+
});
51+
52+
return {
53+
ext: extStats,
54+
loader: loaderStats,
55+
};
56+
}
57+
}

src/utils.js

Lines changed: 69 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,19 @@ import path from 'path';
22
import chalk from 'chalk';
33
import _ from 'lodash';
44
import figures from 'figures';
5+
import { table } from 'table';
6+
import prettyTime from 'pretty-time';
7+
import getDescription from './description';
58

69
const BAR_LENGTH = 25;
710
const IS_WINDOWS = /^win/.test(process.platform);
811
const BLOCK_CHAR = IS_WINDOWS ? ' ' : '█';
912
const BLOCK_CHAR2 = IS_WINDOWS ? '=' : '█';
1013
const BAR_BEFORE = IS_WINDOWS ? '[' : '';
1114
const BAR_AFTER = IS_WINDOWS ? ']' : '';
12-
const NEXT = figures('›');
15+
const NEXT = chalk.blue(figures(' › '));
16+
17+
export const BULLET = figures('●');
1318

1419
export const renderBar = (progress, color) => {
1520
const w = progress * (BAR_LENGTH / 100);
@@ -21,17 +26,71 @@ export const renderBar = (progress, color) => {
2126
BAR_AFTER;
2227
};
2328

24-
export const friendlyName = (s) => {
25-
const match = /[a-z]+-loader/.exec(s);
26-
if (match) {
27-
return match[0];
29+
const hasValue = s => s && s.length;
30+
31+
const nodeModules = `${path.delimiter}node_modules${path.delimiter}`;
32+
const removeAfter = (delimiter, str) => _.first(str.split(delimiter));
33+
const removeBefore = (delimiter, str) => _.last(str.split(delimiter));
34+
35+
const firstMatch = (regex, str) => {
36+
const m = regex.exec(str);
37+
return m ? m[0] : null;
38+
};
39+
40+
export const parseRequst = (requestStr) => {
41+
const parts = (requestStr || '').split('!');
42+
43+
const file = path.relative(process.cwd(), removeAfter('?', removeBefore(nodeModules, (parts.pop()))));
44+
45+
const loaders = _.uniq(parts.map(part => firstMatch(/[a-z]+-loader/, part)).filter(hasValue));
46+
47+
return {
48+
file: hasValue(file) ? file : null,
49+
loaders,
50+
};
51+
};
52+
53+
export const formatRequest = (request) => {
54+
const loaders = request.loaders.join(NEXT);
55+
const format = chalk.grey;
56+
57+
if (!loaders.length) {
58+
return format(request.file || '');
2859
}
2960

30-
let f = path.relative(process.cwd(), s);
31-
f = _.last(f.split(`node_modules${path.delimiter}`));
32-
return f.split('?')[0];
61+
return format(`${loaders}${NEXT}${request.file}`);
3362
};
3463

35-
export const formatModule = s => s.split('!').map(friendlyName).join(NEXT);
64+
export const printStats = (allStats) => {
65+
Object.keys(allStats).forEach((category) => {
66+
const stats = allStats[category];
3667

37-
export const str = s => (s ? String(s) : '');
68+
process.stderr.write(`\nStats by ${chalk.bold(_.startCase(category))}\n`);
69+
70+
let totalRequests = 0;
71+
const totalTime = [0, 0];
72+
73+
const data = [
74+
[_.startCase(category), 'Requests', 'Time', 'Time/Request', 'Description'],
75+
];
76+
77+
Object.keys(stats).forEach((item) => {
78+
const stat = stats[item];
79+
80+
totalRequests += stat.count || 0;
81+
82+
const description = getDescription(category, item);
83+
84+
totalTime[0] += stat.time[0];
85+
totalTime[1] += stat.time[1];
86+
87+
const avgTime = [stat.time[0] / stat.count, stat.time[1] / stat.count];
88+
89+
data.push([item, stat.count || '-', prettyTime(stat.time), prettyTime(avgTime), description]);
90+
});
91+
92+
data.push(['Total', totalRequests, prettyTime(totalTime), '', '']);
93+
94+
process.stderr.write(`\n${table(data)}\n`);
95+
});
96+
};

0 commit comments

Comments
 (0)