UNPKG

@sentry/react-native

Version:
258 lines 11.5 kB
import { debug, getActiveSpan, getClient, spanIsSampled, uuid4 } from '@sentry/core'; import { Platform } from 'react-native'; import { isHermesEnabled } from '../utils/environment'; import { isRootSpan } from '../utils/span'; import { NATIVE } from '../wrapper'; import { PROFILE_QUEUE } from './cache'; import { MAX_PROFILE_DURATION_MS } from './constants'; import { convertToSentryProfile } from './convertHermesProfile'; import { addProfilesToEnvelope, createHermesProfilingEvent, enrichCombinedProfileWithEventContext, findProfiledTransactionsFromEnvelope, } from './utils'; const INTEGRATION_NAME = 'HermesProfiling'; const MS_TO_NS = 1e6; const defaultOptions = { platformProfilers: true, }; /** * Profiling integration creates a profile for each transaction and adds it to the event envelope. * * @experimental */ export const hermesProfilingIntegration = (initOptions = defaultOptions) => { var _a; const usePlatformProfilers = (_a = initOptions.platformProfilers) !== null && _a !== void 0 ? _a : true; let _currentProfile; let _currentProfileTimeout; let isReady = false; const setupOnce = () => { if (isReady) { return; } isReady = true; if (!isHermesEnabled()) { debug.log('[Profiling] Hermes is not enabled, not adding profiling integration.'); return; } const client = getClient(); if (!client || typeof client.on !== 'function') { return; } _startCurrentProfileForActiveTransaction(); client.on('spanStart', _startCurrentProfile); client.on('spanEnd', _finishCurrentProfileForSpan); client.on('beforeEnvelope', (envelope) => { if (!PROFILE_QUEUE.size()) { return; } const profiledTransactions = findProfiledTransactionsFromEnvelope(envelope); if (!profiledTransactions.length) { debug.log('[Profiling] no profiled transactions found in envelope'); return; } const profilesToAddToEnvelope = []; for (const profiledTransaction of profiledTransactions) { const profile = _createProfileEventFor(profiledTransaction); if (profile) { profilesToAddToEnvelope.push(profile); } } addProfilesToEnvelope(envelope, profilesToAddToEnvelope); }); }; const _startCurrentProfileForActiveTransaction = () => { if (_currentProfile) { return; } const activeSpan = getActiveSpan(); activeSpan && _startCurrentProfile(activeSpan); }; const _startCurrentProfile = (activeSpan) => { if (!isRootSpan(activeSpan)) { return; } _finishCurrentProfile(); const shouldStartProfiling = _shouldStartProfiling(activeSpan); if (!shouldStartProfiling) { return; } _currentProfileTimeout = setTimeout(_finishCurrentProfile, MAX_PROFILE_DURATION_MS); _startNewProfile(activeSpan); }; const _shouldStartProfiling = (activeSpan) => { var _a; if (!spanIsSampled(activeSpan)) { debug.log('[Profiling] Transaction is not sampled, skipping profiling'); return false; } const client = getClient(); const options = (_a = client === null || client === void 0 ? void 0 : client.getOptions) === null || _a === void 0 ? void 0 : _a.call(client); const profilesSampleRate = options && typeof options.profilesSampleRate === 'number' ? options.profilesSampleRate : undefined; if (profilesSampleRate === undefined) { debug.log('[Profiling] Profiling disabled, enable it by setting `profilesSampleRate` option to SDK init call.'); return false; } // Check if we should sample this profile if (Math.random() > profilesSampleRate) { debug.log('[Profiling] Skip profiling transaction due to sampling.'); return false; } return true; }; /** * Starts a new profile and links it to the transaction. */ const _startNewProfile = (activeSpan) => { const profileStartTimestampNs = startProfiling(usePlatformProfilers); if (!profileStartTimestampNs) { return; } _currentProfile = { span_id: activeSpan.spanContext().spanId, profile_id: uuid4(), startTimestampNs: profileStartTimestampNs, }; activeSpan.setAttribute('profile_id', _currentProfile.profile_id); debug.log('[Profiling] started profiling: ', _currentProfile.profile_id); }; /** * Stops current profile if the ending span is the currently profiled span. */ const _finishCurrentProfileForSpan = (span) => { if (!isRootSpan(span)) { return; } if (span.spanContext().spanId !== (_currentProfile === null || _currentProfile === void 0 ? void 0 : _currentProfile.span_id)) { debug.log(`[Profiling] Span (${span.spanContext().spanId}) ended is not the currently profiled span (${_currentProfile === null || _currentProfile === void 0 ? void 0 : _currentProfile.span_id}). Not stopping profiling.`); return; } _finishCurrentProfile(); }; /** * Stops profiling and adds the profile to the queue to be processed on beforeEnvelope. */ const _finishCurrentProfile = () => { _clearCurrentProfileTimeout(); if (_currentProfile === undefined) { return; } const profile = stopProfiling(_currentProfile.startTimestampNs); if (!profile) { debug.warn('[Profiling] Stop failed. Cleaning up...'); _currentProfile = undefined; return; } PROFILE_QUEUE.add(_currentProfile.profile_id, profile); debug.log('[Profiling] finished profiling: ', _currentProfile.profile_id); _currentProfile = undefined; }; const _createProfileEventFor = (profiledTransaction) => { var _a, _b, _c, _d, _e, _f; const profile_id = (_c = (_b = (_a = profiledTransaction === null || profiledTransaction === void 0 ? void 0 : profiledTransaction.contexts) === null || _a === void 0 ? void 0 : _a.trace) === null || _b === void 0 ? void 0 : _b.data) === null || _c === void 0 ? void 0 : _c.profile_id; if (typeof profile_id !== 'string') { debug.log('[Profiling] cannot find profile for a transaction without a profile context'); return null; } // Remove the profile from the transaction context before sending, relay will take care of the rest. if ((_f = (_e = (_d = profiledTransaction === null || profiledTransaction === void 0 ? void 0 : profiledTransaction.contexts) === null || _d === void 0 ? void 0 : _d.trace) === null || _e === void 0 ? void 0 : _e.data) === null || _f === void 0 ? void 0 : _f.profile_id) { delete profiledTransaction.contexts.trace.data.profile_id; } const profile = PROFILE_QUEUE.get(profile_id); PROFILE_QUEUE.delete(profile_id); if (!profile) { debug.log(`[Profiling] cannot find profile ${profile_id} for transaction ${profiledTransaction.event_id}`); return null; } const profileWithEvent = enrichCombinedProfileWithEventContext(profile_id, profile, profiledTransaction); debug.log(`[Profiling] Created profile ${profile_id} for transaction ${profiledTransaction.event_id}`); return profileWithEvent; }; const _clearCurrentProfileTimeout = () => { _currentProfileTimeout !== undefined && clearTimeout(_currentProfileTimeout); _currentProfileTimeout = undefined; }; return { name: INTEGRATION_NAME, setupOnce, }; }; /** * Starts Profilers and returns the timestamp when profiling started in nanoseconds. */ export function startProfiling(platformProfilers) { const started = NATIVE.startProfiling(platformProfilers); if (!started) { return null; } return Date.now() * MS_TO_NS; } /** * Stops Profilers and returns collected combined profile. */ export function stopProfiling(profileStartTimestampNs) { const collectedProfiles = NATIVE.stopProfiling(); if (!collectedProfiles) { return null; } const profileEndTimestampNs = Date.now() * MS_TO_NS; const hermesProfile = convertToSentryProfile(collectedProfiles.hermesProfile); if (!hermesProfile) { return null; } const hermesProfileEvent = createHermesProfilingEvent(hermesProfile); if (!hermesProfileEvent) { return null; } if (collectedProfiles.androidProfile) { const durationNs = profileEndTimestampNs - profileStartTimestampNs; return createAndroidWithHermesProfile(hermesProfileEvent, collectedProfiles.androidProfile, durationNs); } else if (collectedProfiles.nativeProfile) { return addNativeProfileToHermesProfile(hermesProfileEvent, collectedProfiles.nativeProfile); } return hermesProfileEvent; } /** * Creates Android profile event with attached javascript profile. */ export function createAndroidWithHermesProfile(hermes, nativeAndroid, durationNs) { return Object.assign(Object.assign({}, nativeAndroid), { platform: 'android', js_profile: hermes.profile, duration_ns: durationNs.toString(10), active_thread_id: hermes.transaction.active_thread_id }); } /** * Merges Hermes and Native profile events into one. */ export function addNativeProfileToHermesProfile(hermes, native) { var _a; return Object.assign(Object.assign(Object.assign(Object.assign({}, hermes), { profile: addNativeThreadCpuProfileToHermes(hermes.profile, native.profile, hermes.transaction.active_thread_id) }), (((_a = native.debug_meta) === null || _a === void 0 ? void 0 : _a.images) ? { debug_meta: { images: native.debug_meta.images } } : {})), { measurements: native.measurements }); } /** * Merges Hermes And Native profiles into one. */ export function addNativeThreadCpuProfileToHermes(hermes, native, hermes_active_thread_id) { // assumes thread ids are unique hermes.thread_metadata = Object.assign(Object.assign({}, native.thread_metadata), hermes.thread_metadata); // assumes queue ids are unique hermes.queue_metadata = Object.assign(Object.assign({}, native.queue_metadata), hermes.queue_metadata); // recalculate frames and stacks using offset const framesOffset = hermes.frames.length; const stacksOffset = hermes.stacks.length; if (native.frames) { for (const frame of native.frames) { hermes.frames.push({ function: frame.function, instruction_addr: frame.instruction_addr, platform: Platform.OS === 'ios' ? 'cocoa' : undefined, }); } } hermes.stacks = [ ...(hermes.stacks || []), ...(native.stacks || []).map(stack => stack.map(frameId => frameId + framesOffset)), ]; hermes.samples = [ ...(hermes.samples || []), ...(native.samples || []) .filter(sample => sample.thread_id !== hermes_active_thread_id) .map(sample => (Object.assign(Object.assign({}, sample), { stack_id: stacksOffset + sample.stack_id }))), ]; return hermes; } //# sourceMappingURL=integration.js.map