ProfilingPlugin.js 12 KB

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