ProfilingPlugin.js 14 KB

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