GT2/Ejectable/node_modules/hermes-profile-transformer/dist/hermes-profile-transformer....

536 lines
18 KiB
JavaScript

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