Skip to content

Commit ba0fdad

Browse files
authored
Merge pull request webpack#6322 from samccone/feature/profiling-plugin
Add profiling plugin to webpack
2 parents 8b888fe + d235a61 commit ba0fdad

File tree

7 files changed

+491
-0
lines changed

7 files changed

+491
-0
lines changed

lib/debug/ProfilingPlugin.js

Lines changed: 388 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,388 @@
1+
const fs = require("fs");
2+
const Trace = require("chrome-trace-event").Tracer;
3+
let inspector = undefined;
4+
5+
try {
6+
inspector = require("inspector"); // eslint-disable-line node/no-missing-require
7+
} catch(e) {
8+
console.log("Unable to CPU profile in < node 8.0");
9+
}
10+
11+
// TODO: Add this to webpack.js.org docs for this plugin, and for profiling build times
12+
/**
13+
* How this plugin works: (placeholder until in docs)
14+
*
15+
* In chrome, open the `Profile Tab`, when you run webpack,
16+
* this plugin will output an events.json file that you
17+
* can drag and drop into the profiler. It will then display timeline stats and calls per plugin!
18+
*
19+
* Example: https://chromedevtools.github.io/timeline-viewer/?url=https%3A%2F%2Fgist.githubusercontent.com%2FTheLarkInn%2Fb94b728fa5e22f62c312e110a9944768%2Fraw%2Fcb672fb3f661a17576803e41db6030382b1a0fc9%2Fevents.json&loadTimelineFromURL=drive://163GY-H0wvF9rSrlwjJcrdTL-YLnppp55
20+
*/
21+
22+
class Profiler {
23+
constructor(inspector) {
24+
this.session = undefined;
25+
this.inspector = inspector;
26+
}
27+
28+
hasSession() {
29+
return this.session !== undefined;
30+
}
31+
32+
startProfiling() {
33+
if(this.inspector === undefined) {
34+
return Promise.resolve();
35+
}
36+
37+
try {
38+
this.session = new inspector.Session();
39+
this.session.connect();
40+
} catch(_) {
41+
this.session = undefined;
42+
return Promise.resolve();
43+
}
44+
45+
return Promise.all([
46+
this.sendCommand("Profiler.setSamplingInterval", {
47+
interval: 100
48+
}),
49+
this.sendCommand("Profiler.enable"),
50+
this.sendCommand("Profiler.start"),
51+
]);
52+
}
53+
54+
sendCommand(method, params) {
55+
if(this.hasSession()) {
56+
return new Promise((res, rej) => {
57+
return this.session.post(method, params, (err, params) => {
58+
if(err !== null) {
59+
rej(err);
60+
} else {
61+
res(params);
62+
}
63+
});
64+
});
65+
} else {
66+
return Promise.resolve();
67+
}
68+
}
69+
70+
destroy() {
71+
if(this.hasSession()) {
72+
this.session.disconnect();
73+
}
74+
75+
return Promise.resolve();
76+
}
77+
78+
stopProfiling() {
79+
return this.sendCommand("Profiler.stop");
80+
}
81+
}
82+
83+
/**
84+
* @param {string} outPath The location where to write the log.
85+
* @returns {{trace: ?, counter: number, profiler: Profiler}} The trace object
86+
*/
87+
function createTrace(outPath) {
88+
const trace = new Trace({
89+
noStream: true
90+
});
91+
const profiler = new Profiler(inspector);
92+
93+
let counter = 0;
94+
95+
trace.pipe(fs.createWriteStream(outPath));
96+
// These are critical events that need to be inserted so that tools like
97+
// chrome dev tools can load the profile.
98+
trace.instantEvent({
99+
name: "TracingStartedInPage",
100+
id: ++counter,
101+
cat: ["disabled-by-default-devtools.timeline"],
102+
args: {
103+
data: {
104+
sessionId: "-1",
105+
page: "0xfff",
106+
frames: [{
107+
frame: "0xfff",
108+
url: "webpack",
109+
name: ""
110+
}]
111+
}
112+
}
113+
});
114+
115+
trace.instantEvent({
116+
name: "TracingStartedInBrowser",
117+
id: ++counter,
118+
cat: ["disabled-by-default-devtools.timeline"],
119+
args: {
120+
data: {
121+
sessionId: "-1"
122+
},
123+
}
124+
});
125+
126+
return {
127+
trace,
128+
counter,
129+
profiler
130+
};
131+
}
132+
133+
class ProfilingPlugin {
134+
// TODO: Add plugin schema validation here since there are options.
135+
constructor(opts) {
136+
opts = opts || {};
137+
this.outPath = opts.outPath || "events.json";
138+
}
139+
140+
apply(compiler) {
141+
const tracer = createTrace(this.outPath);
142+
tracer.profiler.startProfiling();
143+
144+
// Compiler Hooks
145+
Object.keys(compiler.hooks).forEach(hookName => {
146+
compiler.hooks[hookName].intercept(makeInterceptorFor("Compiler", tracer)(hookName));
147+
});
148+
149+
Object.keys(compiler.resolverFactory.hooks).forEach(hookName => {
150+
compiler.resolverFactory.hooks[hookName].intercept(makeInterceptorFor("Resolver", tracer)(hookName));
151+
});
152+
153+
compiler.hooks.compilation.tap("ProfilingPlugin", (compilation, {
154+
normalModuleFactory,
155+
contextModuleFactory
156+
}) => {
157+
interceptAllHooksFor(compilation, tracer, "Compilation");
158+
interceptAllHooksFor(normalModuleFactory, tracer, "Normal Module Factory");
159+
interceptAllHooksFor(contextModuleFactory, tracer, "Context Module Factory");
160+
interceptAllParserHooks(normalModuleFactory, tracer);
161+
interceptTemplateInstancesFrom(compilation, tracer);
162+
});
163+
164+
// We need to write out the CPU profile when we are all done.
165+
compiler.hooks.done.tap({
166+
name: "ProfilingPlugin",
167+
stage: Infinity
168+
}, () => {
169+
tracer.profiler.stopProfiling().then((parsedResults) => {
170+
171+
if(parsedResults === undefined) {
172+
tracer.profiler.destroy();
173+
tracer.trace.flush();
174+
return;
175+
}
176+
177+
const cpuStartTime = parsedResults.profile.startTime;
178+
const cpuEndTime = parsedResults.profile.endTime;
179+
180+
tracer.trace.completeEvent({
181+
name: "TaskQueueManager::ProcessTaskFromWorkQueue",
182+
id: ++tracer.counter,
183+
cat: ["toplevel"],
184+
ts: cpuStartTime,
185+
args: {
186+
src_file: "../../ipc/ipc_moji_bootstrap.cc",
187+
src_func: "Accept"
188+
}
189+
});
190+
191+
tracer.trace.completeEvent({
192+
name: "EvaluateScript",
193+
id: ++tracer.counter,
194+
cat: ["devtools.timeline"],
195+
ts: cpuStartTime,
196+
dur: cpuEndTime - cpuStartTime,
197+
args: {
198+
data: {
199+
url: "webpack",
200+
lineNumber: 1,
201+
columnNumber: 1,
202+
frame: "0xFFF"
203+
}
204+
}
205+
});
206+
207+
tracer.trace.instantEvent({
208+
name: "CpuProfile",
209+
id: ++tracer.counter,
210+
cat: ["disabled-by-default-devtools.timeline"],
211+
ts: cpuEndTime,
212+
args: {
213+
data: {
214+
cpuProfile: parsedResults.profile
215+
}
216+
}
217+
});
218+
219+
tracer.profiler.destroy();
220+
tracer.trace.flush();
221+
});
222+
});
223+
}
224+
}
225+
226+
const interceptTemplateInstancesFrom = (compilation, tracer) => {
227+
const {
228+
mainTemplate,
229+
chunkTemplate,
230+
hotUpdateChunkTemplate,
231+
moduleTemplates
232+
} = compilation;
233+
234+
const {
235+
javascript,
236+
webassembly
237+
} = moduleTemplates;
238+
239+
[{
240+
instance: mainTemplate,
241+
name: "MainTemplate"
242+
},
243+
{
244+
instance: chunkTemplate,
245+
name: "ChunkTemplate"
246+
},
247+
{
248+
instance: hotUpdateChunkTemplate,
249+
name: "HotUpdateChunkTemplate"
250+
},
251+
{
252+
instance: javascript,
253+
name: "JavaScriptModuleTemplate"
254+
},
255+
{
256+
instance: webassembly,
257+
name: "WebAssemblyModuleTemplate"
258+
}
259+
].forEach(templateObject => {
260+
Object.keys(templateObject.instance.hooks).forEach(hookName => {
261+
templateObject.instance.hooks[hookName].intercept(makeInterceptorFor(templateObject.name, tracer)(hookName));
262+
});
263+
});
264+
};
265+
266+
const interceptAllHooksFor = (instance, tracer, logLabel) => {
267+
Object.keys(instance.hooks).forEach(hookName => {
268+
instance.hooks[hookName].intercept(makeInterceptorFor(logLabel, tracer)(hookName));
269+
});
270+
};
271+
272+
const interceptAllParserHooks = (moduleFactory, tracer) => {
273+
const moduleTypes = [
274+
"javascript/auto",
275+
"javascript/dynamic",
276+
"javascript/esm",
277+
"json",
278+
"webassembly/experimental"
279+
];
280+
281+
moduleTypes.forEach(moduleType => {
282+
moduleFactory.hooks.parser.for(moduleType).tap("ProfilingPlugin", (parser, parserOpts) => {
283+
interceptAllHooksFor(parser, tracer, "Parser");
284+
});
285+
});
286+
};
287+
288+
const makeInterceptorFor = (instance, tracer) => (hookName) => ({
289+
register: ({
290+
name,
291+
type,
292+
fn
293+
}) => {
294+
const newFn = makeNewProfiledTapFn(hookName, tracer, {
295+
name,
296+
type,
297+
fn
298+
});
299+
return({ // eslint-disable-line
300+
name,
301+
type,
302+
fn: newFn
303+
});
304+
}
305+
});
306+
307+
/**
308+
* @param {string} hookName Name of the hook to profile.
309+
* @param {{counter: number, trace: *, profiler: *}} tracer Instance of tracer.
310+
* @param {{name: string, type: string, fn: Function}} opts Options for the profiled fn.
311+
* @returns {*} Chainable hooked function.
312+
*/
313+
const makeNewProfiledTapFn = (hookName, tracer, {
314+
name,
315+
type,
316+
fn
317+
}) => {
318+
const defaultCategory = ["blink.user_timing"];
319+
320+
switch(type) {
321+
case "promise":
322+
return(...args) => { // eslint-disable-line
323+
const id = ++tracer.counter;
324+
tracer.trace.begin({
325+
name,
326+
id,
327+
cat: defaultCategory
328+
});
329+
return fn(...args).then(r => {
330+
tracer.trace.end({
331+
name,
332+
id,
333+
cat: defaultCategory
334+
});
335+
return r;
336+
});
337+
};
338+
case "async":
339+
return(...args) => { // eslint-disable-line
340+
const id = ++tracer.counter;
341+
tracer.trace.begin({
342+
name,
343+
id,
344+
cat: defaultCategory
345+
});
346+
fn(...args, (...r) => {
347+
const callback = args.pop();
348+
tracer.trace.end({
349+
name,
350+
id,
351+
cat: defaultCategory
352+
});
353+
callback(...r);
354+
});
355+
};
356+
case "sync":
357+
return(...args) => { // eslint-disable-line
358+
const id = ++tracer.counter;
359+
tracer.trace.begin({
360+
name,
361+
id,
362+
cat: defaultCategory
363+
});
364+
let r;
365+
try {
366+
r = fn(...args);
367+
} catch(error) {
368+
tracer.trace.end({
369+
name,
370+
id,
371+
cat: defaultCategory
372+
});
373+
throw error;
374+
}
375+
tracer.trace.end({
376+
name,
377+
id,
378+
cat: defaultCategory
379+
});
380+
return r;
381+
};
382+
default:
383+
break;
384+
}
385+
};
386+
387+
module.exports = ProfilingPlugin;
388+
module.exports.Profiler = Profiler;

lib/webpack.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,3 +123,7 @@ exportPlugins(exports.node = {}, {
123123
"NodeTemplatePlugin": () => require("./node/NodeTemplatePlugin"),
124124
"ReadFileCompileWasmTemplatePlugin": () => require("./node/ReadFileCompileWasmTemplatePlugin"),
125125
});
126+
127+
exportPlugins(exports.debug = {}, {
128+
"ProfilingPlugin": () => require("./debug/ProfilingPlugin")
129+
});

0 commit comments

Comments
 (0)