Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions packages/profiling-node/src/integration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
} from '@sentry/core';
import type { NodeClient, NodeOptions } from '@sentry/node';
import { CpuProfilerBindings, ProfileFormat, type RawThreadCpuProfile } from '@sentry-internal/node-cpu-profiler';
import { isMainThread } from 'worker_threads';
import { DEBUG_BUILD } from './debug-build';
import { NODE_MAJOR } from './nodeVersion';
import { MAX_PROFILE_DURATION_MS, maybeProfileSpan, stopSpanProfile } from './spanProfileUtils';
Expand Down Expand Up @@ -62,6 +63,14 @@ class ContinuousProfiler {
* @param client
*/
public initialize(client: NodeClient): void {
if (!isMainThread) {
DEBUG_BUILD &&
debug.warn(
'[Profiling] nodeProfilingIntegration() does not support worker threads — profiling will be disabled for this thread.',
);
return;
}

this._client = client;
const options = client.getOptions();

Expand Down
8 changes: 8 additions & 0 deletions packages/profiling-node/src/spanProfileUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import type { CustomSamplingContext, Span } from '@sentry/core';
import { debug, spanIsSampled, spanToJSON, uuid4 } from '@sentry/core';
import type { NodeClient } from '@sentry/node';
import { CpuProfilerBindings, type RawThreadCpuProfile } from '@sentry-internal/node-cpu-profiler';
import { isMainThread } from 'worker_threads';
import { DEBUG_BUILD } from './debug-build';
import { isValidSampleRate } from './utils';

Expand All @@ -17,6 +18,13 @@ export function maybeProfileSpan(
span: Span,
customSamplingContext?: CustomSamplingContext,
): string | undefined {
// Profiling is not supported in worker threads as the native CPU profiler's
// sampling thread can race with V8's GC across isolates, causing heap corruption.
if (!isMainThread) {
DEBUG_BUILD && debug.log('[Profiling] Skipping span profiling in worker thread.');
return;
}

// profilesSampleRate is multiplied with tracesSampleRate to get the final sampling rate. We dont perform
// the actual multiplication to get the final rate, but we discard the profile if the span was sampled,
// so anything after this block from here is based on the span sampling.
Expand Down
92 changes: 57 additions & 35 deletions packages/profiling-node/test/integration.worker.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { ProfilingIntegration, Transport } from '@sentry/core';
import * as Sentry from '@sentry/node';
import { expect, it, vi } from 'vitest';
import { CpuProfilerBindings } from '@sentry-internal/node-cpu-profiler';
import { afterEach, describe, expect, it, vi } from 'vitest';
import { _nodeProfilingIntegration } from '../src/integration';

// Mock the modules before the import, so that the value is initialized before the module is loaded
Expand All @@ -12,7 +13,7 @@ vi.mock('worker_threads', () => {
});
vi.setConfig({ testTimeout: 10_000 });

function makeContinuousProfilingClient(): [Sentry.NodeClient, Transport] {
function makeClient(options: Partial<Sentry.NodeOptions> = {}): [Sentry.NodeClient, Transport] {
const integration = _nodeProfilingIntegration();
const client = new Sentry.NodeClient({
stackParser: Sentry.defaultStackParser,
Expand All @@ -28,48 +29,69 @@ function makeContinuousProfilingClient(): [Sentry.NodeClient, Transport] {
return undefined;
},
}),
...options,
});

return [client, client.getTransport() as Transport];
}

it('worker threads context', () => {
const [client, transport] = makeContinuousProfilingClient();
Sentry.setCurrentClient(client);
client.init();
describe('worker threads', () => {
afterEach(() => {
vi.restoreAllMocks();
});

it('does not start continuous profiling in worker threads', () => {
const startProfilingSpy = vi.spyOn(CpuProfilerBindings, 'startProfiling');

const [client] = makeClient();
Sentry.setCurrentClient(client);
client.init();

const integration = client.getIntegrationByName<ProfilingIntegration<any>>('ProfilingIntegration');
if (!integration) {
throw new Error('Profiling integration not found');
}

const transportSpy = vi.spyOn(transport, 'send').mockReturnValue(Promise.resolve({}));
// Calling start should be a no-op in a worker thread
integration._profiler.start();

const nonProfiledTransaction = Sentry.startInactiveSpan({ forceTransaction: true, name: 'profile_hub' });
nonProfiledTransaction.end();
const transaction = Sentry.startInactiveSpan({ forceTransaction: true, name: 'profile_hub' });
transaction.end();

expect(transportSpy.mock.calls?.[0]?.[0]?.[1]?.[0]?.[1]).not.toMatchObject({
contexts: {
profile: {},
},
// The native profiler should never have been called
expect(startProfilingSpy).not.toHaveBeenCalled();

integration._profiler.stop();
});

const integration = client.getIntegrationByName<ProfilingIntegration<any>>('ProfilingIntegration');
if (!integration) {
throw new Error('Profiling integration not found');
}

integration._profiler.start();
const profiledTransaction = Sentry.startInactiveSpan({ forceTransaction: true, name: 'profile_hub' });
profiledTransaction.end();
integration._profiler.stop();

expect(transportSpy.mock.calls?.[1]?.[0]?.[1]?.[0]?.[1]).toMatchObject({
contexts: {
trace: {
data: expect.objectContaining({
['thread.id']: '9999',
['thread.name']: 'worker',
}),
},
profile: {
profiler_id: expect.any(String),
},
},
it('does not start span profiling in worker threads', () => {
const startProfilingSpy = vi.spyOn(CpuProfilerBindings, 'startProfiling');

const [client] = makeClient({ profilesSampleRate: 1 });
Sentry.setCurrentClient(client);
client.init();

const transaction = Sentry.startInactiveSpan({ forceTransaction: true, name: 'profile_hub' });
transaction.end();

// The native profiler should never have been called even with profilesSampleRate set
expect(startProfilingSpy).not.toHaveBeenCalled();
});

it('does not start trace lifecycle profiling in worker threads', () => {
const startProfilingSpy = vi.spyOn(CpuProfilerBindings, 'startProfiling');

const [client] = makeClient({
profileSessionSampleRate: 1.0,
profileLifecycle: 'trace',
});
Sentry.setCurrentClient(client);
client.init();

const transaction = Sentry.startInactiveSpan({ forceTransaction: true, name: 'profile_hub' });
transaction.end();

// The native profiler should never have been called
expect(startProfilingSpy).not.toHaveBeenCalled();
});
});
Loading