Skip to content

Commit

Permalink
feat: build profiler
Browse files Browse the repository at this point in the history
  • Loading branch information
Pooya Parsa committed Mar 24, 2018
1 parent de75c9d commit 0c05d65
Show file tree
Hide file tree
Showing 9 changed files with 256 additions and 43 deletions.
30 changes: 24 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,18 +13,31 @@
<p>Elegant Progressbar for Webpack</p>
</div>

✔ Display elegant progress bar while building or watch
✔ Support of multiply concurrent builds (useful for SSR)
✔ Pretty print filename and loaders
✔ Windows compatible
✔ Customizable
✔ Display elegant progress bar while building or watch

✔ Support of multiply concurrent builds (useful for SSR)

✔ Pretty print filename and loaders

✔ Windows compatible

✔ Customizable

✔ Advanced build profiler

<div align="center">
<br>
<img src="./assets/screen1.png" width="70%">
<p>Multi progress bars</p>
<br>
</div>

<div align="center">
<br>
<img src="./assets/screen2.png" width="50%">
<p>Build Profiler</p>
<br>
</div>

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

Expand All @@ -42,7 +55,7 @@ Using yarn:
yarn add webpackbar
```

Then add the reporter as a plugin to your webpack config.
Then add the reporter as a plugin to your webpack config.

**webpack.config.js**

Expand Down Expand Up @@ -76,6 +89,11 @@ Display name

Display color

### `profile`
- Default: `false`

Enable profiler

<h2 align="center">Maintainers</h2>

<table>
Expand Down
Binary file modified assets/screen1.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added assets/screen2.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,9 @@
"chalk": "^2.3.2",
"figures": "^2.0.0",
"lodash": "^4.17.5",
"log-update": "^2.3.0"
"log-update": "^2.3.0",
"pretty-time": "^1.0.0",
"table": "^4.0.3"
},
"devDependencies": {
"babel-cli": "^6.26.0",
Expand Down
31 changes: 31 additions & 0 deletions src/description.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import _ from 'lodash';

const DB = {
loader: {
get: loader => _.startCase(loader),
},
ext: {
get: ext => `${ext} files`,
vue: 'Vue Signle File components',
js: 'JavaScript files',
sass: 'SASS files',
scss: 'SASS files',
unknown: 'Unknown files',
},
};

export default function getDescription(category, keyword) {
if (!DB[category]) {
return _.startCase(keyword);
}

if (DB[category][keyword]) {
return DB[category][keyword];
}

if (DB[category].get) {
return DB[category].get(keyword);
}

return '-';
}
68 changes: 44 additions & 24 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,44 +2,64 @@ import webpack from 'webpack';
import chalk from 'chalk';
import _ from 'lodash';
import logUpdate from 'log-update';
import figures from 'figures';
import { formatModule, str, renderBar } from './utils';
import Profile from './profile';
import { BULLET, parseRequst, formatRequest, renderBar, printStats } from './utils';

const sharedState = {};

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

export default class WebpackBarPlugin extends webpack.ProgressPlugin {
constructor(options = { name: 'webpack', color: 'green' }) {
super(options);
this.options = options;
constructor(options) {
super();

this.options = Object.assign({}, defaults, options);

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

this.logUpdate = options.logUpdate || logUpdate;
// Don't throttle when profiling
if (!this.options.profile) {
this.handler = _.throttle(this.handler, 25, { leading: true, trailing: true });
}

this.logUpdate = this.options.logUpdate || logUpdate;

if (!sharedState[this.options.name]) {
sharedState[this.options.name] = {
color: this.options.color,
profile: this.options.profile ? new Profile(this.options.name) : null,
};
}
}

apply(compiler) {
super.apply(compiler);

compiler.hooks.done.tap('progress', () => logUpdate.clear());
compiler.hooks.done.tap('webpackbar', () => {
logUpdate.clear();

if (this.options.profile) {
const stats = sharedState[this.options.name].profile.getStats();
printStats(stats);
}
});
}

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

if (!sharedState[this.options.name]) {
sharedState[this.options.name] = {
color: this.options.color,
};
}
Object.assign(sharedState[this.options.name], {
progress,
msg,
details: details || [],
request: parseRequst(details[2]),
isRunning: (progress && progress !== 100) && (msg && msg.length),
});

const thisState = sharedState[this.options.name];
thisState.progress = progress;
thisState.msg = msg;
thisState.details = details || [];
thisState.isRunning = (progress && progress !== 100) && (msg && msg.length);
if (this.options.profile) {
sharedState[this.options.name].profile
.onRequest(sharedState[this.options.name].request);
}

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

const lColor = chalk.keyword(state.color);
const lIcon = lColor(B1);
const lIcon = lColor(BULLET);
const lName = lColor(_.startCase(name));
const lBar = renderBar(state.progress, state.color);
const lMsg = _.startCase(state.msg);
const lProgress = `(${state.progress}%)`;
const lDetail1 = chalk.grey(str(state.details[0]));
const lDetail2 = chalk.grey(str(state.details[1]));
const lModule = state.details[2] ? chalk.grey(` ${formatModule(state.details[2])}`) : '';
const lDetail1 = chalk.grey(state.details[0] || '');
const lDetail2 = chalk.grey(state.details[1] || '');
const lRequest = formatRequest(state.request);

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

if (!isRunning) {
Expand Down
57 changes: 57 additions & 0 deletions src/profile.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import path from 'path';

export default class Profile {
constructor(name) {
this.name = name;
this.requests = [];
}

onRequest(request) {
if (this.requests.length) {
this.requests[this.requests.length - 1].time = process.hrtime(this.requests[this.requests.length - 1].start);
delete this.requests[this.requests.length - 1].start;
}

this.requests.push({
request,
start: process.hrtime(),
});
}

getStats() {
const loaderStats = {};
const extStats = {};

const getStat = (stats, name) => {
if (!stats[name]) {
// eslint-disable-next-line no-param-reassign
stats[name] = {
count: 0,
time: [0, 0],
};
}
return stats[name];
};

const addToStat = (stats, name, count, time) => {
const stat = getStat(stats, name);
stat.count += count;
stat.time[0] += time[0];
stat.time[1] += time[1];
};

this.requests.forEach(({ request, time = [0, 0] }) => {
request.loaders.forEach((loader) => {
addToStat(loaderStats, loader, 1, time);
});

const ext = request.file && path.extname(request.file).substr(1);
addToStat(extStats, ext && ext.length ? ext : 'unknown', 1, time);
});

return {
ext: extStats,
loader: loaderStats,
};
}
}
79 changes: 69 additions & 10 deletions src/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,19 @@ import path from 'path';
import chalk from 'chalk';
import _ from 'lodash';
import figures from 'figures';
import { table } from 'table';
import prettyTime from 'pretty-time';
import getDescription from './description';

const BAR_LENGTH = 25;
const IS_WINDOWS = /^win/.test(process.platform);
const BLOCK_CHAR = IS_WINDOWS ? ' ' : '█';
const BLOCK_CHAR2 = IS_WINDOWS ? '=' : '█';
const BAR_BEFORE = IS_WINDOWS ? '[' : '';
const BAR_AFTER = IS_WINDOWS ? ']' : '';
const NEXT = figures('›');
const NEXT = chalk.blue(figures(' › '));

export const BULLET = figures('●');

export const renderBar = (progress, color) => {
const w = progress * (BAR_LENGTH / 100);
Expand All @@ -21,17 +26,71 @@ export const renderBar = (progress, color) => {
BAR_AFTER;
};

export const friendlyName = (s) => {
const match = /[a-z]+-loader/.exec(s);
if (match) {
return match[0];
const hasValue = s => s && s.length;

const nodeModules = `${path.delimiter}node_modules${path.delimiter}`;
const removeAfter = (delimiter, str) => _.first(str.split(delimiter));
const removeBefore = (delimiter, str) => _.last(str.split(delimiter));

const firstMatch = (regex, str) => {
const m = regex.exec(str);
return m ? m[0] : null;
};

export const parseRequst = (requestStr) => {
const parts = (requestStr || '').split('!');

const file = path.relative(process.cwd(), removeAfter('?', removeBefore(nodeModules, (parts.pop()))));

const loaders = _.uniq(parts.map(part => firstMatch(/[a-z]+-loader/, part)).filter(hasValue));

return {
file: hasValue(file) ? file : null,
loaders,
};
};

export const formatRequest = (request) => {
const loaders = request.loaders.join(NEXT);
const format = chalk.grey;

if (!loaders.length) {
return format(request.file || '');
}

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

export const formatModule = s => s.split('!').map(friendlyName).join(NEXT);
export const printStats = (allStats) => {
Object.keys(allStats).forEach((category) => {
const stats = allStats[category];

export const str = s => (s ? String(s) : '');
process.stderr.write(`\nStats by ${chalk.bold(_.startCase(category))}\n`);

let totalRequests = 0;
const totalTime = [0, 0];

const data = [
[_.startCase(category), 'Requests', 'Time', 'Time/Request', 'Description'],
];

Object.keys(stats).forEach((item) => {
const stat = stats[item];

totalRequests += stat.count || 0;

const description = getDescription(category, item);

totalTime[0] += stat.time[0];
totalTime[1] += stat.time[1];

const avgTime = [stat.time[0] / stat.count, stat.time[1] / stat.count];

data.push([item, stat.count || '-', prettyTime(stat.time), prettyTime(avgTime), description]);
});

data.push(['Total', totalRequests, prettyTime(totalTime), '', '']);

process.stderr.write(`\n${table(data)}\n`);
});
};
Loading

0 comments on commit 0c05d65

Please sign in to comment.