-
-
Notifications
You must be signed in to change notification settings - Fork 360
Expand file tree
/
Copy pathintegration.ts
More file actions
369 lines (310 loc) · 11 KB
/
integration.ts
File metadata and controls
369 lines (310 loc) · 11 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
import type { Envelope, Event, Integration, Span, ThreadCpuProfile } from '@sentry/core';
import { debug, getActiveSpan, getClient, spanIsSampled, uuid4 } from '@sentry/core';
import { Platform } from 'react-native';
import type { ReactNativeClient } from '../client';
import type { NativeAndroidProfileEvent, NativeProfileEvent } from './nativeTypes';
import type { AndroidCombinedProfileEvent, CombinedProfileEvent, HermesProfileEvent, ProfileEvent } from './types';
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: number = 1e6;
export interface HermesProfilingOptions {
/**
* Enable or disable profiling of native (iOS and Android) threads
*
* @default true
*/
platformProfilers?: boolean;
}
const defaultOptions: Required<HermesProfilingOptions> = {
platformProfilers: true,
};
/**
* Profiling integration creates a profile for each transaction and adds it to the event envelope.
*
* @experimental
*/
export const hermesProfilingIntegration = (initOptions: HermesProfilingOptions = defaultOptions): Integration => {
const usePlatformProfilers = initOptions.platformProfilers ?? true;
let _currentProfile:
| {
span_id: string;
profile_id: string;
startTimestampNs: number;
}
| undefined;
let _currentProfileTimeout: ReturnType<typeof setTimeout> | undefined;
let isReady: boolean = false;
const setupOnce = (): void => {
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: 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: ProfileEvent[] = [];
for (const profiledTransaction of profiledTransactions) {
const profile = _createProfileEventFor(profiledTransaction);
if (profile) {
profilesToAddToEnvelope.push(profile);
}
}
addProfilesToEnvelope(envelope, profilesToAddToEnvelope);
});
};
const _startCurrentProfileForActiveTransaction = (): void => {
if (_currentProfile) {
return;
}
const activeSpan = getActiveSpan();
activeSpan && _startCurrentProfile(activeSpan);
};
const _startCurrentProfile = (activeSpan: Span): void => {
if (!isRootSpan(activeSpan)) {
return;
}
_finishCurrentProfile();
const shouldStartProfiling = _shouldStartProfiling(activeSpan);
if (!shouldStartProfiling) {
return;
}
_currentProfileTimeout = setTimeout(_finishCurrentProfile, MAX_PROFILE_DURATION_MS);
_startNewProfile(activeSpan);
};
const _shouldStartProfiling = (activeSpan: Span): boolean => {
if (!spanIsSampled(activeSpan)) {
debug.log('[Profiling] Transaction is not sampled, skipping profiling');
return false;
}
const client = getClient<ReactNativeClient>();
const options = client?.getOptions?.();
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: Span): void => {
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: Span): void => {
if (!isRootSpan(span)) {
return;
}
if (span.spanContext().spanId !== _currentProfile?.span_id) {
debug.log(
`[Profiling] Span (${span.spanContext().spanId}) ended is not the currently profiled span (${
_currentProfile?.span_id
}). Not stopping profiling.`,
);
return;
}
_finishCurrentProfile();
};
/**
* Stops profiling and adds the profile to the queue to be processed on beforeEnvelope.
*/
const _finishCurrentProfile = (): void => {
_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: Event): ProfileEvent | null => {
const profile_id = profiledTransaction?.contexts?.trace?.data?.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 (profiledTransaction?.contexts?.trace?.data?.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 = (): void => {
_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: boolean): number | null {
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: number,
): CombinedProfileEvent | AndroidCombinedProfileEvent | null {
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;
}
hermesProfileEvent.profilingStartTimestampNs = profileStartTimestampNs;
if (collectedProfiles.androidProfile) {
const durationNs = profileEndTimestampNs - profileStartTimestampNs;
const androidProfile = createAndroidWithHermesProfile(hermesProfileEvent, collectedProfiles.androidProfile, durationNs);
androidProfile.profilingStartTimestampNs = profileStartTimestampNs;
return androidProfile;
} else if (collectedProfiles.nativeProfile) {
return addNativeProfileToHermesProfile(hermesProfileEvent, collectedProfiles.nativeProfile);
}
return hermesProfileEvent;
}
/**
* Creates Android profile event with attached javascript profile.
*/
export function createAndroidWithHermesProfile(
hermes: HermesProfileEvent,
nativeAndroid: NativeAndroidProfileEvent,
durationNs: number,
): AndroidCombinedProfileEvent {
return {
...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: HermesProfileEvent,
native: NativeProfileEvent,
): CombinedProfileEvent {
return {
...hermes,
profile: addNativeThreadCpuProfileToHermes(hermes.profile, native.profile, hermes.transaction.active_thread_id),
...(native.debug_meta?.images ? { debug_meta: { images: native.debug_meta.images } } : {}),
measurements: native.measurements,
};
}
/**
* Merges Hermes And Native profiles into one.
*/
export function addNativeThreadCpuProfileToHermes(
hermes: ThreadCpuProfile,
native: ThreadCpuProfile,
hermes_active_thread_id: string | undefined,
): CombinedProfileEvent['profile'] {
// assumes thread ids are unique
hermes.thread_metadata = { ...native.thread_metadata, ...hermes.thread_metadata };
// assumes queue ids are unique
hermes.queue_metadata = { ...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 => ({
...sample,
stack_id: stacksOffset + sample.stack_id,
})),
];
return hermes;
}