ProfilingPlugin.js 11 KB

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