Skip to content

Commit ea38195

Browse files
committed
prepare profiling for manual mode
1 parent 47b89b1 commit ea38195

File tree

2 files changed

+58
-44
lines changed

2 files changed

+58
-44
lines changed

packages/browser/src/profiling/UIProfiler.ts

Lines changed: 57 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -9,28 +9,23 @@ import {
99
getSdkMetadataForEnvelopeHeader,
1010
uuid4,
1111
} from '@sentry/core';
12-
import { DEBUG_BUILD } from '../../debug-build';
13-
import type { JSSelfProfiler } from '../jsSelfProfiling';
14-
import { createProfileChunkPayload, startJSSelfProfile, validateProfileChunk } from '../utils';
12+
import { DEBUG_BUILD } from './../debug-build';
13+
import type { JSSelfProfiler } from './jsSelfProfiling';
14+
import { createProfileChunkPayload, startJSSelfProfile, validateProfileChunk } from './utils';
1515

16-
// Unified constants (kept identical to previous implementations)
1716
const CHUNK_INTERVAL_MS = 60_000; // 1 minute
17+
// Maximum length for trace lifecycle profiling per root span (e.g. if spanEnd never fires)
1818
const MAX_ROOT_SPAN_PROFILE_MS = 300_000; // 5 minutes max per root span in trace mode
1919

2020
/**
21-
* UIProfiler (Browser UI Profiling / Profiling V2)
21+
* UIProfiler (Profiling V2):
2222
* Supports two lifecycle modes:
2323
* - 'manual': controlled explicitly via start()/stop()
2424
* - 'trace': automatically runs while there are active sampled root spans
2525
*
26-
* While running (either mode), we periodically stop and restart the JS self-profiling API
27-
* to emit standalone `profile_chunk` envelopes every 60s and when profiling stops.
28-
*
29-
* Public API surface (used by integration and user-facing profiler hooks):
30-
* - initialize(client, sessionSampled, lifecycleMode)
31-
* - start()
32-
* - stop()
33-
* - notifyRootSpanActive(span) (only meaningful in 'trace' mode)
26+
* Profiles are emitted as standalone `profile_chunk` envelopes either when:
27+
* - there are no more sampled root spans, or
28+
* - the 60s chunk timer elapses while profiling is running.
3429
*/
3530
export class UIProfiler {
3631
private _client: Client | undefined;
@@ -63,6 +58,7 @@ export class UIProfiler {
6358

6459
/** Initialize the profiler with client, session sampling and lifecycle mode. */
6560
public initialize(client: Client, sessionSampled: boolean, lifecycleMode: 'manual' | 'trace'): void {
61+
// One Profiler ID per profiling session (user session)
6662
this._profilerId = uuid4();
6763

6864
DEBUG_BUILD && debug.log(`[Profiling] Initializing profiler (lifecycle='${lifecycleMode}').`);
@@ -71,10 +67,6 @@ export class UIProfiler {
7167
this._sessionSampled = sessionSampled;
7268
this._lifecycleMode = lifecycleMode;
7369

74-
if (!sessionSampled) {
75-
DEBUG_BUILD && debug.log('[Profiling] Session not sampled; profiler will remain inactive.');
76-
}
77-
7870
if (lifecycleMode === 'trace') {
7971
this._setupTraceLifecycleListeners(client);
8072
}
@@ -118,16 +110,17 @@ export class UIProfiler {
118110
}
119111

120112
/** Handle an already-active root span at integration setup time (used only in trace mode). */
121-
public notifyRootSpanActive(span: Span): void {
113+
public notifyRootSpanActive(rootSpan: Span): void {
122114
if (this._lifecycleMode !== 'trace' || !this._sessionSampled) {
123115
return;
124116
}
125117

126-
const spanId = span.spanContext().spanId;
118+
const spanId = rootSpan.spanContext().spanId;
127119
if (!spanId || this._activeRootSpanIds.has(spanId)) {
128120
return;
129121
}
130-
this._registerTraceRootSpan(spanId);
122+
123+
this._activeRootSpanIds.add(spanId);
131124

132125
const rootSpanCount = this._activeRootSpanIds.size;
133126

@@ -176,8 +169,9 @@ export class UIProfiler {
176169

177170
this._clearAllRootSpanTimeouts();
178171

172+
// Collect whatever was currently recording
179173
this._collectCurrentChunk().catch(e => {
180-
DEBUG_BUILD && debug.error('[Profiling] Failed to collect current profile chunk on stop():', e);
174+
DEBUG_BUILD && debug.error('[Profiling] Failed to collect current profile chunk on `stop()`:', e);
181175
});
182176

183177
// Clear context so subsequent events aren't marked as profiled in manual mode.
@@ -204,7 +198,7 @@ export class UIProfiler {
204198
}
205199

206200
// Match emitted chunks with events: set profiler_id on global scope
207-
// do I need this?
201+
// todo: do I need this?
208202
// getGlobalScope().setContext('profile', { profiler_id: this._profilerId });
209203

210204
const spanId = span.spanContext().spanId;
@@ -254,9 +248,9 @@ export class UIProfiler {
254248
getGlobalScope().setContext('profile', {});
255249
}
256250

257-
/** Clear all trace-mode root span timeouts. */
251+
/** Clear and reset all per-root-span timeouts. */
258252
private _clearAllRootSpanTimeouts(): void {
259-
this._rootSpanTimeouts.forEach(t => clearTimeout(t));
253+
this._rootSpanTimeouts.forEach(timeout => clearTimeout(timeout));
260254
this._rootSpanTimeouts.clear();
261255
}
262256

@@ -267,57 +261,69 @@ export class UIProfiler {
267261
this._rootSpanTimeouts.set(spanId, timeout);
268262
}
269263

270-
/** Start JS self profiler instance if needed. */
264+
/** Start a profiler instance if needed. */
271265
private _startProfilerInstance(): void {
272266
if (this._profiler?.stopped === false) {
273267
return; // already running
274268
}
275269
const profiler = startJSSelfProfile();
276270
if (!profiler) {
277-
DEBUG_BUILD && debug.log('[Profiling] Failed to start JS self profiler.');
271+
DEBUG_BUILD && debug.log('[Profiling] Failed to start JS Profiler.');
278272
return;
279273
}
280274
this._profiler = profiler;
281275
}
282276

283-
/** Schedule periodic chunk collection while running. */
277+
/**
278+
* Schedule the next 60s chunk while running.
279+
* Each tick collects a chunk and restarts the profiler.
280+
* A chunk should be closed when there are no active root spans anymore OR when the maximum chunk interval is reached.
281+
*/
284282
private _startPeriodicChunking(): void {
285283
if (!this._isRunning) {
286284
return;
287285
}
288286

289287
this._chunkTimer = setTimeout(() => {
290288
this._collectCurrentChunk().catch(e => {
291-
DEBUG_BUILD && debug.error('[Profiling] Failed to collect profile chunk during periodic chunking:', e);
289+
DEBUG_BUILD && debug.error('[Profiling] Failed to collect current profile chunk during periodic chunking:', e);
292290
});
293291

294292
if (this._isRunning) {
295293
this._startProfilerInstance();
294+
296295
if (!this._profiler) {
297-
// Could not restart -> stop profiling gracefully
296+
// If restart failed, stop scheduling further chunks and reset context.
298297
this._resetProfilerInfo();
299298
return;
300299
}
300+
301301
this._startPeriodicChunking();
302302
}
303303
}, CHUNK_INTERVAL_MS);
304304
}
305305

306-
/** Root span timeout handler (trace mode). */
307-
private _onRootSpanTimeout(spanId: string): void {
308-
if (!this._rootSpanTimeouts.has(spanId)) {
309-
return; // span already ended
306+
/**
307+
* Handle timeout for a specific root span ID to avoid indefinitely running profiler if `spanEnd` never fires.
308+
* If this was the last active root span, collect the current chunk and stop profiling.
309+
*/
310+
private _onRootSpanTimeout(rootSpanId: string): void {
311+
// If span already ended, ignore
312+
if (!this._rootSpanTimeouts.has(rootSpanId)) {
313+
return;
310314
}
311-
this._rootSpanTimeouts.delete(spanId);
315+
this._rootSpanTimeouts.delete(rootSpanId);
312316

313-
if (!this._activeRootSpanIds.has(spanId)) {
317+
if (!this._activeRootSpanIds.has(rootSpanId)) {
314318
return;
315319
}
316320

317321
DEBUG_BUILD &&
318-
debug.log(`[Profiling] Reached 5-minute timeout for root span ${spanId}. Did you forget to call .end()?`);
322+
debug.log(
323+
`[Profiling] Reached 5-minute timeout for root span ${rootSpanId}. You likely started a manual root span that never called \`.end()\`.`,
324+
);
319325

320-
this._activeRootSpanIds.delete(spanId);
326+
this._activeRootSpanIds.delete(rootSpanId);
321327

322328
if (this._activeRootSpanIds.size === 0) {
323329
this._endProfiling();
@@ -326,25 +332,32 @@ export class UIProfiler {
326332

327333
/** Stop current profiler instance, convert profile to chunk & send. */
328334
private async _collectCurrentChunk(): Promise<void> {
329-
const prev = this._profiler;
335+
const prevProfiler = this._profiler;
330336
this._profiler = undefined;
331-
if (!prev) {
337+
338+
if (!prevProfiler) {
332339
return;
333340
}
341+
334342
try {
335-
const profile = await prev.stop();
343+
const profile = await prevProfiler.stop();
344+
336345
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
337346
const chunk = createProfileChunkPayload(profile, this._client!, this._profilerId);
338-
const validation = validateProfileChunk(chunk);
339-
if ('reason' in validation) {
347+
348+
// Validate chunk before sending
349+
const validationReturn = validateProfileChunk(chunk);
350+
if ('reason' in validationReturn) {
340351
DEBUG_BUILD &&
341352
debug.log(
342353
'[Profiling] Discarding invalid profile chunk (this is probably a bug in the SDK):',
343-
validation.reason,
354+
validationReturn.reason,
344355
);
345356
return;
346357
}
358+
347359
this._sendProfileChunk(chunk);
360+
348361
DEBUG_BUILD && debug.log('[Profiling] Collected browser profile chunk.');
349362
} catch (e) {
350363
DEBUG_BUILD && debug.log('[Profiling] Error while stopping JS Profiler for chunk:', e);
@@ -355,6 +368,7 @@ export class UIProfiler {
355368
private _sendProfileChunk(chunk: ProfileChunk): void {
356369
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
357370
const client = this._client!;
371+
358372
const sdkInfo = getSdkMetadataForEnvelopeHeader(client.getSdkMetadata?.());
359373
const dsn = client.getDsn();
360374
const tunnel = client.getOptions().tunnel;

packages/browser/src/profiling/integration.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ const _browserProfilingIntegration = (() => {
6666
}
6767

6868
const traceLifecycleProfiler = new UIProfiler();
69-
traceLifecycleProfiler.initialize(client, sessionSampled);
69+
traceLifecycleProfiler.initialize(client, sessionSampled, lifecycleMode);
7070

7171
// If there is an active, sampled root span already, notify the profiler
7272
if (rootSpan) {

0 commit comments

Comments
 (0)