source: frontend/node_modules/webpack/lib/debug/ProfilingPlugin.js

Last change on this file was 9af201e, checked in by MBK <marija.karapandzova@…>, 12 days ago

Fix frontend appearance

  • Property mode set to 100644
File size: 15.0 KB
Line 
1/*
2 MIT License http://www.opensource.org/licenses/mit-license.php
3*/
4
5"use strict";
6
7const { Tracer } = require("chrome-trace-event");
8const {
9 CSS_MODULES,
10 JAVASCRIPT_MODULES,
11 JSON_MODULE_TYPE,
12 WEBASSEMBLY_MODULES
13} = require("../ModuleTypeConstants");
14const { dirname, mkdirpSync } = require("../util/fs");
15
16/** @typedef {import("inspector").Session} Session */
17/** @typedef {import("tapable").FullTap} FullTap */
18/** @typedef {import("../../declarations/plugins/debug/ProfilingPlugin").ProfilingPluginOptions} ProfilingPluginOptions */
19/** @typedef {import("../Compilation")} Compilation */
20/** @typedef {import("../Compiler")} Compiler */
21/** @typedef {import("../NormalModuleFactory")} NormalModuleFactory */
22/** @typedef {import("../ResolverFactory")} ResolverFactory */
23/** @typedef {import("../util/fs").IntermediateFileSystem} IntermediateFileSystem */
24
25/**
26 * Defines the hook type used by this module.
27 * @template T, R
28 * @typedef {import("tapable").Hook<T, R>} Hook
29 */
30
31/**
32 * Defines the fake hook type used by this module.
33 * @template T
34 * @typedef {import("../util/deprecation").FakeHook<T>} FakeHook
35 */
36
37/**
38 * Defines the hook map type used by this module.
39 * @template T
40 * @typedef {import("tapable").HookMap<T>} HookMap
41 */
42
43/**
44 * Defines the hook interceptor type used by this module.
45 * @template T, R
46 * @typedef {import("tapable").HookInterceptor<T, R>} HookInterceptor
47 */
48
49/** @typedef {{ Session: typeof import("inspector").Session }} Inspector */
50
51/** @type {Inspector | undefined} */
52let inspector;
53
54try {
55 // eslint-disable-next-line n/no-unsupported-features/node-builtins
56 inspector = require("inspector");
57} catch (_err) {
58 // eslint-disable-next-line no-console
59 console.log("Unable to CPU profile in < node 8.0");
60}
61
62class Profiler {
63 /**
64 * Creates an instance of Profiler.
65 * @param {Inspector} inspector inspector
66 */
67 constructor(inspector) {
68 /** @type {undefined | Session} */
69 this.session = undefined;
70 this.inspector = inspector;
71 this._startTime = 0;
72 }
73
74 hasSession() {
75 return this.session !== undefined;
76 }
77
78 startProfiling() {
79 if (this.inspector === undefined) {
80 return Promise.resolve();
81 }
82
83 try {
84 this.session = new /** @type {Inspector} */ (inspector).Session();
85 /** @type {Session} */
86 (this.session).connect();
87 } catch (_) {
88 this.session = undefined;
89 return Promise.resolve();
90 }
91
92 const hrtime = process.hrtime();
93 this._startTime = hrtime[0] * 1000000 + Math.round(hrtime[1] / 1000);
94
95 return Promise.all([
96 this.sendCommand("Profiler.setSamplingInterval", {
97 interval: 100
98 }),
99 this.sendCommand("Profiler.enable"),
100 this.sendCommand("Profiler.start")
101 ]);
102 }
103
104 /**
105 * Returns promise for the result.
106 * @param {string} method method name
107 * @param {EXPECTED_OBJECT=} params params
108 * @returns {Promise<EXPECTED_ANY | void>} Promise for the result
109 */
110 sendCommand(method, params) {
111 if (this.hasSession()) {
112 return new Promise((res, rej) => {
113 /** @type {Session} */
114 (this.session).post(method, params, (err, params) => {
115 if (err !== null) {
116 rej(err);
117 } else {
118 res(params);
119 }
120 });
121 });
122 }
123 return Promise.resolve();
124 }
125
126 destroy() {
127 if (this.hasSession()) {
128 /** @type {Session} */
129 (this.session).disconnect();
130 }
131
132 return Promise.resolve();
133 }
134
135 /**
136 * Returns }>} profile result.
137 * @returns {Promise<{ profile: { startTime: number, endTime: number } }>} profile result
138 */
139 stopProfiling() {
140 return this.sendCommand("Profiler.stop").then(({ profile }) => {
141 const hrtime = process.hrtime();
142 const endTime = hrtime[0] * 1000000 + Math.round(hrtime[1] / 1000);
143 // Avoid coverage problems due indirect changes
144 /* istanbul ignore next */
145 if (profile.startTime < this._startTime || profile.endTime > endTime) {
146 // In some cases timestamps mismatch and we need to adjust them
147 // Both process.hrtime and the inspector timestamps claim to be relative
148 // to a unknown point in time. But they do not guarantee that this is the
149 // same point in time.
150 const duration = profile.endTime - profile.startTime;
151 const ownDuration = endTime - this._startTime;
152 const untracked = Math.max(0, ownDuration - duration);
153 profile.startTime = this._startTime + untracked / 2;
154 profile.endTime = endTime - untracked / 2;
155 }
156 return { profile };
157 });
158 }
159}
160
161/**
162 * an object that wraps Tracer and Profiler with a counter
163 * @typedef {object} Trace
164 * @property {Tracer} trace instance of Tracer
165 * @property {number} counter Counter
166 * @property {Profiler} profiler instance of Profiler
167 * @property {(callback: (err?: null | Error) => void) => void} end the end function
168 */
169
170/**
171 * Creates a trace from the provided f.
172 * @param {IntermediateFileSystem} fs filesystem used for output
173 * @param {string} outputPath The location where to write the log.
174 * @returns {Trace} The trace object
175 */
176const createTrace = (fs, outputPath) => {
177 const trace = new Tracer();
178 const profiler = new Profiler(/** @type {Inspector} */ (inspector));
179 if (/\/|\\/.test(outputPath)) {
180 const dirPath = dirname(fs, outputPath);
181 mkdirpSync(fs, dirPath);
182 }
183 const fsStream = fs.createWriteStream(outputPath);
184
185 let counter = 0;
186
187 trace.pipe(fsStream);
188 // These are critical events that need to be inserted so that tools like
189 // chrome dev tools can load the profile.
190 trace.instantEvent({
191 name: "TracingStartedInPage",
192 id: ++counter,
193 cat: ["disabled-by-default-devtools.timeline"],
194 args: {
195 data: {
196 sessionId: "-1",
197 page: "0xfff",
198 frames: [
199 {
200 frame: "0xfff",
201 url: "webpack",
202 name: ""
203 }
204 ]
205 }
206 }
207 });
208
209 trace.instantEvent({
210 name: "TracingStartedInBrowser",
211 id: ++counter,
212 cat: ["disabled-by-default-devtools.timeline"],
213 args: {
214 data: {
215 sessionId: "-1"
216 }
217 }
218 });
219
220 return {
221 trace,
222 counter,
223 profiler,
224 end: (callback) => {
225 trace.push("]");
226 // Wait until the write stream finishes.
227 fsStream.on("close", () => {
228 callback();
229 });
230 // Tear down the readable trace stream.
231 trace.push(null);
232 }
233 };
234};
235
236const PLUGIN_NAME = "ProfilingPlugin";
237
238class ProfilingPlugin {
239 /**
240 * Creates an instance of ProfilingPlugin.
241 * @param {ProfilingPluginOptions=} options options object
242 */
243 constructor(options = {}) {
244 /** @type {ProfilingPluginOptions} */
245 this.options = options;
246 }
247
248 /**
249 * Applies the plugin by registering its hooks on the compiler.
250 * @param {Compiler} compiler the compiler instance
251 * @returns {void}
252 */
253 apply(compiler) {
254 compiler.hooks.validate.tap(PLUGIN_NAME, () => {
255 compiler.validate(
256 () => require("../../schemas/plugins/debug/ProfilingPlugin.json"),
257 this.options,
258 {
259 name: "Profiling Plugin",
260 baseDataPath: "options"
261 },
262 (options) =>
263 require("../../schemas/plugins/debug/ProfilingPlugin.check")(options)
264 );
265 });
266
267 const tracer = createTrace(
268 /** @type {IntermediateFileSystem} */
269 (compiler.intermediateFileSystem),
270 this.options.outputPath || "events.json"
271 );
272 tracer.profiler.startProfiling();
273
274 // Compiler Hooks
275 for (const hookName of Object.keys(compiler.hooks)) {
276 const hook =
277 compiler.hooks[/** @type {keyof Compiler["hooks"]} */ (hookName)];
278 if (hook) {
279 hook.intercept(makeInterceptorFor("Compiler", tracer)(hookName));
280 }
281 }
282
283 for (const hookName of Object.keys(compiler.resolverFactory.hooks)) {
284 const hook =
285 compiler.resolverFactory.hooks[
286 /** @type {keyof ResolverFactory["hooks"]} */
287 (hookName)
288 ];
289 if (hook) {
290 hook.intercept(
291 /** @type {EXPECTED_ANY} */
292 (makeInterceptorFor("Resolver", tracer)(hookName))
293 );
294 }
295 }
296
297 compiler.hooks.compilation.tap(
298 PLUGIN_NAME,
299 (compilation, { normalModuleFactory, contextModuleFactory }) => {
300 interceptAllHooksFor(compilation, tracer, "Compilation");
301 interceptAllHooksFor(
302 normalModuleFactory,
303 tracer,
304 "Normal Module Factory"
305 );
306 interceptAllHooksFor(
307 contextModuleFactory,
308 tracer,
309 "Context Module Factory"
310 );
311 interceptAllParserHooks(normalModuleFactory, tracer);
312 interceptAllGeneratorHooks(normalModuleFactory, tracer);
313 interceptAllJavascriptModulesPluginHooks(compilation, tracer);
314 interceptAllCssModulesPluginHooks(compilation, tracer);
315 }
316 );
317
318 // We need to write out the CPU profile when we are all done.
319 compiler.hooks.done.tapAsync(
320 {
321 name: PLUGIN_NAME,
322 stage: Infinity
323 },
324 (stats, callback) => {
325 if (compiler.watchMode) return callback();
326 tracer.profiler.stopProfiling().then((parsedResults) => {
327 if (parsedResults === undefined) {
328 tracer.profiler.destroy();
329 tracer.end(callback);
330 return;
331 }
332
333 const cpuStartTime = parsedResults.profile.startTime;
334 const cpuEndTime = parsedResults.profile.endTime;
335
336 tracer.trace.completeEvent({
337 name: "TaskQueueManager::ProcessTaskFromWorkQueue",
338 id: ++tracer.counter,
339 cat: ["toplevel"],
340 ts: cpuStartTime,
341 args: {
342 // eslint-disable-next-line camelcase
343 src_file: "../../ipc/ipc_moji_bootstrap.cc",
344 // eslint-disable-next-line camelcase
345 src_func: "Accept"
346 }
347 });
348
349 tracer.trace.completeEvent({
350 name: "EvaluateScript",
351 id: ++tracer.counter,
352 cat: ["devtools.timeline"],
353 ts: cpuStartTime,
354 dur: cpuEndTime - cpuStartTime,
355 args: {
356 data: {
357 url: "webpack",
358 lineNumber: 1,
359 columnNumber: 1,
360 frame: "0xFFF"
361 }
362 }
363 });
364
365 tracer.trace.instantEvent({
366 name: "CpuProfile",
367 id: ++tracer.counter,
368 cat: ["disabled-by-default-devtools.timeline"],
369 ts: cpuEndTime,
370 args: {
371 data: {
372 cpuProfile: parsedResults.profile
373 }
374 }
375 });
376
377 tracer.profiler.destroy();
378 tracer.end(callback);
379 });
380 }
381 );
382 }
383}
384
385/** @typedef {Record<string, Hook<EXPECTED_ANY, EXPECTED_ANY> | FakeHook<EXPECTED_ANY> | HookMap<EXPECTED_ANY>>} Hooks */
386
387/**
388 * Intercept all hooks for.
389 * @param {EXPECTED_OBJECT & { hooks?: Hooks }} instance instance
390 * @param {Trace} tracer tracer
391 * @param {string} logLabel log label
392 */
393const interceptAllHooksFor = (instance, tracer, logLabel) => {
394 if (Reflect.has(instance, "hooks")) {
395 const hooks = /** @type {Hooks} */ (instance.hooks);
396 for (const hookName of Object.keys(hooks)) {
397 const hook = hooks[hookName];
398 if (hook && !hook._fakeHook) {
399 hook.intercept(makeInterceptorFor(logLabel, tracer)(hookName));
400 }
401 }
402 }
403};
404
405/**
406 * Intercept all parser hooks.
407 * @param {NormalModuleFactory} moduleFactory normal module factory
408 * @param {Trace} tracer tracer
409 */
410const interceptAllParserHooks = (moduleFactory, tracer) => {
411 const moduleTypes = [
412 ...JAVASCRIPT_MODULES,
413 JSON_MODULE_TYPE,
414 ...WEBASSEMBLY_MODULES,
415 ...CSS_MODULES
416 ];
417
418 for (const moduleType of moduleTypes) {
419 moduleFactory.hooks.parser
420 .for(moduleType)
421 .tap(PLUGIN_NAME, (parser, _parserOpts) => {
422 interceptAllHooksFor(parser, tracer, "Parser");
423 });
424 }
425};
426
427/**
428 * Intercept all generator hooks.
429 * @param {NormalModuleFactory} moduleFactory normal module factory
430 * @param {Trace} tracer tracer
431 */
432const interceptAllGeneratorHooks = (moduleFactory, tracer) => {
433 const moduleTypes = [
434 ...JAVASCRIPT_MODULES,
435 JSON_MODULE_TYPE,
436 ...WEBASSEMBLY_MODULES,
437 ...CSS_MODULES
438 ];
439
440 for (const moduleType of moduleTypes) {
441 moduleFactory.hooks.generator
442 .for(moduleType)
443 .tap(PLUGIN_NAME, (parser, _parserOpts) => {
444 interceptAllHooksFor(parser, tracer, "Generator");
445 });
446 }
447};
448
449/**
450 * Intercept all javascript modules plugin hooks.
451 * @param {Compilation} compilation compilation
452 * @param {Trace} tracer tracer
453 */
454const interceptAllJavascriptModulesPluginHooks = (compilation, tracer) => {
455 interceptAllHooksFor(
456 {
457 hooks:
458 require("../javascript/JavascriptModulesPlugin").getCompilationHooks(
459 compilation
460 )
461 },
462 tracer,
463 "JavascriptModulesPlugin"
464 );
465};
466
467/**
468 * Intercept all css modules plugin hooks.
469 * @param {Compilation} compilation compilation
470 * @param {Trace} tracer tracer
471 */
472const interceptAllCssModulesPluginHooks = (compilation, tracer) => {
473 interceptAllHooksFor(
474 {
475 hooks: require("../css/CssModulesPlugin").getCompilationHooks(compilation)
476 },
477 tracer,
478 "CssModulesPlugin"
479 );
480};
481
482/** @typedef {(...args: EXPECTED_ANY[]) => EXPECTED_ANY | Promise<(...args: EXPECTED_ANY[]) => EXPECTED_ANY>} PluginFunction */
483
484/**
485 * Creates interceptor for.
486 * @template T
487 * @param {string} instance instance
488 * @param {Trace} tracer tracer
489 * @returns {(hookName: string) => HookInterceptor<EXPECTED_ANY, EXPECTED_ANY>} interceptor
490 */
491const makeInterceptorFor = (instance, tracer) => (hookName) => ({
492 /**
493 * Returns modified full tap.
494 * @param {FullTap} tapInfo tap info
495 * @returns {FullTap} modified full tap
496 */
497 register: (tapInfo) => {
498 const { name, type, fn: internalFn } = tapInfo;
499 const newFn =
500 // Don't tap our own hooks to ensure stream can close cleanly
501 name === PLUGIN_NAME
502 ? internalFn
503 : makeNewProfiledTapFn(hookName, tracer, {
504 name,
505 type,
506 fn: /** @type {PluginFunction} */ (internalFn)
507 });
508 return { ...tapInfo, fn: newFn };
509 }
510});
511
512/**
513 * Creates new profiled tap fn.
514 * @param {string} hookName Name of the hook to profile.
515 * @param {Trace} tracer The trace object.
516 * @param {object} options Options for the profiled fn.
517 * @param {string} options.name Plugin name
518 * @param {"sync" | "async" | "promise"} options.type Plugin type (sync | async | promise)
519 * @param {PluginFunction} options.fn Plugin function
520 * @returns {PluginFunction} Chainable hooked function.
521 */
522const makeNewProfiledTapFn = (hookName, tracer, { name, type, fn }) => {
523 const defaultCategory = ["blink.user_timing"];
524
525 switch (type) {
526 case "promise":
527 return (...args) => {
528 const id = ++tracer.counter;
529 tracer.trace.begin({
530 name,
531 id,
532 cat: defaultCategory
533 });
534 const promise =
535 /** @type {Promise<(...args: EXPECTED_ANY[]) => EXPECTED_ANY>} */
536 (fn(...args));
537 return promise.then((r) => {
538 tracer.trace.end({
539 name,
540 id,
541 cat: defaultCategory
542 });
543 return r;
544 });
545 };
546 case "async":
547 return (...args) => {
548 const id = ++tracer.counter;
549 tracer.trace.begin({
550 name,
551 id,
552 cat: defaultCategory
553 });
554 const callback = args.pop();
555 fn(
556 ...args,
557 /**
558 * Handles the cat callback for this hook.
559 * @param {...EXPECTED_ANY[]} r result
560 */
561 (...r) => {
562 tracer.trace.end({
563 name,
564 id,
565 cat: defaultCategory
566 });
567 callback(...r);
568 }
569 );
570 };
571 case "sync":
572 return (...args) => {
573 const id = ++tracer.counter;
574 // Do not instrument ourself due to the CPU
575 // profile needing to be the last event in the trace.
576 if (name === PLUGIN_NAME) {
577 return fn(...args);
578 }
579
580 tracer.trace.begin({
581 name,
582 id,
583 cat: defaultCategory
584 });
585 /** @type {PluginFunction} */
586 let r;
587 try {
588 r = fn(...args);
589 } catch (err) {
590 tracer.trace.end({
591 name,
592 id,
593 cat: defaultCategory
594 });
595 throw err;
596 }
597 tracer.trace.end({
598 name,
599 id,
600 cat: defaultCategory
601 });
602 return r;
603 };
604 default:
605 return fn;
606 }
607};
608
609module.exports = ProfilingPlugin;
610module.exports.Profiler = Profiler;
Note: See TracBrowser for help on using the repository browser.