Skip to content

Commit 11659e2

Browse files
Initial commit
0 parents  commit 11659e2

6 files changed

Lines changed: 353 additions & 0 deletions

File tree

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
node_modules

WrappedPlugin/index.js

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
let idInc = 0;
2+
3+
const genPluginMethod = (orig, pluginName, smp, type) => function(method, func) {
4+
const id = idInc++;
5+
const timeEventName = type + "/" + method;
6+
7+
const wrappedFunc = (...args) => {
8+
smp.addTimeEvent("plugins", timeEventName, "start", {
9+
id,
10+
name: pluginName,
11+
});
12+
const ret = func.apply(
13+
this,
14+
args.map(a => wrap(a, pluginName, smp))
15+
);
16+
smp.addTimeEvent("plugins", timeEventName, "end", { id });
17+
return ret;
18+
};
19+
20+
return orig.plugin(method, wrappedFunc);
21+
};
22+
23+
const construcNamesToWrap = [
24+
"Compiler",
25+
"Compilation",
26+
"MainTemplate",
27+
"Parser",
28+
"NormalModuleFactory",
29+
"ContextModuleFactory",
30+
];
31+
32+
const wrap = (orig, pluginName, smp) => {
33+
const origConstrucName = orig && orig.constructor && orig.constructor.name;
34+
const shouldWrap = construcNamesToWrap.includes(origConstrucName);
35+
if(!shouldWrap) return orig;
36+
37+
const proxy = new Proxy(orig, {
38+
get: (target, property) => {
39+
if(property === "plugin")
40+
return genPluginMethod(orig, pluginName, smp, origConstrucName).bind(proxy);
41+
42+
if(typeof orig[property] === "function") return orig[property].bind(proxy);
43+
return wrap(orig[property], pluginName, smp);
44+
},
45+
set: (target, property, value) => {
46+
target[property] = value;
47+
},
48+
deleteProperty: (target, property) => {
49+
delete target[property];
50+
},
51+
});
52+
53+
return proxy;
54+
}
55+
56+
module.exports.WrappedPlugin = class WrappedPlugin {
57+
constructor(plugin, pluginName, smp) {
58+
this._smp_plugin = plugin;
59+
this._smp_pluginName = pluginName;
60+
this._smp = smp;
61+
62+
this.apply = this.apply.bind(this);
63+
}
64+
65+
apply(compiler) {
66+
return this._smp_plugin.apply(wrap(compiler, this._smp_pluginName, this._smp));
67+
}
68+
}

index.js

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
const { WrappedPlugin } = require("./WrappedPlugin");
2+
const { getModuleName, getLoaderNames } = require("./utils");
3+
const { getHumanOutput, getMiscOutput, getPluginsOutput, getLoadersOutput } = require("./output");
4+
5+
module.exports = class SpeedMeasurePlugin {
6+
constructor(options) {
7+
this.options = options || {};
8+
9+
this.timeEventData = {};
10+
11+
this.getOutput = this.getOutput.bind(this);
12+
this.addTimeEvent = this.addTimeEvent.bind(this);
13+
this.apply = this.apply.bind(this);
14+
}
15+
16+
static wrapPlugins(plugins, options) {
17+
const smp = new SpeedMeasurePlugin(options);
18+
19+
if(Array.isArray(plugins)) {
20+
let i = 1;
21+
plugins = plugins.reduce((acc, p) => ({
22+
...acc,
23+
["plugin " + (i++)]: p,
24+
}));
25+
}
26+
plugins = plugins || {};
27+
28+
const wrappedPlugins = Object
29+
.keys(plugins)
30+
.map(pluginName => new WrappedPlugin(plugins[pluginName], pluginName, smp));
31+
32+
return wrappedPlugins.concat(smp);
33+
}
34+
35+
getOutput() {
36+
const outputObj = {};
37+
if(this.timeEventData.misc)
38+
outputObj.misc = getMiscOutput(this.timeEventData.misc);
39+
if(this.timeEventData.plugins)
40+
outputObj.plugins = getPluginsOutput(this.timeEventData.plugins);
41+
if(this.timeEventData.loaders)
42+
outputObj.loaders = getLoadersOutput(this.timeEventData.loaders);
43+
44+
return this.options.outputFormat === "human"
45+
? getHumanOutput(outputObj)
46+
: JSON.stringify(outputObj, null, 2);
47+
}
48+
49+
addTimeEvent(category, event, eventType, data = {}) {
50+
const tED = this.timeEventData;
51+
if(!tED[category]) tED[category] = {};
52+
if(!tED[category][event]) tED[category][event] = [];
53+
const eventList = tED[category][event];
54+
const curTime = new Date().getTime();
55+
56+
if(eventType === "start") {
57+
eventList.push({
58+
start: curTime,
59+
...data,
60+
});
61+
} else if(eventType === "end") {
62+
const matchingEvent = eventList.find(e => !e.end && (e.id === data.id || e.name === data.name));
63+
const eventToModify = matchingEvent || eventList.find(e => !e.end);
64+
if(!eventToModify) {
65+
console.log("Could not find a matching event to end", category, event, data);
66+
throw new Error("No matching event!");
67+
}
68+
69+
eventToModify.end = curTime;
70+
}
71+
}
72+
73+
apply(compiler) {
74+
compiler.plugin("compile", () => {
75+
this.addTimeEvent("misc", "compile", "start", { watch: false });
76+
});
77+
compiler.plugin("done", () => {
78+
this.addTimeEvent("misc", "compile", "end");
79+
80+
const output = this.getOutput();
81+
if(this.options.outputTarget) {
82+
const writeMethod = fs.existsSync(this.options.outputTarget) ? fs.appendFile : fs.writeFile;
83+
writeMethod(this.options.outputTarget, output + "\n", err => {
84+
if(err) throw err;
85+
console.log("Outputted timing info to " + this.options.outputTarget);
86+
});
87+
} else {
88+
console.log(output);
89+
}
90+
91+
this.timeEventData = {};
92+
});
93+
94+
compiler.plugin("compilation", compilation => {
95+
compilation.plugin("build-module", module => {
96+
const name = getModuleName(module);
97+
if(name) {
98+
this.addTimeEvent("loaders", "build", "start", {
99+
name,
100+
loaders: getLoaderNames(module.loaders),
101+
});
102+
}
103+
});
104+
105+
compilation.plugin("succeed-module", module => {
106+
const name = getModuleName(module);
107+
if(name) {
108+
this.addTimeEvent("loaders", "build", "end", { name });
109+
}
110+
});
111+
});
112+
}
113+
}

output.js

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
const { groupBy, getAverages, getTotalActiveTime } = require("./utils");
2+
3+
const humanTime = ms => {
4+
const hours = ms / 3600000;
5+
const minutes = ms / 60000;
6+
const seconds = ms / 1000;
7+
8+
if(hours > 0.5) return hours.toFixed(2) + " hours";
9+
if(minutes > 0.5) return minutes.toFixed(2) + " minutes";
10+
if(seconds > 0.5) return seconds.toFixed(2) + " seconds";
11+
return ms.toFixed(0) + " milliseconds";
12+
};
13+
14+
const b = text => "\x1b[1m" + text + "\x1b[0m";
15+
const bb = text => "\x1b[1m\x1b[32m" + text + "\x1b[0m";
16+
17+
module.exports.getHumanOutput = outputObj => {
18+
const delim = "----------------------------";
19+
let output = delim + "\n";
20+
21+
if(outputObj.misc) {
22+
output += "General output time took " + bb(humanTime(outputObj.misc.compileTime));
23+
output += "\n\n";
24+
}
25+
if(outputObj.plugins) {
26+
Object.keys(outputObj.plugins).forEach(pluginName => {
27+
output += b(pluginName) + " took " + bb(humanTime(outputObj.plugins[pluginName]));
28+
output += "\n";
29+
});
30+
output += "\n";
31+
}
32+
if(outputObj.loaders) {
33+
outputObj.loaders.build.forEach(loaderObj => {
34+
output += loaderObj.loaders.map(b).join(", and \n") + " took " + bb(humanTime(loaderObj.activeTime));
35+
output += "\n";
36+
output += " Med = " + humanTime(loaderObj.averages.median) + ",\n";
37+
output += " x̄ = " + humanTime(loaderObj.averages.mean) + ",\n";
38+
if(typeof loaderObj.averages.variance === "number")
39+
output += " σ = " + humanTime(Math.sqrt(loaderObj.averages.variance)) + ", \n";
40+
output += " range = (" + humanTime(loaderObj.averages.range.start) + ", " + humanTime(loaderObj.averages.range.end) + "), \n";
41+
output += " n = " + loaderObj.averages.dataPoints + "\n";
42+
});
43+
}
44+
45+
output += delim + "\n";
46+
47+
return output;
48+
};
49+
50+
module.exports.getMiscOutput = data => ({
51+
compileTime: data.compile[0].end - data.compile[0].start
52+
});
53+
54+
module.exports.getPluginsOutput = data => Object.values(data).reduce((acc, inData) => {
55+
const startEndsByName = groupBy("name", inData);
56+
57+
return startEndsByName.reduce((innerAcc, startEnds) => ({
58+
...innerAcc,
59+
[startEnds[0].name]: (innerAcc[startEnds[0].name] || 0) + getTotalActiveTime(startEnds),
60+
}), acc);
61+
}, {});
62+
63+
module.exports.getLoadersOutput = data => Object.keys(data).reduce((acc, key) => {
64+
const startEndsByLoader = groupBy("loaders", data[key]);
65+
66+
return {
67+
...acc,
68+
[key]: startEndsByLoader.map(startEnds => {
69+
const averages = getAverages(startEnds);
70+
const activeTime = getTotalActiveTime(startEnds);
71+
72+
return {
73+
averages,
74+
activeTime,
75+
loaders: startEnds[0].loaders
76+
};
77+
}),
78+
};
79+
}, {});

package.json

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
{
2+
"name": "speed-measure-webpack-plugin",
3+
"version": "0.0.1",
4+
"description": "A Webpack plugin, to help measure the speed of your other loaders and plugins",
5+
"main": "index.js",
6+
"scripts": {
7+
"test": "echo \"Error: no test specified\" && exit 1"
8+
},
9+
"repository": {
10+
"type": "git",
11+
"url": "git+https://github.com/stephencookdev/speed-measure-webpack-plugin.git"
12+
},
13+
"author": "",
14+
"license": "ISC",
15+
"bugs": {
16+
"url": "https://github.com/stephencookdev/speed-measure-webpack-plugin/issues"
17+
},
18+
"homepage": "https://github.com/stephencookdev/speed-measure-webpack-plugin#readme"
19+
}

utils.js

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
const isEqual = (x, y) => Array.isArray(x)
2+
? Array.isArray(y) && x.every(xi => y.includes(xi)) && y.every(yi => x.includes(yi))
3+
: x === y;
4+
5+
const mergeRanges = rangeList => {
6+
const mergedQueue = [];
7+
const inputQueue = [...rangeList];
8+
while(inputQueue.length) {
9+
const cur = inputQueue.pop();
10+
const overlapIndex = mergedQueue.findIndex(item =>
11+
(item.start >= cur.start && item.start <= cur.end) ||
12+
(item.end >= cur.start && item.end <= cur.end)
13+
);
14+
15+
if(overlapIndex === -1) {
16+
mergedQueue.push(cur);
17+
} else {
18+
const toMerge = mergedQueue.splice(overlapIndex, 1)[0];
19+
inputQueue.push({
20+
start: Math.min(cur.start, cur.end, toMerge.start, toMerge.end),
21+
end: Math.max(cur.start, cur.end, toMerge.start, toMerge.end),
22+
});
23+
}
24+
};
25+
26+
return mergedQueue;
27+
};
28+
29+
const sqr = x => x * x;
30+
const mean = xs => xs.reduce((acc, x) => acc + x, 0) / xs.length;
31+
const median = xs => xs.sort()[Math.floor(xs.length / 2)];
32+
const variance = (xs, mean) => xs.reduce((acc, x) => acc + sqr(x - mean), 0) / (xs.length - 1);
33+
const range = xs => xs.reduce((acc, x) => ({
34+
start: Math.min(x, acc.start),
35+
end: Math.max(x, acc.end),
36+
}), { start: Number.POSITIVE_INFINITY, end: Number.NEGATIVE_INFINITY });
37+
38+
module.exports.getModuleName = module => module.userRequest;
39+
40+
module.exports.getLoaderNames = loaders => loaders.length
41+
? loaders
42+
.map(l => l.loader)
43+
.map(l => l.replace(/^.*\/node_modules\/([^\/]+).*$/, (_, m) => m))
44+
: ["(none)"];
45+
46+
module.exports.groupBy = (key, arr) => {
47+
const groups = [];
48+
arr.forEach(arrItem => {
49+
const groupItem = groups.find(poss => isEqual(poss[0][key], arrItem[key]));
50+
if(groupItem) groupItem.push(arrItem);
51+
else groups.push([arrItem]);
52+
});
53+
54+
return groups;
55+
};
56+
57+
module.exports.getAverages = group => {
58+
const durationList = group.map(cur => cur.end - cur.start);
59+
60+
const averages = {};
61+
averages.dataPoints = group.length;
62+
averages.median = median(durationList);
63+
averages.mean = Math.round(mean(durationList));
64+
averages.range = range(durationList);
65+
if(group.length > 1) averages.variance = Math.round(variance(durationList, averages.mean));
66+
67+
return averages;
68+
};
69+
70+
module.exports.getTotalActiveTime = group => {
71+
const mergedRanges = mergeRanges(group);
72+
return mergedRanges.reduce((acc, range) => acc + range.end - range.start, 0);
73+
};

0 commit comments

Comments
 (0)