diff --git a/bindings/profilers/wall.cc b/bindings/profilers/wall.cc index 98a8d2dd..f5e6cc7f 100644 --- a/bindings/profilers/wall.cc +++ b/bindings/profilers/wall.cc @@ -1089,6 +1089,7 @@ NAN_MODULE_INIT(WallProfiler::Init) { Nan::SetPrototypeMethod(tpl, "v8ProfilerStuckEventLoopDetected", V8ProfilerStuckEventLoopDetected); + Nan::SetPrototypeMethod(tpl, "createContextHolder", CreateContextHolder); Nan::SetAccessor(tpl->InstanceTemplate(), Nan::New("state").ToLocalChecked(), @@ -1176,23 +1177,29 @@ void WallProfiler::SetContext(Isolate* isolate, Local value) { SignalGuard m(setInProgress_); cpedMap->Delete(v8Ctx, localKey).Check(); } else { - auto wrap = - wrapObjectTemplate_.Get(isolate)->NewInstance(v8Ctx).ToLocalChecked(); - // for easy access from JS when cpedKey is an ALS, it can do - // als.getStore()?.[0]; - wrap->Set(v8Ctx, 0, value).Check(); - auto contextPtr = new PersistentContextPtr(&liveContextPtrs_, wrap); - liveContextPtrs_.insert(contextPtr); - contextPtr->Set(isolate, value); - + auto contextHolder = CreateContextHolder(isolate, v8Ctx, value); SignalGuard m(setInProgress_); - cpedMap->Set(v8Ctx, localKey, wrap).ToLocalChecked(); + cpedMap->Set(v8Ctx, localKey, contextHolder).ToLocalChecked(); } #else SetCurrentContextPtr(isolate, value); #endif } +Local WallProfiler::CreateContextHolder(Isolate* isolate, + Local v8Ctx, + Local value) { + auto wrap = + wrapObjectTemplate_.Get(isolate)->NewInstance(v8Ctx).ToLocalChecked(); + // for easy access from JS when cpedKey is an ALS, it can do + // als.getStore()?.[0]; + wrap->Set(v8Ctx, 0, value).Check(); + auto contextPtr = new PersistentContextPtr(&liveContextPtrs_, wrap); + liveContextPtrs_.insert(contextPtr); + contextPtr->Set(isolate, value); + return wrap; +} + ContextPtr WallProfiler::GetContextPtrSignalSafe(Isolate* isolate) { auto isSetInProgress = setInProgress_.load(std::memory_order_relaxed); std::atomic_signal_fence(std::memory_order_acquire); @@ -1279,6 +1286,18 @@ NAN_SETTER(WallProfiler::SetContext) { profiler->SetContext(info.GetIsolate(), value); } +NAN_METHOD(WallProfiler::CreateContextHolder) { + auto profiler = Nan::ObjectWrap::Unwrap(info.This()); + if (!profiler->useCPED()) { + return Nan::ThrowTypeError( + "CreateContextHolder can only be used with CPED"); + } + auto isolate = info.GetIsolate(); + auto contextHolder = profiler->CreateContextHolder( + isolate, isolate->GetCurrentContext(), info[0]); + info.GetReturnValue().Set(contextHolder); +} + NAN_GETTER(WallProfiler::SharedArrayGetter) { auto profiler = Nan::ObjectWrap::Unwrap(info.This()); info.GetReturnValue().Set(profiler->jsArray_.Get(v8::Isolate::GetCurrent())); diff --git a/bindings/profilers/wall.hh b/bindings/profilers/wall.hh index 9b4ed58a..7e01f354 100644 --- a/bindings/profilers/wall.hh +++ b/bindings/profilers/wall.hh @@ -143,6 +143,10 @@ class WallProfiler : public Nan::ObjectWrap { v8::Local GetContext(v8::Isolate*); void SetContext(v8::Isolate*, v8::Local); + v8::Local CreateContextHolder(v8::Isolate*, + v8::Local, + v8::Local); + void PushContext(int64_t time_from, int64_t time_to, int64_t cpu_time, @@ -186,6 +190,7 @@ class WallProfiler : public Nan::ObjectWrap { static NAN_MODULE_INIT(Init); static NAN_GETTER(GetContext); static NAN_SETTER(SetContext); + static NAN_METHOD(CreateContextHolder); static NAN_GETTER(SharedArrayGetter); static NAN_GETTER(GetMetrics); }; diff --git a/ts/src/index.ts b/ts/src/index.ts index be2a4170..3e266f71 100644 --- a/ts/src/index.ts +++ b/ts/src/index.ts @@ -36,6 +36,7 @@ export const time = { stop: timeProfiler.stop, getContext: timeProfiler.getContext, setContext: timeProfiler.setContext, + runWithContext: timeProfiler.runWithContext, isStarted: timeProfiler.isStarted, v8ProfilerStuckEventLoopDetected: timeProfiler.v8ProfilerStuckEventLoopDetected, diff --git a/ts/src/time-profiler.ts b/ts/src/time-profiler.ts index 0aabb53e..631a89dc 100644 --- a/ts/src/time-profiler.ts +++ b/ts/src/time-profiler.ts @@ -169,6 +169,19 @@ export function setContext(context?: object) { gProfiler.context = context; } +export function runWithContext( + context: object, + f: (...args: TArgs) => R, + ...args: TArgs +): R { + if (!gProfiler) { + throw new Error('Wall profiler is not started'); + } else if (!gStore) { + throw new Error('Can only use runWithContext with AsyncContextFrame'); + } + return gStore.run(gProfiler.createContextHolder(context), f, ...args); +} + export function getContext() { if (!gProfiler) { throw new Error('Wall profiler is not started'); diff --git a/ts/test/test-get-value-from-map-profiler.ts b/ts/test/test-get-value-from-map-profiler.ts index 329f3254..5e03bf00 100644 --- a/ts/test/test-get-value-from-map-profiler.ts +++ b/ts/test/test-get-value-from-map-profiler.ts @@ -124,6 +124,27 @@ if (useCPED && supportedPlatform) { profiler.dispose(); }); + + it('should work with createContextHolder pattern', () => { + // This tests the pattern used by runWithContext where + // createContextHolder creates a wrap object that's stored in CPED map + + const als = new AsyncLocalStorage(); + const profiler = createProfiler(als); + + const context = {label: 'wrapped-context', id: 999}; + + // Using als.run mimics what runWithContext does internally + als.run(profiler.createContextHolder(context), () => { + const retrieved = profiler.context; + + // The wrap object stores context at index 0 + assert.ok(retrieved !== null && typeof retrieved === 'object'); + assert.deepStrictEqual(retrieved, context); + }); + + profiler.dispose(); + }); }); describe('multiple context frames', () => { diff --git a/ts/test/test-time-profiler.ts b/ts/test/test-time-profiler.ts index 8082245f..6727d77c 100644 --- a/ts/test/test-time-profiler.ts +++ b/ts/test/test-time-profiler.ts @@ -35,6 +35,10 @@ const useCPED = const collectAsyncId = satisfies(process.versions.node, '>=24.0.0'); +const unsupportedPlatform = + process.platform !== 'darwin' && process.platform !== 'linux'; +const shouldSkipCPEDTests = !useCPED || unsupportedPlatform; + const PROFILE_OPTIONS = { durationMillis: 500, intervalMicros: 1000, @@ -49,7 +53,7 @@ describe('Time Profiler', () => { }); it('should update state', function shouldUpdateState() { - if (process.platform !== 'darwin' && process.platform !== 'linux') { + if (unsupportedPlatform) { this.skip(); } const startTime = BigInt(Date.now()) * 1000n; @@ -101,7 +105,7 @@ describe('Time Profiler', () => { }); it('should have labels', function shouldHaveLabels() { - if (process.platform !== 'darwin' && process.platform !== 'linux') { + if (unsupportedPlatform) { this.skip(); } this.timeout(3000); @@ -536,7 +540,7 @@ describe('Time Profiler', () => { describe('lowCardinalityLabels', () => { it('should handle lowCardinalityLabels parameter in stop function', async function testLowCardinalityLabels() { - if (process.platform !== 'darwin' && process.platform !== 'linux') { + if (unsupportedPlatform) { this.skip(); } this.timeout(3000); @@ -726,4 +730,240 @@ describe('Time Profiler', () => { assert.ok(threadId > 0); }); }); + + describe('runWithContext', () => { + it('should throw when profiler is not started', () => { + assert.throws(() => { + time.runWithContext({label: 'test'}, () => {}); + }, /Wall profiler is not started/); + }); + + it('should throw when useCPED is not enabled', function testNoCPED() { + if (unsupportedPlatform) { + this.skip(); + } + + time.start({ + intervalMicros: PROFILE_OPTIONS.intervalMicros, + durationMillis: PROFILE_OPTIONS.durationMillis, + withContexts: true, + useCPED: false, + }); + + try { + assert.throws(() => { + time.runWithContext({label: 'test'}, () => {}); + }, /Can only use runWithContext with AsyncContextFrame/); + } finally { + time.stop(); + } + }); + + it('should run function with context when useCPED is enabled', function testRunWithContext() { + if (shouldSkipCPEDTests) { + this.skip(); + } + + time.start({ + intervalMicros: PROFILE_OPTIONS.intervalMicros, + durationMillis: PROFILE_OPTIONS.durationMillis, + withContexts: true, + useCPED: true, + }); + + try { + const testContext = {label: 'test-value', id: '123'}; + let contextInsideFunction; + + time.runWithContext(testContext, () => { + contextInsideFunction = time.getContext(); + }); + + assert.deepEqual( + contextInsideFunction, + testContext, + 'Context should be accessible within function' + ); + } finally { + time.stop(); + } + }); + + it('should pass arguments to function correctly', function testArguments() { + if (shouldSkipCPEDTests) { + this.skip(); + } + + time.start({ + intervalMicros: PROFILE_OPTIONS.intervalMicros, + durationMillis: PROFILE_OPTIONS.durationMillis, + withContexts: true, + useCPED: true, + }); + + try { + const testContext = {label: 'test'}; + const result = time.runWithContext( + testContext, + (a: number, b: string, c: boolean) => { + return {a, b, c}; + }, + 42, + 'hello', + true + ); + + assert.deepEqual( + result, + {a: 42, b: 'hello', c: true}, + 'Arguments should be passed correctly' + ); + } finally { + time.stop(); + } + }); + + it('should return function result', function testReturnValue() { + if (shouldSkipCPEDTests) { + this.skip(); + } + + time.start({ + intervalMicros: PROFILE_OPTIONS.intervalMicros, + durationMillis: PROFILE_OPTIONS.durationMillis, + withContexts: true, + useCPED: true, + }); + + try { + const testContext = {label: 'test'}; + const result = time.runWithContext(testContext, () => { + return 'test-result'; + }); + + assert.strictEqual( + result, + 'test-result', + 'Function result should be returned' + ); + } finally { + time.stop(); + } + }); + + it('should handle nested runWithContext calls', function testNestedCalls() { + if (shouldSkipCPEDTests) { + this.skip(); + } + + time.start({ + intervalMicros: PROFILE_OPTIONS.intervalMicros, + durationMillis: PROFILE_OPTIONS.durationMillis, + withContexts: true, + useCPED: true, + }); + + try { + const outerContext = {label: 'outer'}; + const innerContext = {label: 'inner'}; + const results: string[] = []; + + time.runWithContext(outerContext, () => { + const ctx1 = time.getContext(); + results.push((ctx1 as any).label); + + time.runWithContext(innerContext, () => { + const ctx2 = time.getContext(); + results.push((ctx2 as any).label); + }); + + const ctx3 = time.getContext(); + results.push((ctx3 as any).label); + }); + + assert.deepEqual( + results, + ['outer', 'inner', 'outer'], + 'Nested contexts should be properly isolated and restored' + ); + } finally { + time.stop(); + } + }); + + it('should isolate context from outside runWithContext', function testContextIsolation() { + if (shouldSkipCPEDTests) { + this.skip(); + } + + time.start({ + intervalMicros: PROFILE_OPTIONS.intervalMicros, + durationMillis: PROFILE_OPTIONS.durationMillis, + withContexts: true, + useCPED: true, + }); + + try { + const runWithContextContext = {label: 'inside'}; + let contextInside; + + time.runWithContext(runWithContextContext, () => { + contextInside = time.getContext(); + }); + + // Context outside runWithContext should be undefined since we're using CPED + const contextOutside = time.getContext(); + + assert.deepEqual( + contextInside, + runWithContextContext, + 'Context inside should match' + ); + assert.strictEqual( + contextOutside, + undefined, + 'Context outside should be undefined with CPED' + ); + } finally { + time.stop(); + } + }); + + it('should work with async functions', async function testAsyncFunction() { + if (shouldSkipCPEDTests) { + this.skip(); + } + + time.start({ + intervalMicros: PROFILE_OPTIONS.intervalMicros, + durationMillis: PROFILE_OPTIONS.durationMillis, + withContexts: true, + useCPED: true, + }); + + try { + const testContext = {label: 'async-test'}; + + const result = await time.runWithContext(testContext, async () => { + const ctx1 = time.getContext(); + await setTimeoutPromise(10); + const ctx2 = time.getContext(); + return {ctx1, ctx2}; + }); + + assert.deepEqual( + result.ctx1, + testContext, + 'Context should be available before await' + ); + assert.deepEqual( + result.ctx2, + testContext, + 'Context should be preserved after await' + ); + } finally { + time.stop(); + } + }); + }); });