hermes-profile-transformer.cjs.development.js 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541
  1. 'use strict';
  2. Object.defineProperty(exports, '__esModule', { value: true });
  3. function _interopDefault (ex) { return (ex && (typeof ex === 'object') && 'default' in ex) ? ex['default'] : ex; }
  4. var fs = require('fs');
  5. var util = require('util');
  6. var path = _interopDefault(require('path'));
  7. var sourceMap = require('source-map');
  8. function _extends() {
  9. _extends = Object.assign || function (target) {
  10. for (var i = 1; i < arguments.length; i++) {
  11. var source = arguments[i];
  12. for (var key in source) {
  13. if (Object.prototype.hasOwnProperty.call(source, key)) {
  14. target[key] = source[key];
  15. }
  16. }
  17. }
  18. return target;
  19. };
  20. return _extends.apply(this, arguments);
  21. }
  22. function _unsupportedIterableToArray(o, minLen) {
  23. if (!o) return;
  24. if (typeof o === "string") return _arrayLikeToArray(o, minLen);
  25. var n = Object.prototype.toString.call(o).slice(8, -1);
  26. if (n === "Object" && o.constructor) n = o.constructor.name;
  27. if (n === "Map" || n === "Set") return Array.from(o);
  28. if (n === "Arguments" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)) return _arrayLikeToArray(o, minLen);
  29. }
  30. function _arrayLikeToArray(arr, len) {
  31. if (len == null || len > arr.length) len = arr.length;
  32. for (var i = 0, arr2 = new Array(len); i < len; i++) arr2[i] = arr[i];
  33. return arr2;
  34. }
  35. function _createForOfIteratorHelperLoose(o, allowArrayLike) {
  36. var it;
  37. if (typeof Symbol === "undefined" || o[Symbol.iterator] == null) {
  38. if (Array.isArray(o) || (it = _unsupportedIterableToArray(o)) || allowArrayLike && o && typeof o.length === "number") {
  39. if (it) o = it;
  40. var i = 0;
  41. return function () {
  42. if (i >= o.length) return {
  43. done: true
  44. };
  45. return {
  46. done: false,
  47. value: o[i++]
  48. };
  49. };
  50. }
  51. throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.");
  52. }
  53. it = o[Symbol.iterator]();
  54. return it.next.bind(it);
  55. }
  56. var EventsPhase;
  57. (function (EventsPhase) {
  58. EventsPhase["DURATION_EVENTS_BEGIN"] = "B";
  59. EventsPhase["DURATION_EVENTS_END"] = "E";
  60. EventsPhase["COMPLETE_EVENTS"] = "X";
  61. EventsPhase["INSTANT_EVENTS"] = "I";
  62. EventsPhase["COUNTER_EVENTS"] = "C";
  63. EventsPhase["ASYNC_EVENTS_NESTABLE_START"] = "b";
  64. EventsPhase["ASYNC_EVENTS_NESTABLE_INSTANT"] = "n";
  65. EventsPhase["ASYNC_EVENTS_NESTABLE_END"] = "e";
  66. EventsPhase["FLOW_EVENTS_START"] = "s";
  67. EventsPhase["FLOW_EVENTS_STEP"] = "t";
  68. EventsPhase["FLOW_EVENTS_END"] = "f";
  69. EventsPhase["SAMPLE_EVENTS"] = "P";
  70. EventsPhase["OBJECT_EVENTS_CREATED"] = "N";
  71. EventsPhase["OBJECT_EVENTS_SNAPSHOT"] = "O";
  72. EventsPhase["OBJECT_EVENTS_DESTROYED"] = "D";
  73. EventsPhase["METADATA_EVENTS"] = "M";
  74. EventsPhase["MEMORY_DUMP_EVENTS_GLOBAL"] = "V";
  75. EventsPhase["MEMORY_DUMP_EVENTS_PROCESS"] = "v";
  76. EventsPhase["MARK_EVENTS"] = "R";
  77. EventsPhase["CLOCK_SYNC_EVENTS"] = "c";
  78. EventsPhase["CONTEXT_EVENTS_ENTER"] = "(";
  79. EventsPhase["CONTEXT_EVENTS_LEAVE"] = ")"; // Deprecated
  80. EventsPhase["ASYNC_EVENTS_START"] = "S";
  81. EventsPhase["ASYNC_EVENTS_STEP_INTO"] = "T";
  82. EventsPhase["ASYNC_EVENTS_STEP_PAST"] = "p";
  83. EventsPhase["ASYNC_EVENTS_END"] = "F";
  84. EventsPhase["LINKED_ID_EVENTS"] = "=";
  85. })(EventsPhase || (EventsPhase = {}));
  86. var CpuProfilerModel = /*#__PURE__*/function () {
  87. function CpuProfilerModel(profile) {
  88. this._profile = profile;
  89. this._nodesById = this._createNodeMap();
  90. this._activeNodeArraysById = this._createActiveNodeArrays();
  91. }
  92. /**
  93. * Initialization function to enable O(1) access to nodes by node ID.
  94. * @return {Map<number, CPUProfileChunkNode}
  95. */
  96. var _proto = CpuProfilerModel.prototype;
  97. _proto._createNodeMap = function _createNodeMap() {
  98. /** @type {Map<number, CpuProfile['nodes'][0]>} */
  99. var map = new Map();
  100. for (var _iterator = _createForOfIteratorHelperLoose(this._profile.nodes), _step; !(_step = _iterator()).done;) {
  101. var node = _step.value;
  102. map.set(node.id, node);
  103. }
  104. return map;
  105. }
  106. /**
  107. * Initialization function to enable O(1) access to the set of active nodes in the stack by node ID.
  108. * @return Map<number, number[]>
  109. */
  110. ;
  111. _proto._createActiveNodeArrays = function _createActiveNodeArrays() {
  112. var _this = this;
  113. var map = new Map();
  114. /**
  115. * Given a nodeId, `getActiveNodes` gets all the parent nodes in reversed call order
  116. * @param {number} id
  117. */
  118. var getActiveNodes = function getActiveNodes(id) {
  119. if (map.has(id)) return map.get(id) || [];
  120. var node = _this._nodesById.get(id);
  121. if (!node) throw new Error("No such node " + id);
  122. if (node.parent) {
  123. var array = getActiveNodes(node.parent).concat([id]);
  124. map.set(id, array);
  125. return array;
  126. } else {
  127. return [id];
  128. }
  129. };
  130. for (var _iterator2 = _createForOfIteratorHelperLoose(this._profile.nodes), _step2; !(_step2 = _iterator2()).done;) {
  131. var node = _step2.value;
  132. map.set(node.id, getActiveNodes(node.id));
  133. }
  134. return map;
  135. }
  136. /**
  137. * Returns all the node IDs in a stack when a specific nodeId is at the top of the stack
  138. * (i.e. a stack's node ID and the node ID of all of its parents).
  139. */
  140. ;
  141. _proto._getActiveNodeIds = function _getActiveNodeIds(nodeId) {
  142. var activeNodeIds = this._activeNodeArraysById.get(nodeId);
  143. if (!activeNodeIds) throw new Error("No such node ID " + nodeId);
  144. return activeNodeIds;
  145. }
  146. /**
  147. * Generates the necessary B/E-style trace events for a single transition from stack A to stack B
  148. * at the given timestamp.
  149. *
  150. * Example:
  151. *
  152. * timestamp 1234
  153. * previousNodeIds 1,2,3
  154. * currentNodeIds 1,2,4
  155. *
  156. * yields [end 3 at ts 1234, begin 4 at ts 1234]
  157. *
  158. * @param {number} timestamp
  159. * @param {Array<number>} previousNodeIds
  160. * @param {Array<number>} currentNodeIds
  161. * @returns {Array<DurationEvent>}
  162. */
  163. ;
  164. _proto._createStartEndEventsForTransition = function _createStartEndEventsForTransition(timestamp, previousNodeIds, currentNodeIds) {
  165. var _this2 = this;
  166. // Start nodes are the nodes which are present only in the currentNodeIds and not in PreviousNodeIds
  167. var startNodes = currentNodeIds.filter(function (id) {
  168. return !previousNodeIds.includes(id);
  169. }).map(function (id) {
  170. return _this2._nodesById.get(id);
  171. }); // End nodes are the nodes which are present only in the PreviousNodeIds and not in CurrentNodeIds
  172. var endNodes = previousNodeIds.filter(function (id) {
  173. return !currentNodeIds.includes(id);
  174. }).map(function (id) {
  175. return _this2._nodesById.get(id);
  176. });
  177. /**
  178. * The name needs to be modified if `http://` is present as this directs us to bundle files which does not add any information for the end user
  179. * @param name
  180. */
  181. var removeLinksIfExist = function removeLinksIfExist(name) {
  182. // If the name includes `http://`, we can filter the name
  183. if (name.includes('http://')) {
  184. name = name.substring(0, name.lastIndexOf('('));
  185. }
  186. return name || 'anonymous';
  187. };
  188. /**
  189. * Create a Duration Event from CPUProfileChunkNodes.
  190. * @param {CPUProfileChunkNode} node
  191. * @return {DurationEvent} */
  192. var createEvent = function createEvent(node) {
  193. return {
  194. ts: timestamp,
  195. pid: _this2._profile.pid,
  196. tid: Number(_this2._profile.tid),
  197. ph: EventsPhase.DURATION_EVENTS_BEGIN,
  198. name: removeLinksIfExist(node.callFrame.name),
  199. cat: node.callFrame.category,
  200. args: _extends({}, node.callFrame)
  201. };
  202. };
  203. var startEvents = startNodes.map(createEvent).map(function (evt) {
  204. return _extends({}, evt, {
  205. ph: EventsPhase.DURATION_EVENTS_BEGIN
  206. });
  207. });
  208. var endEvents = endNodes.map(createEvent).map(function (evt) {
  209. return _extends({}, evt, {
  210. ph: EventsPhase.DURATION_EVENTS_END
  211. });
  212. });
  213. return [].concat(endEvents.reverse(), startEvents);
  214. }
  215. /**
  216. * Creates B/E-style trace events from a CpuProfile object created by `collectProfileEvents()`
  217. * @return {DurationEvent}
  218. * @throws If the length of timeDeltas array or the samples array does not match with the length of samples in Hermes Profile
  219. */
  220. ;
  221. _proto.createStartEndEvents = function createStartEndEvents() {
  222. var profile = this._profile;
  223. var length = profile.samples.length;
  224. if (profile.timeDeltas.length !== length || profile.samples.length !== length) throw new Error("Invalid CPU profile length");
  225. var events = [];
  226. var timestamp = profile.startTime;
  227. var lastActiveNodeIds = [];
  228. for (var i = 0; i < profile.samples.length; i++) {
  229. var nodeId = profile.samples[i];
  230. var timeDelta = Math.max(profile.timeDeltas[i], 0);
  231. var node = this._nodesById.get(nodeId);
  232. if (!node) throw new Error("Missing node " + nodeId);
  233. timestamp += timeDelta;
  234. var activeNodeIds = this._getActiveNodeIds(nodeId);
  235. events.push.apply(events, this._createStartEndEventsForTransition(timestamp, lastActiveNodeIds, activeNodeIds));
  236. lastActiveNodeIds = activeNodeIds;
  237. }
  238. events.push.apply(events, this._createStartEndEventsForTransition(timestamp, lastActiveNodeIds, []));
  239. return events;
  240. }
  241. /**
  242. * Creates B/E-style trace events from a CpuProfile object created by `collectProfileEvents()`
  243. * @param {CPUProfileChunk} profile
  244. */
  245. ;
  246. CpuProfilerModel.createStartEndEvents = function createStartEndEvents(profile) {
  247. var model = new CpuProfilerModel(profile);
  248. return model.createStartEndEvents();
  249. }
  250. /**
  251. * Converts the Hermes Sample into a single CpuProfileChunk object for consumption
  252. * by `createStartEndEvents()`.
  253. *
  254. * @param {HermesCPUProfile} profile
  255. * @throws Profile must have at least one sample
  256. * @return {CPUProfileChunk}
  257. */
  258. ;
  259. CpuProfilerModel.collectProfileEvents = function collectProfileEvents(profile) {
  260. if (profile.samples.length >= 0) {
  261. var samples = profile.samples,
  262. stackFrames = profile.stackFrames; // Assumption: The sample will have a single process
  263. var pid = samples[0].pid; // Assumption: Javascript is single threaded, so there should only be one thread throughout
  264. var tid = samples[0].tid; // TODO: What role does id play in string parsing
  265. var id = '0x1';
  266. var startTime = Number(samples[0].ts);
  267. var _this$constructNodes = this.constructNodes(samples, stackFrames),
  268. nodes = _this$constructNodes.nodes,
  269. sampleNumbers = _this$constructNodes.sampleNumbers,
  270. timeDeltas = _this$constructNodes.timeDeltas;
  271. return {
  272. id: id,
  273. pid: pid,
  274. tid: tid,
  275. startTime: startTime,
  276. nodes: nodes,
  277. samples: sampleNumbers,
  278. timeDeltas: timeDeltas
  279. };
  280. } else {
  281. throw new Error('The hermes profile has zero samples');
  282. }
  283. }
  284. /**
  285. * Constructs CPUProfileChunk Nodes and the resultant samples and time deltas to be inputted into the
  286. * CPUProfileChunk object which will be processed to give createStartEndEvents()
  287. *
  288. * @param {HermesSample} samples
  289. * @param {<string, HermesStackFrame>} stackFrames
  290. * @return {CPUProfileChunker}
  291. */
  292. ;
  293. CpuProfilerModel.constructNodes = function constructNodes(samples, stackFrames) {
  294. samples = samples.map(function (sample) {
  295. sample.stackFrameData = stackFrames[sample.sf];
  296. return sample;
  297. });
  298. var stackFrameIds = Object.keys(stackFrames);
  299. var profileNodes = stackFrameIds.map(function (stackFrameId) {
  300. var stackFrame = stackFrames[stackFrameId];
  301. return {
  302. id: Number(stackFrameId),
  303. callFrame: _extends({}, stackFrame, {
  304. url: stackFrame.name
  305. }),
  306. parent: stackFrames[stackFrameId].parent
  307. };
  308. });
  309. var returnedSamples = [];
  310. var timeDeltas = [];
  311. var lastTimeStamp = Number(samples[0].ts);
  312. samples.forEach(function (sample, idx) {
  313. returnedSamples.push(sample.sf);
  314. if (idx === 0) {
  315. timeDeltas.push(0);
  316. } else {
  317. var timeDiff = Number(sample.ts) - lastTimeStamp;
  318. lastTimeStamp = Number(sample.ts);
  319. timeDeltas.push(timeDiff);
  320. }
  321. });
  322. return {
  323. nodes: profileNodes,
  324. sampleNumbers: returnedSamples,
  325. timeDeltas: timeDeltas
  326. };
  327. };
  328. return CpuProfilerModel;
  329. }();
  330. // A type of promise-like that resolves synchronously and supports only one observer
  331. const _iteratorSymbol = /*#__PURE__*/ typeof Symbol !== "undefined" ? (Symbol.iterator || (Symbol.iterator = Symbol("Symbol.iterator"))) : "@@iterator";
  332. const _asyncIteratorSymbol = /*#__PURE__*/ typeof Symbol !== "undefined" ? (Symbol.asyncIterator || (Symbol.asyncIterator = Symbol("Symbol.asyncIterator"))) : "@@asyncIterator";
  333. // Asynchronously call a function and send errors to recovery continuation
  334. function _catch(body, recover) {
  335. try {
  336. var result = body();
  337. } catch(e) {
  338. return recover(e);
  339. }
  340. if (result && result.then) {
  341. return result.then(void 0, recover);
  342. }
  343. return result;
  344. }
  345. var readFileAsync = function readFileAsync(path) {
  346. try {
  347. return Promise.resolve(_catch(function () {
  348. var readFileAsync = util.promisify(fs.readFile);
  349. return Promise.resolve(readFileAsync(path, 'utf-8')).then(function (fileString) {
  350. if (fileString.length === 0) {
  351. throw new Error(path + " is an empty file");
  352. }
  353. var obj = JSON.parse(fileString);
  354. return obj;
  355. });
  356. }, function (err) {
  357. throw err;
  358. }));
  359. } catch (e) {
  360. return Promise.reject(e);
  361. }
  362. };
  363. /**
  364. * This function is a helper to the applySourceMapsToEvents. The category allocation logic is implemented here based on the sourcemap url (if available)
  365. * @param defaultCategory The category the event is of by default without the use of Source maps
  366. * @param url The URL which can be parsed to interpret the new category of the event (depends on node_modules)
  367. */
  368. var improveCategories = function improveCategories(defaultCategory, url) {
  369. var obtainCategory = function obtainCategory(url) {
  370. var dirs = url.substring(url.lastIndexOf(path.sep + "node_modules" + path.sep)).split(path.sep);
  371. return dirs.length > 2 && dirs[1] === 'node_modules' ? dirs[2] : defaultCategory;
  372. };
  373. return url ? obtainCategory(url) : defaultCategory;
  374. };
  375. /**
  376. * Enhances the function line, column and params information and event categories
  377. * based on JavaScript source maps to make it easier to associate trace events with
  378. * the application code
  379. *
  380. * Throws error if args not set up in ChromeEvents
  381. * @param {SourceMap} sourceMap
  382. * @param {DurationEvent[]} chromeEvents
  383. * @param {string} indexBundleFileName
  384. * @throws If `args` for events are not populated
  385. * @returns {DurationEvent[]}
  386. */
  387. var applySourceMapsToEvents = function applySourceMapsToEvents(sourceMap$1, chromeEvents, indexBundleFileName) {
  388. try {
  389. // SEE: Should file here be an optional parameter, so take indexBundleFileName as a parameter and use
  390. // a default name of `index.bundle`
  391. var rawSourceMap = {
  392. version: Number(sourceMap$1.version),
  393. file: indexBundleFileName || 'index.bundle',
  394. sources: sourceMap$1.sources,
  395. mappings: sourceMap$1.mappings,
  396. names: sourceMap$1.names
  397. };
  398. return Promise.resolve(new sourceMap.SourceMapConsumer(rawSourceMap)).then(function (consumer) {
  399. var events = chromeEvents.map(function (event) {
  400. if (event.args) {
  401. var sm = consumer.originalPositionFor({
  402. line: Number(event.args.line),
  403. column: Number(event.args.column)
  404. });
  405. /**
  406. * The categories can help us better visualise the profile if we modify the categories.
  407. * We change these categories only in the root level and not deeper inside the args, just so we have our
  408. * original categories as well as these modified categories (as the modified categories simply help with visualisation)
  409. */
  410. event.cat = improveCategories(event.cat, sm.source);
  411. event.args = _extends({}, event.args, {
  412. url: sm.source,
  413. line: sm.line,
  414. column: sm.column,
  415. params: sm.name,
  416. allocatedCategory: event.cat,
  417. allocatedName: event.name
  418. });
  419. } else {
  420. throw new Error("Source maps could not be derived for an event at " + event.ts + " and with stackFrame ID " + event.sf);
  421. }
  422. return event;
  423. });
  424. consumer.destroy();
  425. return events;
  426. });
  427. } catch (e) {
  428. return Promise.reject(e);
  429. }
  430. };
  431. /**
  432. * This transformer can take in the path of the profile, the source map (optional) and the bundle file name (optional)
  433. * and return a promise which resolves to Chrome Dev Tools compatible events
  434. * @param profilePath string
  435. * @param sourceMapPath string
  436. * @param bundleFileName string
  437. * @return Promise<DurationEvent[]>
  438. */
  439. var transformer = function transformer(profilePath, sourceMapPath, bundleFileName) {
  440. try {
  441. return Promise.resolve(readFileAsync(profilePath)).then(function (hermesProfile) {
  442. var _exit = false;
  443. var profileChunk = CpuProfilerModel.collectProfileEvents(hermesProfile);
  444. var profiler = new CpuProfilerModel(profileChunk);
  445. var chromeEvents = profiler.createStartEndEvents();
  446. var _temp = function () {
  447. if (sourceMapPath) {
  448. return Promise.resolve(readFileAsync(sourceMapPath)).then(function (sourceMap) {
  449. var events = applySourceMapsToEvents(sourceMap, chromeEvents, bundleFileName);
  450. _exit = true;
  451. return events;
  452. });
  453. }
  454. }();
  455. return _temp && _temp.then ? _temp.then(function (_result) {
  456. return _exit ? _result : chromeEvents;
  457. }) : _exit ? _temp : chromeEvents;
  458. });
  459. } catch (e) {
  460. return Promise.reject(e);
  461. }
  462. };
  463. exports.default = transformer;
  464. //# sourceMappingURL=hermes-profile-transformer.cjs.development.js.map