Skip to content

Commit 5fba3cc

Browse files
Granular loader data (#17)
Add in an experimental mode of granular loader timing data
1 parent 1615741 commit 5fba3cc

9 files changed

Lines changed: 3041 additions & 37 deletions

File tree

README.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,8 @@ const webpackConfig = smp.wrap({
5353
});
5454
```
5555

56+
and you're done! SMP will now be printing timing output to the console by default
57+
5658
## Options
5759

5860
Options are (optionally) passed in to the constructor
@@ -87,3 +89,17 @@ Type: `Boolean`<br>
8789
Default: `false`
8890

8991
If truthy, this plugin does nothing at all. It is recommended to set this with something similar to `{ disable: !process.env.MEASURE }` to allow opt-in measurements with a `MEASURE=true npm run build`
92+
93+
### `options.granularLoaderData` _(experimental)_
94+
95+
Type: `Boolean`<br>
96+
Default: `false`
97+
98+
If truthy, this plugin will attempt to break down the loader timing data to give per-loader timing information.
99+
100+
Points of note that the following loaders will have inaccurate results in this mode:
101+
102+
* loaders using separate processes (e.g. `thread-loader`) - these make it difficult to get timing information on the subsequent loaders, as they're not attached to the main thread
103+
* loaders emitting file output (e.g. `file-loader`) - the time taken in outputting the actual file is not included in the running time of the loader
104+
105+
These are restrictions from technical limitations - ideally we would find solutions to these problems before removing the _(experimental)_ flag on this options

WrappedPlugin/index.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,9 @@ let idInc = 0;
22

33
const genPluginMethod = (orig, pluginName, smp, type) =>
44
function(method, func) {
5+
const timeEventName = pluginName + "/" + type + "/" + method;
56
const wrappedFunc = (...args) => {
67
const id = idInc++;
7-
const timeEventName = pluginName + "/" + type + "/" + method;
88
// we don't know if there's going to be a callback applied to a particular
99
// call, so we just set it multiple times, letting each one override the last
1010
let endCallCount = 0;

index.js

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1+
const path = require("path");
12
const fs = require("fs");
23
const { WrappedPlugin } = require("./WrappedPlugin");
3-
const { getModuleName, getLoaderNames } = require("./utils");
4+
const { getModuleName, getLoaderNames, prependLoader } = require("./utils");
45
const {
56
getHumanOutput,
67
getMiscOutput,
@@ -9,6 +10,8 @@ const {
910
} = require("./output");
1011
const { stripColours } = require("./colours");
1112

13+
const NS = path.dirname(fs.realpathSync(__filename));
14+
1215
module.exports = class SpeedMeasurePlugin {
1316
constructor(options) {
1417
this.options = options || {};
@@ -20,18 +23,24 @@ module.exports = class SpeedMeasurePlugin {
2023
this.getOutput = this.getOutput.bind(this);
2124
this.addTimeEvent = this.addTimeEvent.bind(this);
2225
this.apply = this.apply.bind(this);
26+
this.provideLoaderTiming = this.provideLoaderTiming.bind(this);
2327
}
2428

2529
wrap(config) {
2630
if (this.options.disable) return config;
2731

2832
config.plugins = (config.plugins || []).map(plugin => {
29-
const pluginName = (plugin.constructor && plugin.constructor.name) ||
33+
const pluginName =
34+
(plugin.constructor && plugin.constructor.name) ||
3035
"(unable to deduce plugin name)";
3136
return new WrappedPlugin(plugin, pluginName, this);
3237
});
3338

34-
if(!this.smpPluginAdded) {
39+
if (config.module && this.options.granularLoaderData) {
40+
config.module = prependLoader(config.module);
41+
}
42+
43+
if (!this.smpPluginAdded) {
3544
config.plugins = config.plugins.concat(this);
3645
this.smpPluginAdded = true;
3746
}
@@ -78,7 +87,7 @@ module.exports = class SpeedMeasurePlugin {
7887
const eventToModify =
7988
matchingEvent || (data.fillLast && eventList.find(e => !e.end));
8089
if (!eventToModify) {
81-
console.log(
90+
console.error(
8291
"Could not find a matching event to end",
8392
category,
8493
event,
@@ -119,6 +128,10 @@ module.exports = class SpeedMeasurePlugin {
119128
});
120129

121130
compiler.plugin("compilation", compilation => {
131+
compilation.plugin("normal-module-loader", loaderContext => {
132+
loaderContext[NS] = this.provideLoaderTiming;
133+
});
134+
122135
compilation.plugin("build-module", module => {
123136
const name = getModuleName(module);
124137
if (name) {
@@ -141,4 +154,14 @@ module.exports = class SpeedMeasurePlugin {
141154
});
142155
});
143156
}
157+
158+
provideLoaderTiming(info) {
159+
const infoData = { id: info.id };
160+
if(info.type !== "end") {
161+
infoData.loader = info.loaderName;
162+
infoData.name = info.module;
163+
}
164+
165+
this.addTimeEvent("loaders", "build-specific", info.type, infoData);
166+
}
144167
};

loader.js

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
const path = require("path");
2+
const fs = require("fs");
3+
const { hackWrapLoaders } = require("./utils");
4+
5+
let id = 0;
6+
7+
const NS = path.dirname(fs.realpathSync(__filename));
8+
9+
const getLoaderName = path => {
10+
const nodeModuleName = /\/node_modules\/([^\/]+)/.exec(path);
11+
return (nodeModuleName && nodeModuleName[1]) || "";
12+
};
13+
14+
module.exports.pitch = function() {
15+
const callback = this[NS];
16+
const module = this.resourcePath;
17+
const loaderPaths = this.loaders
18+
.map(l => l.path)
19+
.filter(l => !l.includes("speed-measure-webpack-plugin"));
20+
21+
// Hack ourselves to overwrite the `require` method so we can override the
22+
// loadLoaders
23+
hackWrapLoaders(loaderPaths, (loader, path) => {
24+
const loaderName = getLoaderName(path);
25+
const wrapFunc = func =>
26+
function() {
27+
const loaderId = id++;
28+
const almostThis = Object.assign({}, this, {
29+
async: function() {
30+
const asyncCallback = this.async.apply(this, arguments);
31+
32+
return function() {
33+
callback({
34+
id: loaderId,
35+
type: "end",
36+
});
37+
return asyncCallback.apply(this, arguments);
38+
};
39+
}.bind(this)
40+
});
41+
42+
callback({
43+
module,
44+
loaderName,
45+
id: loaderId,
46+
type: "start",
47+
});
48+
const ret = func.apply(almostThis, arguments);
49+
callback({
50+
id: loaderId,
51+
type: "end",
52+
});
53+
return ret;
54+
};
55+
56+
if (loader.normal) loader.normal = wrapFunc(loader.normal);
57+
if (loader.default) loader.default = wrapFunc(loader.default);
58+
if (loader.pitch) loader.pitch = wrapFunc(loader.pitch);
59+
if (typeof loader === "function") return wrapFunc(loader);
60+
return loader;
61+
});
62+
};

output.js

Lines changed: 55 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -67,24 +67,38 @@ module.exports.getHumanOutput = (outputObj, options = {}) => {
6767
fg(hT(loaderObj.activeTime), loaderObj.activeTime) +
6868
"\n";
6969

70+
let xEqualsY = [];
7071
if (options.verbose) {
71-
output +=
72-
" median = " + hT(loaderObj.averages.median) + ",\n";
73-
output += " mean = " + hT(loaderObj.averages.mean) + ",\n";
72+
xEqualsY.push(["median", hT(loaderObj.averages.median)]);
73+
xEqualsY.push(["mean", hT(loaderObj.averages.mean)]);
7474
if (typeof loaderObj.averages.variance === "number")
75-
output +=
76-
" s.d = " +
77-
hT(Math.sqrt(loaderObj.averages.variance)) +
78-
", \n";
79-
output +=
80-
" range = (" +
81-
hT(loaderObj.averages.range.start) +
82-
" --> " +
83-
hT(loaderObj.averages.range.end) +
84-
"), \n";
75+
xEqualsY.push(["s.d.", hT(Math.sqrt(loaderObj.averages.variance))]);
76+
xEqualsY.push([
77+
"range",
78+
"(" +
79+
hT(loaderObj.averages.range.start) +
80+
" --> " +
81+
hT(loaderObj.averages.range.end) +
82+
")",
83+
]);
8584
}
8685

87-
output += " module count = " + loaderObj.averages.dataPoints + "\n";
86+
if (loaderObj.loaders.length > 1) {
87+
Object.keys(loaderObj.subLoadersTime).forEach(subLoader => {
88+
xEqualsY.push([subLoader, hT(loaderObj.subLoadersTime[subLoader])]);
89+
});
90+
}
91+
92+
xEqualsY.push(["module count", loaderObj.averages.dataPoints]);
93+
94+
const maxXLength = xEqualsY.reduce(
95+
(acc, cur) => Math.max(acc, cur[0].length),
96+
0
97+
);
98+
xEqualsY.forEach(xY => {
99+
const padEnd = maxXLength - xY[0].length;
100+
output += " " + xY[0] + " ".repeat(padEnd) + " = " + xY[1] + "\n";
101+
});
88102
});
89103
}
90104

@@ -100,6 +114,7 @@ module.exports.getMiscOutput = data => ({
100114
module.exports.getPluginsOutput = data =>
101115
Object.keys(data).reduce((acc, key) => {
102116
const inData = data[key];
117+
103118
const startEndsByName = groupBy("name", inData);
104119

105120
return startEndsByName.reduce((innerAcc, startEnds) => {
@@ -109,20 +124,29 @@ module.exports.getPluginsOutput = data =>
109124
}, acc);
110125
}, {});
111126

112-
module.exports.getLoadersOutput = data =>
113-
Object.keys(data).reduce((acc, key) => {
114-
const startEndsByLoader = groupBy("loaders", data[key]);
115-
116-
acc[key] = startEndsByLoader.map(startEnds => {
117-
const averages = getAverages(startEnds);
118-
const activeTime = getTotalActiveTime(startEnds);
119-
120-
return {
121-
averages,
122-
activeTime,
123-
loaders: startEnds[0].loaders,
124-
};
125-
});
126-
127-
return acc;
128-
}, {});
127+
module.exports.getLoadersOutput = data => {
128+
const startEndsByLoader = groupBy("loaders", data.build);
129+
const allSubLoaders = data["build-specific"] || [];
130+
131+
const buildData = startEndsByLoader.map(startEnds => {
132+
const averages = getAverages(startEnds);
133+
const activeTime = getTotalActiveTime(startEnds);
134+
const subLoaders = groupBy(
135+
"loader",
136+
allSubLoaders.filter(l => startEnds.find(x => x.name === l.name))
137+
);
138+
const subLoadersActiveTime = subLoaders.reduce((acc, loaders) => {
139+
acc[loaders[0].loader] = getTotalActiveTime(loaders);
140+
return acc;
141+
}, {});
142+
143+
return {
144+
averages,
145+
activeTime,
146+
loaders: startEnds[0].loaders,
147+
subLoadersTime: subLoadersActiveTime,
148+
};
149+
});
150+
151+
return { build: buildData };
152+
};

package.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@
33
"version": "1.0.0",
44
"description": "Measure + analyse the speed of your webpack loaders and plugins",
55
"main": "index.js",
6+
"scripts": {
7+
"test": "jest"
8+
},
69
"repository": {
710
"type": "git",
811
"url": "git+https://github.com/stephencookdev/speed-measure-webpack-plugin.git"
@@ -24,6 +27,7 @@
2427
"webpack": "^3"
2528
},
2629
"devDependencies": {
30+
"jest": "^22.3.0",
2731
"prettier": "1.10.2"
2832
}
2933
}

utils.js

Lines changed: 55 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,11 +51,12 @@ module.exports.getLoaderNames = loaders =>
5151
? loaders
5252
.map(l => l.loader)
5353
.map(l => l.replace(/^.*\/node_modules\/([^\/]+).*$/, (_, m) => m))
54+
.filter(l => !l.includes("speed-measure-webpack-plugin"))
5455
: ["modules with no loaders"];
5556

5657
module.exports.groupBy = (key, arr) => {
5758
const groups = [];
58-
arr.forEach(arrItem => {
59+
(arr || []).forEach(arrItem => {
5960
const groupItem = groups.find(poss => isEqual(poss[0][key], arrItem[key]));
6061
if (groupItem) groupItem.push(arrItem);
6162
else groups.push([arrItem]);
@@ -82,3 +83,56 @@ module.exports.getTotalActiveTime = group => {
8283
const mergedRanges = mergeRanges(group);
8384
return mergedRanges.reduce((acc, range) => acc + range.end - range.start, 0);
8485
};
86+
87+
const prependLoader = rules => {
88+
if (!rules) return rules;
89+
if (Array.isArray(rules)) return rules.map(prependLoader);
90+
91+
if (rules.loader) {
92+
rules.use = [rules.loader];
93+
delete rules.loader;
94+
}
95+
96+
if (rules.use) {
97+
rules.use.unshift("speed-measure-webpack-plugin/loader");
98+
}
99+
100+
if (rules.oneOf) {
101+
rules.oneOf = prependLoader(rules.oneOf);
102+
}
103+
if (rules.rules) {
104+
rules.rules = prependLoader(rules.rules);
105+
}
106+
if (Array.isArray(rules.resource)) {
107+
rules.resource = prependLoader(rules.resource);
108+
}
109+
if (rules.resource && rules.resource.and) {
110+
rules.resource.and = prependLoader(rules.resource.and);
111+
}
112+
if (rules.resource && rules.resource.or) {
113+
rules.resource.or = prependLoader(rules.resource.or);
114+
}
115+
116+
return rules;
117+
};
118+
module.exports.prependLoader = prependLoader;
119+
120+
module.exports.hackWrapLoaders = (loaderPaths, callback) => {
121+
const wrapReq = reqMethod => {
122+
return function() {
123+
const ret = reqMethod.apply(this, arguments);
124+
if (loaderPaths.includes(arguments[0])) {
125+
if(ret.__smpHacked) return ret;
126+
ret.__smpHacked = true;
127+
return callback(ret, arguments[0]);
128+
}
129+
return ret;
130+
};
131+
};
132+
133+
if (typeof System === "object" && typeof System.import === "function") {
134+
System.import = wrapReq(System.import);
135+
}
136+
const Module = require("module");
137+
Module.prototype.require = wrapReq(Module.prototype.require);
138+
};

0 commit comments

Comments
 (0)