import { readFile } from 'fs'; import { promisify } from 'util'; import path from 'path'; import { SourceMapConsumer } from 'source-map'; function _extends() { _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; return _extends.apply(this, arguments); } function _unsupportedIterableToArray(o, minLen) { if (!o) return; if (typeof o === "string") return _arrayLikeToArray(o, minLen); var n = Object.prototype.toString.call(o).slice(8, -1); if (n === "Object" && o.constructor) n = o.constructor.name; if (n === "Map" || n === "Set") return Array.from(o); if (n === "Arguments" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)) return _arrayLikeToArray(o, minLen); } function _arrayLikeToArray(arr, len) { if (len == null || len > arr.length) len = arr.length; for (var i = 0, arr2 = new Array(len); i < len; i++) arr2[i] = arr[i]; return arr2; } function _createForOfIteratorHelperLoose(o, allowArrayLike) { var it; if (typeof Symbol === "undefined" || o[Symbol.iterator] == null) { if (Array.isArray(o) || (it = _unsupportedIterableToArray(o)) || allowArrayLike && o && typeof o.length === "number") { if (it) o = it; var i = 0; return function () { if (i >= o.length) return { done: true }; return { done: false, value: o[i++] }; }; } throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); } it = o[Symbol.iterator](); return it.next.bind(it); } var EventsPhase; (function (EventsPhase) { EventsPhase["DURATION_EVENTS_BEGIN"] = "B"; EventsPhase["DURATION_EVENTS_END"] = "E"; EventsPhase["COMPLETE_EVENTS"] = "X"; EventsPhase["INSTANT_EVENTS"] = "I"; EventsPhase["COUNTER_EVENTS"] = "C"; EventsPhase["ASYNC_EVENTS_NESTABLE_START"] = "b"; EventsPhase["ASYNC_EVENTS_NESTABLE_INSTANT"] = "n"; EventsPhase["ASYNC_EVENTS_NESTABLE_END"] = "e"; EventsPhase["FLOW_EVENTS_START"] = "s"; EventsPhase["FLOW_EVENTS_STEP"] = "t"; EventsPhase["FLOW_EVENTS_END"] = "f"; EventsPhase["SAMPLE_EVENTS"] = "P"; EventsPhase["OBJECT_EVENTS_CREATED"] = "N"; EventsPhase["OBJECT_EVENTS_SNAPSHOT"] = "O"; EventsPhase["OBJECT_EVENTS_DESTROYED"] = "D"; EventsPhase["METADATA_EVENTS"] = "M"; EventsPhase["MEMORY_DUMP_EVENTS_GLOBAL"] = "V"; EventsPhase["MEMORY_DUMP_EVENTS_PROCESS"] = "v"; EventsPhase["MARK_EVENTS"] = "R"; EventsPhase["CLOCK_SYNC_EVENTS"] = "c"; EventsPhase["CONTEXT_EVENTS_ENTER"] = "("; EventsPhase["CONTEXT_EVENTS_LEAVE"] = ")"; // Deprecated EventsPhase["ASYNC_EVENTS_START"] = "S"; EventsPhase["ASYNC_EVENTS_STEP_INTO"] = "T"; EventsPhase["ASYNC_EVENTS_STEP_PAST"] = "p"; EventsPhase["ASYNC_EVENTS_END"] = "F"; EventsPhase["LINKED_ID_EVENTS"] = "="; })(EventsPhase || (EventsPhase = {})); var CpuProfilerModel = /*#__PURE__*/function () { function CpuProfilerModel(profile) { this._profile = profile; this._nodesById = this._createNodeMap(); this._activeNodeArraysById = this._createActiveNodeArrays(); } /** * Initialization function to enable O(1) access to nodes by node ID. * @return {Map} */ var map = new Map(); for (var _iterator = _createForOfIteratorHelperLoose(this._profile.nodes), _step; !(_step = _iterator()).done;) { var node = _step.value; map.set(node.id, node); } return map; } /** * Initialization function to enable O(1) access to the set of active nodes in the stack by node ID. * @return Map */ ; _proto._createActiveNodeArrays = function _createActiveNodeArrays() { var _this = this; var map = new Map(); /** * Given a nodeId, `getActiveNodes` gets all the parent nodes in reversed call order * @param {number} id */ var getActiveNodes = function getActiveNodes(id) { if (map.has(id)) return map.get(id) || []; var node = _this._nodesById.get(id); if (!node) throw new Error("No such node " + id); if (node.parent) { var array = getActiveNodes(node.parent).concat([id]); map.set(id, array); return array; } else { return [id]; } }; for (var _iterator2 = _createForOfIteratorHelperLoose(this._profile.nodes), _step2; !(_step2 = _iterator2()).done;) { var node = _step2.value; map.set(node.id, getActiveNodes(node.id)); } return map; } /** * Returns all the node IDs in a stack when a specific nodeId is at the top of the stack * (i.e. a stack's node ID and the node ID of all of its parents). */ ; _proto._getActiveNodeIds = function _getActiveNodeIds(nodeId) { var activeNodeIds = this._activeNodeArraysById.get(nodeId); if (!activeNodeIds) throw new Error("No such node ID " + nodeId); return activeNodeIds; } /** * Generates the necessary B/E-style trace events for a single transition from stack A to stack B * at the given timestamp. * * Example: * * timestamp 1234 * previousNodeIds 1,2,3 * currentNodeIds 1,2,4 * * yields [end 3 at ts 1234, begin 4 at ts 1234] * * @param {number} timestamp * @param {Array} previousNodeIds * @param {Array} currentNodeIds * @returns {Array} */ ; _proto._createStartEndEventsForTransition = function _createStartEndEventsForTransition(timestamp, previousNodeIds, currentNodeIds) { var _this2 = this; // Start nodes are the nodes which are present only in the currentNodeIds and not in PreviousNodeIds var startNodes = currentNodeIds.filter(function (id) { return !previousNodeIds.includes(id); }).map(function (id) { return _this2._nodesById.get(id); }); // End nodes are the nodes which are present only in the PreviousNodeIds and not in CurrentNodeIds var endNodes = previousNodeIds.filter(function (id) { return !currentNodeIds.includes(id); }).map(function (id) { return _this2._nodesById.get(id); }); /** * 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 * @param name */ var removeLinksIfExist = function removeLinksIfExist(name) { // If the name includes `http://`, we can filter the name if (name.includes('http://')) { name = name.substring(0, name.lastIndexOf('(')); } return name || 'anonymous'; }; /** * Create a Duration Event from CPUProfileChunkNodes. * @param {CPUProfileChunkNode} node * @return {DurationEvent} */ var createEvent = function createEvent(node) { return { ts: timestamp, pid: _this2._profile.pid, tid: Number(_this2._profile.tid), ph: EventsPhase.DURATION_EVENTS_BEGIN, name: removeLinksIfExist(node.callFrame.name), cat: node.callFrame.category, args: _extends({}, node.callFrame) }; }; var startEvents = startNodes.map(createEvent).map(function (evt) { return _extends({}, evt, { ph: EventsPhase.DURATION_EVENTS_BEGIN }); }); var endEvents = endNodes.map(createEvent).map(function (evt) { return _extends({}, evt, { ph: EventsPhase.DURATION_EVENTS_END }); }); return [].concat(endEvents.reverse(), startEvents); } /** * Creates B/E-style trace events from a CpuProfile object created by `collectProfileEvents()` * @return {DurationEvent} * @throws If the length of timeDeltas array or the samples array does not match with the length of samples in Hermes Profile */ ; _proto.createStartEndEvents = function createStartEndEvents() { var profile = this._profile; var length = profile.samples.length; if (profile.timeDeltas.length !== length || profile.samples.length !== length) throw new Error("Invalid CPU profile length"); var events = []; var timestamp = profile.startTime; var lastActiveNodeIds = []; for (var i = 0; i < profile.samples.length; i++) { var nodeId = profile.samples[i]; var timeDelta = Math.max(profile.timeDeltas[i], 0); var node = this._nodesById.get(nodeId); if (!node) throw new Error("Missing node " + nodeId); timestamp += timeDelta; var activeNodeIds = this._getActiveNodeIds(nodeId); events.push.apply(events, this._createStartEndEventsForTransition(timestamp, lastActiveNodeIds, activeNodeIds)); lastActiveNodeIds = activeNodeIds; } events.push.apply(events, this._createStartEndEventsForTransition(timestamp, lastActiveNodeIds, [])); return events; } /** * Creates B/E-style trace events from a CpuProfile object created by `collectProfileEvents()` * @param {CPUProfileChunk} profile */ ; CpuProfilerModel.createStartEndEvents = function createStartEndEvents(profile) { var model = new CpuProfilerModel(profile); return model.createStartEndEvents(); } /** * Converts the Hermes Sample into a single CpuProfileChunk object for consumption * by `createStartEndEvents()`. * * @param {HermesCPUProfile} profile * @throws Profile must have at least one sample * @return {CPUProfileChunk} */ ; CpuProfilerModel.collectProfileEvents = function collectProfileEvents(profile) { if (profile.samples.length >= 0) { var samples = profile.samples, stackFrames = profile.stackFrames; // Assumption: The sample will have a single process var pid = samples[0].pid; // Assumption: Javascript is single threaded, so there should only be one thread throughout var tid = samples[0].tid; // TODO: What role does id play in string parsing var id = '0x1'; var startTime = Number(samples[0].ts); var _this$constructNodes = this.constructNodes(samples, stackFrames), nodes = _this$constructNodes.nodes, sampleNumbers = _this$constructNodes.sampleNumbers, timeDeltas = _this$constructNodes.timeDeltas; return { id: id, pid: pid, tid: tid, startTime: startTime, nodes: nodes, samples: sampleNumbers, timeDeltas: timeDeltas }; } else { throw new Error('The hermes profile has zero samples'); } } /** * Constructs CPUProfileChunk Nodes and the resultant samples and time deltas to be inputted into the * CPUProfileChunk object which will be processed to give createStartEndEvents() * * @param {HermesSample} samples * @param {} stackFrames * @return {CPUProfileChunker} */ ; CpuProfilerModel.constructNodes = function constructNodes(samples, stackFrames) { samples = samples.map(function (sample) { sample.stackFrameData = stackFrames[sample.sf]; return sample; }); var stackFrameIds = Object.keys(stackFrames); var profileNodes = stackFrameIds.map(function (stackFrameId) { var stackFrame = stackFrames[stackFrameId]; return { id: Number(stackFrameId), callFrame: _extends({}, stackFrame, { url: stackFrame.name }), parent: stackFrames[stackFrameId].parent }; }); var returnedSamples = []; var timeDeltas = []; var lastTimeStamp = Number(samples[0].ts); samples.forEach(function (sample, idx) { returnedSamples.push(sample.sf); if (idx === 0) { timeDeltas.push(0); } else { var timeDiff = Number(sample.ts) - lastTimeStamp; lastTimeStamp = Number(sample.ts); timeDeltas.push(timeDiff); } }); return { nodes: profileNodes, sampleNumbers: returnedSamples, timeDeltas: timeDeltas }; }; return CpuProfilerModel; }(); // A type of promise-like that resolves synchronously and supports only one observer const _iteratorSymbol = /*#__PURE__*/ typeof Symbol !== "undefined" ? (Symbol.iterator || (Symbol.iterator = Symbol("Symbol.iterator"))) : "@@iterator"; const _asyncIteratorSymbol = /*#__PURE__*/ typeof Symbol !== "undefined" ? (Symbol.asyncIterator || (Symbol.asyncIterator = Symbol("Symbol.asyncIterator"))) : "@@asyncIterator"; // Asynchronously call a function and send errors to recovery continuation function _catch(body, recover) { try { var result = body(); } catch(e) { return recover(e); } if (result && result.then) { return result.then(void 0, recover); } return result; } var readFileAsync = function readFileAsync(path) { try { return Promise.resolve(_catch(function () { var readFileAsync = promisify(readFile); return Promise.resolve(readFileAsync(path, 'utf-8')).then(function (fileString) { if (fileString.length === 0) { throw new Error(path + " is an empty file"); } var obj = JSON.parse(fileString); return obj; }); }, function (err) { throw err; })); } catch (e) { return Promise.reject(e); } }; /** * This function is a helper to the applySourceMapsToEvents. The category allocation logic is implemented here based on the sourcemap url (if available) * @param defaultCategory The category the event is of by default without the use of Source maps * @param url The URL which can be parsed to interpret the new category of the event (depends on node_modules) */ var improveCategories = function improveCategories(defaultCategory, url) { var obtainCategory = function obtainCategory(url) { var dirs = url.substring(url.lastIndexOf(path.sep + "node_modules" + path.sep)).split(path.sep); return dirs.length > 2 && dirs[1] === 'node_modules' ? dirs[2] : defaultCategory; }; return url ? obtainCategory(url) : defaultCategory; }; /** * Enhances the function line, column and params information and event categories * based on JavaScript source maps to make it easier to associate trace events with * the application code * * Throws error if args not set up in ChromeEvents * @param {SourceMap} sourceMap * @param {DurationEvent[]} chromeEvents * @param {string} indexBundleFileName * @throws If `args` for events are not populated * @returns {DurationEvent[]} */ var applySourceMapsToEvents = function applySourceMapsToEvents(sourceMap, chromeEvents, indexBundleFileName) { try { // SEE: Should file here be an optional parameter, so take indexBundleFileName as a parameter and use // a default name of `index.bundle` var rawSourceMap = { version: Number(sourceMap.version), file: indexBundleFileName || 'index.bundle', sources: sourceMap.sources, mappings: sourceMap.mappings, names: sourceMap.names }; return Promise.resolve(new SourceMapConsumer(rawSourceMap)).then(function (consumer) { var events = chromeEvents.map(function (event) { if (event.args) { var sm = consumer.originalPositionFor({ line: Number(event.args.line), column: Number(event.args.column) }); /** * The categories can help us better visualise the profile if we modify the categories. * We change these categories only in the root level and not deeper inside the args, just so we have our * original categories as well as these modified categories (as the modified categories simply help with visualisation) */ event.cat = improveCategories(event.cat, sm.source); event.args = _extends({}, event.args, { url: sm.source, line: sm.line, column: sm.column, params: sm.name, allocatedCategory: event.cat, allocatedName: event.name }); } else { throw new Error("Source maps could not be derived for an event at " + event.ts + " and with stackFrame ID " + event.sf); } return event; }); consumer.destroy(); return events; }); } catch (e) { return Promise.reject(e); } }; /** * This transformer can take in the path of the profile, the source map (optional) and the bundle file name (optional) * and return a promise which resolves to Chrome Dev Tools compatible events * @param profilePath string * @param sourceMapPath string * @param bundleFileName string * @return Promise */ var transformer = function transformer(profilePath, sourceMapPath, bundleFileName) { try { return Promise.resolve(readFileAsync(profilePath)).then(function (hermesProfile) { var _exit = false; var profileChunk = CpuProfilerModel.collectProfileEvents(hermesProfile); var profiler = new CpuProfilerModel(profileChunk); var chromeEvents = profiler.createStartEndEvents(); var _temp = function () { if (sourceMapPath) { return Promise.resolve(readFileAsync(sourceMapPath)).then(function (sourceMap) { var events = applySourceMapsToEvents(sourceMap, chromeEvents, bundleFileName); _exit = true; return events; }); } }(); return _temp && _temp.then ? _temp.then(function (_result) { return _exit ? _result : chromeEvents; }) : _exit ? _temp : chromeEvents; }); } catch (e) { return Promise.reject(e); } }; export default transformer; //# sourceMappingURL=hermes-profile-transformer.esm.js.map