Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
- Notifications
You must be signed in to change notification settings - Fork 1.8k
feat(profiling) add global profile context while profiler is running#12394
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Uh oh!
There was an error while loading. Please reload this page.
Changes from all commits
bef3900484dde71fe34a75844088cbb65fe8d8fbf382cc1105092d74d2ccedfFile filter
Filter by extension
Conversations
Uh oh!
There was an error while loading. Please reload this page.
Jump to
Uh oh!
There was an error while loading. Please reload this page.
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,17 +1,20 @@ | ||
| import { defineIntegration, getCurrentScope, getIsolationScope, getRootSpan, spanToJSON } from '@sentry/core'; | ||
| import type { NodeClient } from '@sentry/node'; | ||
| import type { Integration, IntegrationFn, Profile, ProfileChunk, Span } from '@sentry/types'; | ||
| import type { Event, Integration, IntegrationFn, Profile, ProfileChunk, Span } from '@sentry/types'; | ||
| import { LRUMap, logger, timestampInSeconds, uuid4 } from '@sentry/utils'; | ||
| import { getGlobalScope } from '../../core/src/currentScopes'; | ||
| import { CpuProfilerBindings } from './cpu_profiler'; | ||
| import { DEBUG_BUILD } from './debug-build'; | ||
| import { NODE_MAJOR, NODE_VERSION } from './nodeVersion'; | ||
| import { MAX_PROFILE_DURATION_MS, maybeProfileSpan, stopSpanProfile } from './spanProfileUtils'; | ||
| import type { RawThreadCpuProfile } from './types'; | ||
| import type { RawChunkCpuProfile, RawThreadCpuProfile } from './types'; | ||
| import { ProfileFormat } from './types'; | ||
| import { PROFILER_THREAD_NAME } from './utils'; | ||
| import { | ||
| PROFILER_THREAD_ID_STRING, | ||
| addProfilesToEnvelope, | ||
| createProfilingChunkEvent, | ||
| createProfilingEvent, | ||
| @@ -211,7 +214,9 @@ class ContinuousProfiler { | ||
| logger.log(`[Profiling] Failed to collect profile for: ${this._chunkData?.id}, the chunk_id is missing.`); | ||
| return; | ||
| } | ||
| const profile = CpuProfilerBindings.stopProfiling(this._chunkData.id, ProfileFormat.CHUNK); | ||
| const profile = this._stopChunkProfiling(this._chunkData); | ||
| if (!profile || !this._chunkData.startTimestampMS) { | ||
| DEBUG_BUILD && logger.log(`[Profiling] _chunkiledStartTraceID to collect profile for: ${this._chunkData.id}`); | ||
| return; | ||
| @@ -274,12 +279,22 @@ class ContinuousProfiler { | ||
| }); | ||
| } | ||
| /** | ||
| * Stops the profile and clears chunk instrumentation from global scope | ||
| * @returns void | ||
| */ | ||
| private _stopChunkProfiling(chunk: ChunkData): RawChunkCpuProfile | null { | ||
| this._teardownSpanChunkInstrumentation(); | ||
| return CpuProfilerBindings.stopProfiling(chunk.id, ProfileFormat.CHUNK); | ||
| } | ||
| /** | ||
| * Starts the profiler and registers the flush timer for a given chunk. | ||
| * @param chunk | ||
| */ | ||
| private _startChunkProfiling(chunk: ChunkData): void { | ||
| CpuProfilerBindings.startProfiling(chunk.id!); | ||
| this._setupSpanChunkInstrumentation(); | ||
| CpuProfilerBindings.startProfiling(chunk.id); | ||
| DEBUG_BUILD && logger.log(`[Profiling] starting profiling chunk: ${chunk.id}`); | ||
| chunk.timer = global.setTimeout(() => { | ||
| @@ -293,6 +308,32 @@ class ContinuousProfiler { | ||
| chunk.timer.unref(); | ||
| } | ||
| /** | ||
| * Attaches profiling information to spans that were started | ||
| * during a profiling session. | ||
| */ | ||
| private _setupSpanChunkInstrumentation(): void { | ||
| if (!this._client) { | ||
| DEBUG_BUILD && | ||
| logger.log('[Profiling] Failed to collect profile, sentry client was never attached to the profiler.'); | ||
| return; | ||
| } | ||
| getGlobalScope().setContext('profile', { | ||
| profiler_id: this._profilerId, | ||
| }); | ||
| this._client.on('beforeSendEvent', e => this._assignThreadIdContext(e)); | ||
| } | ||
| /** | ||
| * Clear profiling information from global context when a profile is not running. | ||
| */ | ||
| private _teardownSpanChunkInstrumentation(): void { | ||
| const globalScope = getGlobalScope(); | ||
| globalScope.setContext('profile', {}); | ||
| } | ||
| /** | ||
| * Initializes new profile chunk metadata | ||
| */ | ||
| @@ -305,6 +346,30 @@ class ContinuousProfiler { | ||
| }; | ||
| } | ||
| /** | ||
| * Assigns thread_id and thread name context to a profiled event. | ||
| */ | ||
| private _assignThreadIdContext(event: Event): any { | ||
| if (!event?.['contexts']?.['profile']) { | ||
| return; | ||
| } | ||
| if (!event.contexts) { | ||
| return; | ||
| } | ||
| // @ts-expect-error the trace fallback value is wrong, though it should never happen | ||
| // and in case it does, we dont want to override whatever was passed initially. | ||
| event.contexts['trace'] = { | ||
| ...(event.contexts?.['trace'] ?? {}), | ||
| data: { | ||
| ...(event.contexts?.['trace']?.['data'] ?? {}), | ||
| ['thread.id']: PROFILER_THREAD_ID_STRING, | ||
| ['thread.name']: PROFILER_THREAD_NAME, | ||
JonasBa marked this conversation as resolved.
Uh oh!There was an error while loading. Please reload this page. | ||
| }, | ||
| }; | ||
| } | ||
| /** | ||
| * Resets the current chunk state. | ||
| */ | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -7,8 +7,6 @@ import { GLOBAL_OBJ, createEnvelope, logger } from '@sentry/utils'; | ||
| import { CpuProfilerBindings } from '../src/cpu_profiler'; | ||
| import { type ProfilingIntegration, _nodeProfilingIntegration } from '../src/integration'; | ||
| jest.setTimeout(10000); | ||
| function makeClientWithHooks(): [Sentry.NodeClient, Transport] { | ||
| const integration = _nodeProfilingIntegration(); | ||
| const client = new Sentry.NodeClient({ | ||
| @@ -322,7 +320,6 @@ describe('automated span instrumentation', () => { | ||
| transaction.end(); | ||
| expect(stopProfilingSpy).toHaveBeenCalledTimes(1); | ||
| }); | ||
| it('enriches profile with debug_id', async () => { | ||
| GLOBAL_OBJ._sentryDebugIds = { | ||
| 'Error\n at filename.js (filename.js:36:15)': 'aaaaaaaa-aaaa-4aaa-aaaa-aaaaaaaaaa', | ||
| @@ -574,6 +571,47 @@ describe('continuous profiling', () => { | ||
| expect(transportSpy.mock.calls?.[0]?.[0]?.[1]?.[0]?.[0].type).toBe('profile_chunk'); | ||
| }); | ||
| it('sets global profile context', async () => { | ||
| const [client, transport] = makeContinuousProfilingClient(); | ||
| Sentry.setCurrentClient(client); | ||
| client.init(); | ||
| const transportSpy = jest.spyOn(transport, 'send').mockReturnValue(Promise.resolve({})); | ||
| const nonProfiledTransaction = Sentry.startInactiveSpan({ forceTransaction: true, name: 'profile_hub' }); | ||
| nonProfiledTransaction.end(); | ||
| expect(transportSpy.mock.calls?.[0]?.[0]?.[1]?.[0]?.[1]).not.toMatchObject({ | ||
| contexts: { | ||
| profile: {}, | ||
| }, | ||
| }); | ||
| const integration = client.getIntegrationByName<ProfilingIntegration>('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']: expect.any(String), | ||
| ['thread.name']: expect.any(String), | ||
Contributor There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. we should add a test to validate the ContributorAuthor There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Will do | ||
| }), | ||
| }, | ||
| profile: { | ||
| profiler_id: expect.any(String), | ||
| }, | ||
| }, | ||
| }); | ||
| }); | ||
| }); | ||
| describe('span profiling mode', () => { | ||
| @@ -610,7 +648,6 @@ describe('span profiling mode', () => { | ||
| Sentry.startInactiveSpan({ forceTransaction: true, name: 'profile_hub' }); | ||
| expect(startProfilingSpy).toHaveBeenCalled(); | ||
| const integration = client.getIntegrationByName<ProfilingIntegration>('ProfilingIntegration'); | ||
| if (!integration) { | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,75 @@ | ||
| // Mock the modules before the import, so that the value is initialized before the module is loaded | ||
| jest.mock('worker_threads', () => { | ||
| return { | ||
| isMainThread: false, | ||
| threadId: 9999, | ||
| }; | ||
| }); | ||
| jest.setTimeout(10000); | ||
| import * as Sentry from '@sentry/node'; | ||
| import type { Transport } from '@sentry/types'; | ||
| import { type ProfilingIntegration, _nodeProfilingIntegration } from '../src/integration'; | ||
| function makeContinuousProfilingClient(): [Sentry.NodeClient, Transport] { | ||
| const integration = _nodeProfilingIntegration(); | ||
| const client = new Sentry.NodeClient({ | ||
| stackParser: Sentry.defaultStackParser, | ||
| tracesSampleRate: 1, | ||
| profilesSampleRate: undefined, | ||
| debug: true, | ||
| environment: 'test-environment', | ||
| dsn: 'https://7fa19397baaf433f919fbe02228d5470@o1137848.ingest.sentry.io/6625302', | ||
| integrations: [integration], | ||
| transport: _opts => | ||
| Sentry.makeNodeTransport({ | ||
| url: 'https://7fa19397baaf433f919fbe02228d5470@o1137848.ingest.sentry.io/6625302', | ||
| recordDroppedEvent: () => { | ||
| return undefined; | ||
| }, | ||
| }), | ||
| }); | ||
| return [client, client.getTransport() as Transport]; | ||
| } | ||
| it('worker threads context', () => { | ||
| const [client, transport] = makeContinuousProfilingClient(); | ||
| Sentry.setCurrentClient(client); | ||
| client.init(); | ||
| const transportSpy = jest.spyOn(transport, 'send').mockReturnValue(Promise.resolve({})); | ||
| const nonProfiledTransaction = Sentry.startInactiveSpan({ forceTransaction: true, name: 'profile_hub' }); | ||
| nonProfiledTransaction.end(); | ||
| expect(transportSpy.mock.calls?.[0]?.[0]?.[1]?.[0]?.[1]).not.toMatchObject({ | ||
| contexts: { | ||
| profile: {}, | ||
| }, | ||
| }); | ||
| const integration = client.getIntegrationByName<ProfilingIntegration>('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), | ||
| }, | ||
| }, | ||
| }); | ||
| }); |
Uh oh!
There was an error while loading. Please reload this page.