UNPKG

@margelo/hermes-profile-transformer

Version:
123 lines (117 loc) 4.92 kB
import path from 'path'; import { SourceMapConsumer, RawSourceMap } from 'source-map'; import { DurationEvent } from '../types/EventInterfaces'; import { SourceMap } from '../types/SourceMap'; /** * This function is a helper to the applySourceMapsToEvents. The node_module identification logic is implemented here based on the sourcemap url (if available). Incase a node_module could not be found, this defaults to the category of the event * @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) */ const findNodeModuleNameIfExists = ( defaultCategory: string, url: string | null ): string => { const obtainCategory = (url: string): string => { const 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; }; /** * The unification of categories is important as we want identify the specific reasons why the application slows down, namely via unoptimised native/JS code, or react-native renders or third party modules. The common colours for node_modules can help idenitfy problems instantly * @param nodeModuleName The node module name associated with the event obtained via sourcemap, this nodeModule name is simply the output of @see findNodeModuleNameIfExists */ const improveCategories = ( nodeModuleName: string, defaultCategory: string ): string => { // The nodeModuleName obtained from `findNodeModuleNameIfExists` by default is the original category name in the generated Hermes Profile. If we cannot isolate a nodeModule name, we simply return with the default category if (nodeModuleName === defaultCategory) { return defaultCategory; } // The events from these modules will fall under the umbrella of react-native events and hence be represented by the same colour const reactNativeModuleNames = ['react-native', 'react', 'metro']; if (reactNativeModuleNames.includes(nodeModuleName)) { return 'react-native-internals'; } else { return 'other_node_modules'; } }; /** * 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[]} */ const applySourceMapsToEvents = async ( sourceMap: SourceMap, chromeEvents: DurationEvent[], indexBundleFileName: string | undefined ): Promise<DurationEvent[]> => { // SEE: Should file here be an optional parameter, so take indexBundleFileName as a parameter and use // a default name of `index.bundle` const rawSourceMap: RawSourceMap = { version: Number(sourceMap.version), file: indexBundleFileName || 'index.bundle', sources: sourceMap.sources, mappings: sourceMap.mappings, names: sourceMap.names, }; const consumer = await new SourceMapConsumer(rawSourceMap); const events = chromeEvents.map((event: DurationEvent) => { if (event.args) { const sm = consumer.originalPositionFor({ line: Number(event.args.line), column: Number(event.args.column), }); /** * The categories can help us better visualize 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 visualization) */ const nodeModuleNameIfAvailable = findNodeModuleNameIfExists( event.cat!, sm.source ); const name = sm.source ? event.name?.replace( /\(.*\)/, `(${path .relative(process.cwd(), sm.source) .replace(/^.*\/node_modules\//, '')})` ) : event.name; event.cat = improveCategories(nodeModuleNameIfAvailable, event.cat!); event.args = { ...event.args, name, url: sm.source, line: sm.line, column: sm.column, params: sm.name, allocatedCategory: event.cat, allocatedName: event.name, node_module: nodeModuleNameIfAvailable, }; event.name = 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; }; export default applySourceMapsToEvents;