diff --git a/ts/src/time-profiler.ts b/ts/src/time-profiler.ts index 1c023c0d..d5b465b6 100644 --- a/ts/src/time-profiler.ts +++ b/ts/src/time-profiler.ts @@ -168,10 +168,12 @@ export function stop( } const profile = gProfiler.stop(restart); + if (restart) { + // Restart the profiler *before* serializing so the cost of serialization is + // captured in the next profile. handleStopRestart() does not dispose of the + // source mapper, so the source-map resolution below is unaffected. handleStopRestart(); - } else { - handleStopNoRestart(); } const serializedProfile = serializeTimeProfile( @@ -182,6 +184,15 @@ export function stop( generateLabels, lowCardinalityLabels, ); + + if (!restart) { + // Tear down *after* serializing: handleStopNoRestart() clears gSourceMapper + // (and disposes gProfiler), so serializing afterwards would drop source-map + // resolution and leave transpiled frames pointing at the generated files + // instead of the original sources. + handleStopNoRestart(); + } + return serializedProfile; } diff --git a/ts/test/test-time-profiler.ts b/ts/test/test-time-profiler.ts index 87c8e4a5..54c5b856 100644 --- a/ts/test/test-time-profiler.ts +++ b/ts/test/test-time-profiler.ts @@ -18,6 +18,8 @@ import * as sinon from 'sinon'; import {time, getNativeThreadId} from '../src'; import {profileV2, stopV2} from '../src/time-profiler'; import * as v8TimeProfiler from '../src/time-profiler-bindings'; +import * as profileSerializer from '../src/profile-serializer'; +import {SourceMapper} from '../src/sourcemapper/sourcemapper'; import {timeProfile, v8TimeProfile} from './profiles-for-tests'; import {hrtime} from 'process'; import {Label, Profile} from 'pprof-format'; @@ -495,6 +497,50 @@ describe('Time Profiler', () => { sinon.assert.notCalled(timeProfilerStub.start); sinon.assert.calledOnce(timeProfilerStub.stop); }); + + it('should serialize with the source mapper still set when stopping', () => { + // Regression test: stop() used to tear down the profiler state (via + // handleStopNoRestart, which clears gSourceMapper) *before* serializing, + // so the source mapper passed to start() was dropped and transpiled + // frames were left pointing at the generated files instead of the + // original sources. The third argument to serializeTimeProfile is the + // source mapper; it must still be the one passed to start(). + const sourceMapper = {} as unknown as SourceMapper; + const serializeStub = sinon + .stub(profileSerializer, 'serializeTimeProfile') + .returns(timeProfile); + try { + // no-restart path: handleStopNoRestart() clears gSourceMapper, so it + // must run after serialization. + time.start({ + intervalMicros: PROFILE_OPTIONS.intervalMicros, + sourceMapper, + }); + time.stop(); + sinon.assert.calledOnce(serializeStub); + assert.strictEqual( + serializeStub.getCall(0).args[2], + sourceMapper, + 'source mapper dropped on stop()', + ); + + // restart path: the source mapper must be preserved here too. + serializeStub.resetHistory(); + time.start({ + intervalMicros: PROFILE_OPTIONS.intervalMicros, + sourceMapper, + }); + time.stop(true); + assert.strictEqual( + serializeStub.getCall(0).args[2], + sourceMapper, + 'source mapper dropped on stop(true)', + ); + time.stop(); // finalize: dispose the restarted profiler + } finally { + serializeStub.restore(); + } + }); }); describe('profileV2', () => {