hermes-profile-transformer.esm.js 18 KB

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