Skip to content

Commit d62e3aa

Browse files
committed
ref(profiling): move profiling under browser package
1 parent 3f9220c commit d62e3aa

22 files changed

+231
-247
lines changed

package.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,6 @@
4949
"packages/node",
5050
"packages/node-integration-tests",
5151
"packages/opentelemetry-node",
52-
"packages/profiling-browser",
5352
"packages/react",
5453
"packages/remix",
5554
"packages/replay",

packages/browser/src/index.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,3 +32,8 @@ export { Replay } from '@sentry/replay';
3232
// __ROLLUP_EXCLUDE_OFFLINE_FROM_BUNDLES_BEGIN__
3333
export { makeBrowserOfflineTransport } from './transports/offline';
3434
// __ROLLUP_EXCLUDE_OFFLINE_FROM_BUNDLES_END__
35+
36+
// __ROLLUP_EXCLUDE_BROWSER_PROFILING_FROM_BUNDLES_BEGIN__
37+
export { wrapTransactionWithProfiling } from './profiling/hubextensions';
38+
export { BrowserProfilingIntegration } from './profiling/browserProfiling';
39+
// __ROLLUP_EXCLUDE_BROWSER_PROFILING_FROM_BUNDLES_END__

packages/profiling-browser/src/integration.ts renamed to packages/browser/src/profiling/browserProfiling.ts

Lines changed: 85 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,82 @@
11
import { getCurrentHub } from '@sentry/core';
22
import type { Event, EventProcessor, Hub, Integration } from '@sentry/types';
33
import { logger } from '@sentry/utils';
4-
import { LRUMap } from 'lru_map';
54

5+
import { addExtensionMethods } from './hubextensions';
66
import type { ProcessedJSSelfProfile } from './jsSelfProfiling';
77
import type { ProfiledEvent } from './utils';
88
import { createProfilingEventEnvelope } from './utils';
99

10-
// We need this integration in order to actually send data to Sentry. We hook into the event processor
11-
// and inspect each event to see if it is a transaction event and if that transaction event
12-
// contains a profile on it's metadata. If that is the case, we create a profiling event envelope
13-
// and delete the profile from the transaction metadata.
14-
export const PROFILING_EVENT_CACHE = new LRUMap<string, Event>(20);
10+
/**
11+
* Creates a simple cache that evicts keys in fifo order
12+
* @param size {Number}
13+
*/
14+
export function makeProfilingCache<Key extends string, Value extends Event>(
15+
size: number,
16+
): {
17+
get: (key: Key) => Value | undefined;
18+
add: (key: Key, value: Value) => void;
19+
delete: (key: Key) => boolean;
20+
clear: () => void;
21+
size: () => number;
22+
} {
23+
// Maintain a fifo queue of keys, we cannot rely on Object.keys as the browser may not support it.
24+
let evictionOrder: Key[] = [];
25+
let cache: Record<string, Value> = {};
26+
27+
return {
28+
add(key: Key, value: Value) {
29+
while (evictionOrder.length >= size) {
30+
// shift is O(n) but this is small size and only happens if we are
31+
// exceeding the cache size so it should be fine.
32+
const evictCandidate = evictionOrder.shift();
33+
34+
if (evictCandidate !== undefined) {
35+
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
36+
delete cache[evictCandidate];
37+
}
38+
}
39+
40+
// in case we have a collision, delete the old key.
41+
if (cache[key]) {
42+
this.delete(key);
43+
}
44+
45+
evictionOrder.push(key);
46+
cache[key] = value;
47+
},
48+
clear() {
49+
cache = {};
50+
evictionOrder = [];
51+
},
52+
get(key: Key): Value | undefined {
53+
return cache[key];
54+
},
55+
size() {
56+
return evictionOrder.length;
57+
},
58+
// Delete cache key and return true if it existed, false otherwise.
59+
delete(key: Key): boolean {
60+
if (!cache[key]) {
61+
return false;
62+
}
63+
64+
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
65+
delete cache[key];
66+
67+
for (let i = 0; i < evictionOrder.length; i++) {
68+
if (evictionOrder[i] === key) {
69+
evictionOrder.splice(i, 1);
70+
break;
71+
}
72+
}
73+
74+
return true;
75+
},
76+
};
77+
}
78+
79+
export const PROFILING_EVENT_CACHE = makeProfilingCache<string, Event>(20);
1580
/**
1681
* Browser profiling integration. Stores any event that has contexts["profile"]["profile_id"]
1782
* This exists because we do not want to await async profiler.stop calls as transaction.finish is called
@@ -21,27 +86,32 @@ export const PROFILING_EVENT_CACHE = new LRUMap<string, Event>(20);
2186
*/
2287
export class BrowserProfilingIntegration implements Integration {
2388
public readonly name: string = 'BrowserProfilingIntegration';
24-
public getCurrentHub?: () => Hub = undefined;
2589

2690
/**
2791
* @inheritDoc
2892
*/
29-
public setupOnce(addGlobalEventProcessor: (callback: EventProcessor) => void, getCurrentHub: () => Hub): void {
30-
this.getCurrentHub = getCurrentHub;
93+
public setupOnce(addGlobalEventProcessor: (callback: EventProcessor) => void, _getCurrentHub: () => Hub): void {
94+
// Patching the hub to add the extension methods.
95+
// Warning: we have an implicit dependency on import order and we will fail patching if the constructor of
96+
// BrowserProfilingIntegration is called before @sentry/tracing is imported. This is because we need to patch
97+
// the methods of @sentry/tracing which are patched as a side effect of importing @sentry/tracing.
98+
addExtensionMethods();
99+
100+
// Add our event processor
31101
addGlobalEventProcessor(this.handleGlobalEvent.bind(this));
32102
}
33103

34104
/**
35105
* @inheritDoc
36106
*/
37107
public handleGlobalEvent(event: Event): Event {
38-
const profile_id = event.contexts && event.contexts['profile'] && event.contexts['profile']['profile_id'];
108+
const profileId = event.contexts && event.contexts['profile'] && event.contexts['profile']['profile_id'];
39109

40-
if (profile_id && typeof profile_id === 'string') {
110+
if (profileId && typeof profileId === 'string') {
41111
if (__DEBUG_BUILD__) {
42112
logger.log('[Profiling] Profiling event found, caching it.');
43113
}
44-
PROFILING_EVENT_CACHE.set(profile_id, event);
114+
PROFILING_EVENT_CACHE.add(profileId, event);
45115
}
46116

47117
return event;
@@ -53,8 +123,8 @@ export class BrowserProfilingIntegration implements Integration {
53123
* If the profiled transaction event is found, we use the profiled transaction event and profile
54124
* to construct a profile type envelope and send it to Sentry.
55125
*/
56-
export function sendProfile(profile_id: string, profile: ProcessedJSSelfProfile): void {
57-
const event = PROFILING_EVENT_CACHE.get(profile_id);
126+
export function sendProfile(profileId: string, profile: ProcessedJSSelfProfile): void {
127+
const event = PROFILING_EVENT_CACHE.get(profileId);
58128

59129
if (!event) {
60130
// We could not find a corresponding transaction event for this profile.
@@ -112,7 +182,7 @@ export function sendProfile(profile_id: string, profile: ProcessedJSSelfProfile)
112182
const envelope = createProfilingEventEnvelope(event as ProfiledEvent, dsn);
113183

114184
// Evict event from the cache - we want to prevent the LRU cache from prioritizing already sent events over new ones.
115-
PROFILING_EVENT_CACHE.delete(profile_id);
185+
PROFILING_EVENT_CACHE.delete(profileId);
116186

117187
if (!envelope) {
118188
if (__DEBUG_BUILD__) {

packages/profiling-browser/src/hubextensions.ts renamed to packages/browser/src/profiling/hubextensions.ts

Lines changed: 28 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
1-
import { WINDOW } from '@sentry/browser';
21
import { getMainCarrier } from '@sentry/core';
32
import type { CustomSamplingContext, Hub, Transaction, TransactionContext } from '@sentry/types';
43
import { logger, uuid4 } from '@sentry/utils';
54

6-
import { sendProfile } from './integration';
5+
import { WINDOW } from '../helpers';
6+
import { sendProfile } from './browserProfiling';
77
import type { JSSelfProfile, JSSelfProfiler, ProcessedJSSelfProfile } from './jsSelfProfiling';
88

99
// Max profile duration.
@@ -34,11 +34,18 @@ export function wrapTransactionWithProfiling(
3434
transactionContext: TransactionContext,
3535
getCurrentHub: () => Hub,
3636
): Transaction | undefined {
37-
// We create "unique" transaction names to avoid concurrent transactions with same names
38-
// from being ignored by the profiler. From here on, only this transaction name should be used when
39-
// calling the profiler methods. Note: we log the original name to the user to avoid confusion.
40-
const profile_id = uuid4();
37+
// Feature support check first
38+
const JSProfiler = WINDOW.Profiler;
39+
if (!isJSProfilerSupported(JSProfiler)) {
40+
if (__DEBUG_BUILD__) {
41+
logger.log(
42+
'[Profiling] Profiling is not supported by this browser, Profiler interface missing on window object.',
43+
);
44+
}
45+
return transaction;
46+
}
4147

48+
// Check if we have a valid
4249
if (!transaction) {
4350
if (__DEBUG_BUILD__) {
4451
logger.log(`[Profiling] transaction not found, skipping profiling for: ${transactionContext.name}`);
@@ -57,18 +64,6 @@ export function wrapTransactionWithProfiling(
5764
const client = getCurrentHub().getClient();
5865
const options = client && client.getOptions();
5966

60-
const JSProfiler = WINDOW.Profiler;
61-
62-
// Feature support check
63-
if (!isJSProfilerSupported(JSProfiler)) {
64-
if (__DEBUG_BUILD__) {
65-
logger.log(
66-
'[Profiling] Profiling is not supported by this browser, Profiler interface missing on window object.',
67-
);
68-
}
69-
return transaction;
70-
}
71-
7267
// @ts-ignore not part of the browser options yet
7368
const profilesSampleRate = (options && options.profilesSampleRate) || 0;
7469
if (profilesSampleRate === undefined) {
@@ -86,16 +81,20 @@ export function wrapTransactionWithProfiling(
8681
return transaction;
8782
}
8883

89-
// Defer any profilesSamplingInterval validation to the profiler API
90-
// @ts-ignore not part of the browser options yet and might never be, but useful to control during poc stage
91-
const samplingInterval = options.profilesSamplingInterval || 10;
84+
// From initial testing, it seems that the minimum value for sampleInterval is 10ms.
85+
const samplingIntervalMS = 10;
9286
// Start the profiler
93-
const maxSamples = Math.floor(MAX_PROFILE_DURATION_MS / samplingInterval);
94-
const profiler = new JSProfiler({ sampleInterval: samplingInterval, maxBufferSize: maxSamples });
87+
const maxSamples = Math.floor(MAX_PROFILE_DURATION_MS / samplingIntervalMS);
88+
const profiler = new JSProfiler({ sampleInterval: samplingIntervalMS, maxBufferSize: maxSamples });
9589
if (__DEBUG_BUILD__) {
9690
logger.log(`[Profiling] started profiling transaction: ${transaction.name || transaction.description}`);
9791
}
9892

93+
// We create "unique" transaction names to avoid concurrent transactions with same names
94+
// from being ignored by the profiler. From here on, only this transaction name should be used when
95+
// calling the profiler methods. Note: we log the original name to the user to avoid confusion.
96+
const profileId = uuid4();
97+
9998
// A couple of important things to note here:
10099
// `CpuProfilerBindings.stopProfiling` will be scheduled to run in 30seconds in order to exceed max profile duration.
101100
// Whichever of the two (transaction.finish/timeout) is first to run, the profiling will be stopped and the gathered profile
@@ -152,8 +151,8 @@ export function wrapTransactionWithProfiling(
152151
return;
153152
}
154153

155-
processedProfile = { ...p, profile_id: profile_id };
156-
sendProfile(profile_id, processedProfile);
154+
processedProfile = { ...p, profile_id: profileId };
155+
sendProfile(profileId, processedProfile);
157156
})
158157
.catch(error => {
159158
if (__DEBUG_BUILD__) {
@@ -191,7 +190,7 @@ export function wrapTransactionWithProfiling(
191190
onProfileHandler();
192191

193192
// Set profile context
194-
transaction.setContext('profile', { profile_id });
193+
transaction.setContext('profile', { profile_id: profileId });
195194

196195
return originalFinish();
197196
}
@@ -201,9 +200,9 @@ export function wrapTransactionWithProfiling(
201200
}
202201

203202
/**
204-
*
203+
* Wraps startTransaction with profiling logic. This is done automatically by the profiling integration.
205204
*/
206-
export function __PRIVATE__wrapStartTransactionWithProfiling(startTransaction: StartTransaction): StartTransaction {
205+
function __PRIVATE__wrapStartTransactionWithProfiling(startTransaction: StartTransaction): StartTransaction {
207206
return function wrappedStartTransaction(
208207
this: Hub,
209208
transactionContext: TransactionContext,
@@ -240,6 +239,7 @@ function _addProfilingExtensionMethods(): void {
240239
if (__DEBUG_BUILD__) {
241240
logger.log('[Profiling] startTransaction exists, patching it with profiling functionality...');
242241
}
242+
243243
carrier.__SENTRY__.extensions['startTransaction'] = __PRIVATE__wrapStartTransactionWithProfiling(
244244
// This is already patched by sentry/tracing, we are going to re-patch it...
245245
carrier.__SENTRY__.extensions['startTransaction'] as StartTransaction,

packages/profiling-browser/src/utils.ts renamed to packages/browser/src/profiling/utils.ts

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import { WINDOW } from '@sentry/browser';
21
import type {
32
DsnComponents,
43
DynamicSamplingContext,
@@ -11,16 +10,20 @@ import type {
1110
} from '@sentry/types';
1211
import { createEnvelope, dropUndefinedKeys, dsnToString, logger, uuid4 } from '@sentry/utils';
1312

13+
import { WINDOW } from '../helpers';
1414
import type { JSSelfProfile, JSSelfProfileStack, RawThreadCpuProfile, ThreadCpuProfile } from './jsSelfProfiling';
1515

16+
const MS_TO_NS = 1e6;
17+
// Use 0 as main thread id which is identical to threadId in node:worker_threads
18+
// where main logs 0 and workers seem to log in increments of 1
1619
const THREAD_ID_STRING = String(0);
1720
const THREAD_NAME = 'main';
1821

1922
// Machine properties (eval only once)
2023
let OS_PLATFORM = ''; // macos
2124
let OS_PLATFORM_VERSION = ''; // 13.2
2225
let OS_ARCH = ''; // arm64
23-
let OS_BROWSER = WINDOW.navigator && WINDOW.navigator.userAgent || '';
26+
let OS_BROWSER = (WINDOW.navigator && WINDOW.navigator.userAgent) || '';
2427
let OS_MODEL = '';
2528
const OS_LOCALE =
2629
(WINDOW.navigator && WINDOW.navigator.language) || (WINDOW.navigator && WINDOW.navigator.languages[0]) || '';
@@ -203,9 +206,10 @@ function getTraceId(event: Event): string {
203206
* @param tunnel
204207
* @returns {EventEnvelope | null}
205208
*/
206-
// We will live dangerously and disable complexity here, time is of the essence.
207-
// Onwards to the next refactor my fellow engineers!
208-
// eslint-disable-next-line complexity
209+
210+
/**
211+
* Creates a profiling event envelope from a Sentry event.
212+
*/
209213
export function createProfilingEventEnvelope(
210214
event: ProfiledEvent,
211215
dsn: DsnComponents,
@@ -257,7 +261,7 @@ export function createProfilingEventEnvelope(
257261
environment: event.environment || '',
258262
runtime: {
259263
name: 'javascript',
260-
version: WINDOW.navigator.userAgent
264+
version: WINDOW.navigator.userAgent,
261265
},
262266
os: {
263267
name: OS_PLATFORM,
@@ -325,6 +329,7 @@ export function convertJSSelfProfileToSampledFormat(input: JSSelfProfile): Threa
325329
let EMPTY_STACK_ID: undefined | number = undefined;
326330
let STACK_ID = 0;
327331

332+
// Initialize the profile that we will fill with data
328333
const profile: ThreadCpuProfile = {
329334
samples: [],
330335
stacks: [],
@@ -338,9 +343,8 @@ export function convertJSSelfProfileToSampledFormat(input: JSSelfProfile): Threa
338343
return profile;
339344
}
340345

341-
// We assert samples.length > 0 above
346+
// We assert samples.length > 0 above and timestamp should always be present
342347
const start = input.samples[0].timestamp;
343-
profile.stacks = [];
344348

345349
for (let i = 0; i < input.samples.length; i++) {
346350
const jsSample = input.samples[i];
@@ -354,7 +358,8 @@ export function convertJSSelfProfileToSampledFormat(input: JSSelfProfile): Threa
354358
}
355359

356360
profile['samples'][i] = {
357-
elapsed_since_start_ns: ((jsSample.timestamp - start) * 1e6).toFixed(0),
361+
// convert ms timestamp to ns
362+
elapsed_since_start_ns: ((jsSample.timestamp - start) * MS_TO_NS).toFixed(0),
358363
stack_id: EMPTY_STACK_ID,
359364
thread_id: THREAD_ID_STRING,
360365
};
@@ -386,7 +391,8 @@ export function convertJSSelfProfileToSampledFormat(input: JSSelfProfile): Threa
386391
}
387392

388393
const sample: ThreadCpuProfile['samples'][0] = {
389-
elapsed_since_start_ns: ((jsSample.timestamp - start) * 1e6).toFixed(0),
394+
// convert ms timestamp to ns
395+
elapsed_since_start_ns: ((jsSample.timestamp - start) * MS_TO_NS).toFixed(0),
390396
stack_id: STACK_ID,
391397
thread_id: THREAD_ID_STRING,
392398
};

0 commit comments

Comments
 (0)