123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535 |
- 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<number, CPUProfileChunkNode}
- */
- var _proto = CpuProfilerModel.prototype;
- _proto._createNodeMap = function _createNodeMap() {
- /** @type {Map<number, CpuProfile['nodes'][0]>} */
- 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<number, number[]>
- */
- ;
- _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<number>} previousNodeIds
- * @param {Array<number>} currentNodeIds
- * @returns {Array<DurationEvent>}
- */
- ;
- _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 {<string, HermesStackFrame>} 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<DurationEvent[]>
- */
- 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
|