diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 79acbe19..c573dc73 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -34,6 +34,29 @@ jobs: - run: npm install - run: npm run test:js-valgrind + bun-smoke: + strategy: + fail-fast: false + matrix: + include: + - triplet: linux-x64 + runs-on: ubuntu-latest + - triplet: linux-arm64 + runs-on: ubuntu-24.04-arm + - triplet: darwin-arm64 + runs-on: macos-14 + runs-on: ${{ matrix.runs-on }} + name: bun-smoke-${{ matrix.triplet }} + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-node@v3 + with: + node-version: 24 + - uses: oven-sh/setup-bun@v2 + - run: npm install + - run: npm run bun:smoke + - run: npm run bun:smoke:pack + build: uses: Datadog/action-prebuildify/.github/workflows/build.yml@main with: @@ -44,7 +67,7 @@ jobs: skip: 'linux-arm,linux-ia32' # skip building for these platforms dev_publish: - needs: build + needs: [build, bun-smoke] runs-on: ubuntu-latest if: github.ref == 'refs/heads/main' environment: npm @@ -66,7 +89,7 @@ jobs: build-successful: if: always() - needs: [build] + needs: [build, bun-smoke] runs-on: ubuntu-latest steps: - name: Determine if everything is passing diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 5d204889..2da8f2e5 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -15,8 +15,31 @@ jobs: min-node-version: 18 skip: 'linux-arm,linux-ia32' # skip building for these platforms + bun-smoke: + strategy: + fail-fast: false + matrix: + include: + - triplet: linux-x64 + runs-on: ubuntu-latest + - triplet: linux-arm64 + runs-on: ubuntu-24.04-arm + - triplet: darwin-arm64 + runs-on: macos-14 + runs-on: ${{ matrix.runs-on }} + name: bun-smoke-${{ matrix.triplet }} + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-node@v3 + with: + node-version: 24 + - uses: oven-sh/setup-bun@v2 + - run: npm install + - run: npm run bun:smoke + - run: npm run bun:smoke:pack + publish: - needs: build + needs: [build, bun-smoke] runs-on: ubuntu-latest environment: npm permissions: diff --git a/.gitignore b/.gitignore index f75c1f40..ae8c8566 100644 --- a/.gitignore +++ b/.gitignore @@ -3,7 +3,9 @@ .vscode /*.build /build -out +out/* +!out/src/ +!out/src/** node_modules system-test/busybench/package-lock.json system-test/busybench-js/package-lock.json diff --git a/out/src/bun-native-backend.d.ts b/out/src/bun-native-backend.d.ts new file mode 100644 index 00000000..56bb7e4d --- /dev/null +++ b/out/src/bun-native-backend.d.ts @@ -0,0 +1,40 @@ +/** + * Copyright 2026 Datadog + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + */ +import { AllocationProfileNode, TimeProfile, TimeProfilerMetrics } from './v8-types'; +export declare class BunTimeProfiler { + metrics: TimeProfilerMetrics; + state: { + [key: string]: number; + }; + private readonly withContexts; + private readonly intervalMicros; + private readonly collectCpuTime; + private started; + private startTime; + private contextTimeline; + private currentContext; + private currentContextSignature; + private lastRecordedContextSignature; + private lastContextCpuUsage; + constructor(...args: unknown[]); + get context(): object | undefined; + set context(context: object | undefined); + private recordContext; + private normalizedContextTimeline; + start(): void; + stop(restart: boolean): TimeProfile; + dispose(): void; + v8ProfilerStuckEventLoopDetected(): number; +} +export declare function bunGetNativeThreadId(): number; +export declare function bunStartSamplingHeapProfiler(): void; +export declare function bunStopSamplingHeapProfiler(): void; +export declare function bunGetAllocationProfile(): AllocationProfileNode; +export declare function bunMonitorOutOfMemory(_heapLimitExtensionSize: number, _maxHeapLimitExtensionCount: number, _dumpHeapProfileOnSdterr: boolean, _exportCommand: Array | undefined, _callback: unknown, _callbackMode: number, _isMainThread: boolean): void; diff --git a/out/src/bun-native-backend.js b/out/src/bun-native-backend.js new file mode 100644 index 00000000..b622479c --- /dev/null +++ b/out/src/bun-native-backend.js @@ -0,0 +1,349 @@ +"use strict"; +/** + * Copyright 2026 Datadog + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + */ +Object.defineProperty(exports, "__esModule", { value: true }); +exports.BunTimeProfiler = void 0; +exports.bunGetNativeThreadId = bunGetNativeThreadId; +exports.bunStartSamplingHeapProfiler = bunStartSamplingHeapProfiler; +exports.bunStopSamplingHeapProfiler = bunStopSamplingHeapProfiler; +exports.bunGetAllocationProfile = bunGetAllocationProfile; +exports.bunMonitorOutOfMemory = bunMonitorOutOfMemory; +const MICROS_PER_MILLI = 1000; +const NANOS_PER_MICRO = 1000; +const EPOCH_ORIGIN_MICROS = BigInt(Date.now()) * BigInt(MICROS_PER_MILLI); +const HRTIME_ORIGIN_NANOS = process.hrtime.bigint(); +function nowMicros() { + return Number(process.hrtime.bigint() / 1000n); +} +function nowMicrosBigInt() { + return (EPOCH_ORIGIN_MICROS + + (process.hrtime.bigint() - HRTIME_ORIGIN_NANOS) / BigInt(NANOS_PER_MICRO)); +} +function cloneContext(context) { + if (!context) { + return context; + } + return { ...context }; +} +function cpuUsageDeltaNanos(current, previous) { + const userMicros = Math.max(current.user - previous.user, 0); + const systemMicros = Math.max(current.system - previous.system, 0); + return (userMicros + systemMicros) * NANOS_PER_MICRO; +} +function stableContextValue(value, seen) { + switch (typeof value) { + case 'string': + return `s:${value}`; + case 'number': + return `n:${value}`; + case 'bigint': + return `bi:${value.toString(10)}`; + case 'boolean': + return `b:${value ? 1 : 0}`; + case 'undefined': + return 'u:'; + case 'symbol': + return `sy:${String(value)}`; + case 'function': + return `fn:${value.name ?? ''}`; + case 'object': + if (value === null) { + return 'null:'; + } + if (Array.isArray(value)) { + const nextSeen = seen ?? new WeakSet(); + if (nextSeen.has(value)) { + return 'a:[circular]'; + } + nextSeen.add(value); + const encodedItems = value.map(item => stableContextValue(item, nextSeen)); + nextSeen.delete(value); + return `a:[${encodedItems.join(',')}]`; + } + const objectValue = value; + const nextSeen = seen ?? new WeakSet(); + if (nextSeen.has(objectValue)) { + return 'o:[circular]'; + } + nextSeen.add(objectValue); + const keys = Object.keys(objectValue).sort(); + let encoded = 'o:{'; + for (const key of keys) { + encoded += `${key}:${stableContextValue(objectValue[key], nextSeen)};`; + } + encoded += '}'; + nextSeen.delete(objectValue); + return encoded; + default: + return `${typeof value}:`; + } +} +function contextSignature(context) { + if (typeof context === 'undefined') { + return '__undefined__'; + } + if (context === null) { + return '__null__'; + } + const contextObject = context; + const contextKeys = Object.keys(contextObject).sort(); + let signature = ''; + for (const key of contextKeys) { + signature += `${key}\u0000${stableContextValue(contextObject[key])}\u0000`; + } + return signature; +} +class BunTimeProfiler { + constructor(...args) { + this.metrics = { + usedAsyncContextCount: 0, + totalAsyncContextCount: 0, + }; + this.state = { sampleCount: 0 }; + this.started = false; + this.startTime = 0; + this.contextTimeline = []; + this.currentContextSignature = contextSignature(undefined); + this.lastRecordedContextSignature = contextSignature(undefined); + const options = args[0] ?? + {}; + this.withContexts = options.withContexts === true; + this.intervalMicros = Math.max(options.intervalMicros ?? 1000, 1); + this.collectCpuTime = options.collectCpuTime === true; + } + get context() { + return this.currentContext; + } + set context(context) { + const nextContext = cloneContext(context); + this.currentContext = nextContext; + this.currentContextSignature = contextSignature(nextContext); + if (this.started && this.withContexts) { + if (this.lastRecordedContextSignature === this.currentContextSignature) { + return; + } + this.recordContext(nextContext, this.currentContextSignature); + } + } + recordContext(context, signature) { + const nextContext = { + context, + timestamp: nowMicrosBigInt(), + signature, + }; + if (this.collectCpuTime) { + const currentCpuUsage = process.cpuUsage(); + if (this.lastContextCpuUsage) { + nextContext.cpuTime = cpuUsageDeltaNanos(currentCpuUsage, this.lastContextCpuUsage); + } + else { + nextContext.cpuTime = 0; + } + this.lastContextCpuUsage = currentCpuUsage; + } + this.contextTimeline.push(nextContext); + this.lastRecordedContextSignature = signature; + } + normalizedContextTimeline(endTimestampMicros) { + if (!this.withContexts || this.contextTimeline.length === 0) { + return undefined; + } + const minSampleWindow = BigInt(this.intervalMicros); + const normalized = []; + let lastNormalizedSignature; + let pendingCpuTime = 0; + for (let i = 0; i < this.contextTimeline.length; i++) { + const current = this.contextTimeline[i]; + const nextTimestamp = i + 1 < this.contextTimeline.length + ? this.contextTimeline[i + 1].timestamp + : endTimestampMicros; + const durationMicros = nextTimestamp > current.timestamp + ? nextTimestamp - current.timestamp + : 0n; + // Ignore ultra-short context flips that are below one sampling period. + if (durationMicros < minSampleWindow) { + if (typeof current.cpuTime === 'number') { + pendingCpuTime += current.cpuTime; + } + continue; + } + const normalizedCpuTime = pendingCpuTime + (typeof current.cpuTime === 'number' ? current.cpuTime : 0); + pendingCpuTime = 0; + const last = normalized[normalized.length - 1]; + if (last && lastNormalizedSignature === current.signature) { + if (normalizedCpuTime > 0) { + last.cpuTime = (last.cpuTime ?? 0) + normalizedCpuTime; + } + continue; + } + normalized.push({ + context: current.context, + timestamp: current.timestamp, + cpuTime: normalizedCpuTime > 0 + ? normalizedCpuTime + : typeof current.cpuTime === 'number' + ? current.cpuTime + : undefined, + }); + lastNormalizedSignature = current.signature; + } + if (normalized.length > 0) { + return normalized; + } + const lastRawContext = this.contextTimeline[this.contextTimeline.length - 1]; + const fallbackCpuTime = pendingCpuTime > 0 + ? pendingCpuTime + : typeof lastRawContext.cpuTime === 'number' + ? lastRawContext.cpuTime + : undefined; + return [ + { + context: lastRawContext.context, + timestamp: lastRawContext.timestamp, + cpuTime: fallbackCpuTime, + }, + ]; + } + start() { + this.started = true; + this.startTime = nowMicros(); + this.state.sampleCount = 0; + this.contextTimeline = []; + this.lastRecordedContextSignature = contextSignature(undefined); + this.lastContextCpuUsage = this.collectCpuTime + ? process.cpuUsage() + : undefined; + if (this.withContexts && this.currentContext) { + this.recordContext(this.currentContext, this.currentContextSignature); + } + } + stop(restart) { + if (!this.started) { + throw new Error('Wall profiler is not started'); + } + const windowStartTime = this.startTime; + const endTime = nowMicros(); + const elapsedMicros = Math.max(endTime - windowStartTime, 1); + const estimatedSamples = Math.max(Math.floor(elapsedMicros / this.intervalMicros), 1); + this.state.sampleCount = estimatedSamples; + const stopTimestampMicros = nowMicrosBigInt(); + const context = this.normalizedContextTimeline(stopTimestampMicros); + if (this.collectCpuTime) { + const currentCpuUsage = process.cpuUsage(); + if (context && context.length > 0 && this.lastContextCpuUsage) { + const lastContext = context[context.length - 1]; + const cpuTime = cpuUsageDeltaNanos(currentCpuUsage, this.lastContextCpuUsage); + lastContext.cpuTime = (lastContext.cpuTime ?? 0) + cpuTime; + } + this.lastContextCpuUsage = currentCpuUsage; + } + const timeNode = { + name: 'Bun runtime', + scriptName: '', + scriptId: 0, + lineNumber: 0, + columnNumber: 0, + hitCount: estimatedSamples, + contexts: context, + children: [], + }; + const result = { + startTime: windowStartTime, + endTime, + hasCpuTime: this.collectCpuTime, + topDownRoot: { + name: '(root)', + scriptName: '', + scriptId: 0, + lineNumber: 0, + columnNumber: 0, + hitCount: 0, + children: [timeNode], + }, + }; + if (restart) { + this.startTime = endTime; + this.state.sampleCount = 0; + this.contextTimeline = []; + this.lastRecordedContextSignature = contextSignature(undefined); + this.lastContextCpuUsage = this.collectCpuTime + ? process.cpuUsage() + : undefined; + if (this.withContexts && this.currentContext) { + this.recordContext(this.currentContext, this.currentContextSignature); + } + } + return result; + } + dispose() { + this.started = false; + } + v8ProfilerStuckEventLoopDetected() { + return 0; + } +} +exports.BunTimeProfiler = BunTimeProfiler; +function bunGetNativeThreadId() { + try { + // Keep worker identities distinct when worker_threads is available. + // Use a process-scoped numeric space to avoid collisions with threadId=0 + // on the main thread. + // eslint-disable-next-line @typescript-eslint/no-var-requires + const { threadId } = require('worker_threads'); + if (typeof threadId === 'number' && Number.isInteger(threadId)) { + return process.pid * 1000 + threadId; + } + } + catch { + // Ignore resolution failures and fall back to pid-only identity. + } + return process.pid * 1000; +} +let heapSamplingEnabled = false; +function bunStartSamplingHeapProfiler() { + heapSamplingEnabled = true; +} +function bunStopSamplingHeapProfiler() { + heapSamplingEnabled = false; +} +function bunGetAllocationProfile() { + if (!heapSamplingEnabled) { + throw new Error('Heap profiler is not enabled.'); + } + const usage = process.memoryUsage(); + const heapNode = { + name: 'Bun heap', + scriptName: '', + scriptId: 0, + lineNumber: 0, + columnNumber: 0, + allocations: [ + { + sizeBytes: Math.max(usage.heapUsed, 1), + count: 1, + }, + ], + children: [], + }; + return { + name: '(root)', + scriptName: '', + scriptId: 0, + lineNumber: 0, + columnNumber: 0, + allocations: [], + children: [heapNode], + }; +} +function bunMonitorOutOfMemory(_heapLimitExtensionSize, _maxHeapLimitExtensionCount, _dumpHeapProfileOnSdterr, _exportCommand, _callback, _callbackMode, _isMainThread) { + // Bun does not expose a near-heap-limit callback hook equivalent to V8's native API. + // Keep this as a no-op to avoid emitting synthetic OOM events or spurious profiles. +} +//# sourceMappingURL=bun-native-backend.js.map \ No newline at end of file diff --git a/out/src/bun-native-backend.js.map b/out/src/bun-native-backend.js.map new file mode 100644 index 00000000..88aa33cf --- /dev/null +++ b/out/src/bun-native-backend.js.map @@ -0,0 +1 @@ +{"version":3,"file":"bun-native-backend.js","sourceRoot":"","sources":["../../ts/src/bun-native-backend.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/out/src/heap-profiler-bindings.d.ts b/out/src/heap-profiler-bindings.d.ts new file mode 100644 index 00000000..a0885439 --- /dev/null +++ b/out/src/heap-profiler-bindings.d.ts @@ -0,0 +1,21 @@ +/** + * Copyright 2018 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { AllocationProfileNode } from './v8-types'; +export declare function startSamplingHeapProfiler(heapIntervalBytes: number, heapStackDepth: number): void; +export declare function stopSamplingHeapProfiler(): void; +export declare function getAllocationProfile(): AllocationProfileNode; +export type NearHeapLimitCallback = (profile: AllocationProfileNode) => void; +export declare function monitorOutOfMemory(heapLimitExtensionSize: number, maxHeapLimitExtensionCount: number, dumpHeapProfileOnSdterr: boolean, exportCommand: Array | undefined, callback: NearHeapLimitCallback | undefined, callbackMode: number, isMainThread: boolean): void; diff --git a/out/src/heap-profiler-bindings.js b/out/src/heap-profiler-bindings.js new file mode 100644 index 00000000..c9546eea --- /dev/null +++ b/out/src/heap-profiler-bindings.js @@ -0,0 +1,22 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.startSamplingHeapProfiler = startSamplingHeapProfiler; +exports.stopSamplingHeapProfiler = stopSamplingHeapProfiler; +exports.getAllocationProfile = getAllocationProfile; +exports.monitorOutOfMemory = monitorOutOfMemory; +const native_backend_loader_1 = require("./native-backend-loader"); +const profiler = (0, native_backend_loader_1.loadNativeModule)(); +// Wrappers around native heap profiler functions. +function startSamplingHeapProfiler(heapIntervalBytes, heapStackDepth) { + profiler.heapProfiler.startSamplingHeapProfiler(heapIntervalBytes, heapStackDepth); +} +function stopSamplingHeapProfiler() { + profiler.heapProfiler.stopSamplingHeapProfiler(); +} +function getAllocationProfile() { + return profiler.heapProfiler.getAllocationProfile(); +} +function monitorOutOfMemory(heapLimitExtensionSize, maxHeapLimitExtensionCount, dumpHeapProfileOnSdterr, exportCommand, callback, callbackMode, isMainThread) { + profiler.heapProfiler.monitorOutOfMemory(heapLimitExtensionSize, maxHeapLimitExtensionCount, dumpHeapProfileOnSdterr, exportCommand, callback, callbackMode, isMainThread); +} +//# sourceMappingURL=heap-profiler-bindings.js.map \ No newline at end of file diff --git a/out/src/heap-profiler-bindings.js.map b/out/src/heap-profiler-bindings.js.map new file mode 100644 index 00000000..ca6f04a8 --- /dev/null +++ b/out/src/heap-profiler-bindings.js.map @@ -0,0 +1 @@ +{"version":3,"file":"heap-profiler-bindings.js","sourceRoot":"","sources":["../../ts/src/heap-profiler-bindings.ts"],"names":[],"mappings":";;AAsBA,8DAQC;AAED,4DAEC;AAED,oDAEC;AAID,gDAkBC;AA5CD,mEAAyD;AAEzD,MAAM,QAAQ,GAAG,IAAA,wCAAgB,GAAE,CAAC;AAEpC,kDAAkD;AAElD,SAAgB,yBAAyB,CACvC,iBAAyB,EACzB,cAAsB;IAEtB,QAAQ,CAAC,YAAY,CAAC,yBAAyB,CAC7C,iBAAiB,EACjB,cAAc,CACf,CAAC;AACJ,CAAC;AAED,SAAgB,wBAAwB;IACtC,QAAQ,CAAC,YAAY,CAAC,wBAAwB,EAAE,CAAC;AACnD,CAAC;AAED,SAAgB,oBAAoB;IAClC,OAAO,QAAQ,CAAC,YAAY,CAAC,oBAAoB,EAA2B,CAAC;AAC/E,CAAC;AAID,SAAgB,kBAAkB,CAChC,sBAA8B,EAC9B,0BAAkC,EAClC,uBAAgC,EAChC,aAAwC,EACxC,QAA2C,EAC3C,YAAoB,EACpB,YAAqB;IAErB,QAAQ,CAAC,YAAY,CAAC,kBAAkB,CACtC,sBAAsB,EACtB,0BAA0B,EAC1B,uBAAuB,EACvB,aAAa,EACb,QAAQ,EACR,YAAY,EACZ,YAAY,CACb,CAAC;AACJ,CAAC"} \ No newline at end of file diff --git a/out/src/heap-profiler.d.ts b/out/src/heap-profiler.d.ts new file mode 100644 index 00000000..e8216714 --- /dev/null +++ b/out/src/heap-profiler.d.ts @@ -0,0 +1,72 @@ +/** + * Copyright 2017 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import type { Profile } from 'pprof-format'; +import { SourceMapper } from './sourcemapper/sourcemapper'; +import { AllocationProfileNode, GenerateAllocationLabelsFunction } from './v8-types'; +export declare function v8Profile(): AllocationProfileNode; +/** + * Collects a profile and returns it serialized in pprof format. + * Throws if heap profiler is not enabled. + * + * @param ignoreSamplePath + * @param sourceMapper + */ +export declare function profile(ignoreSamplePath?: string, sourceMapper?: SourceMapper, generateLabels?: GenerateAllocationLabelsFunction): Profile; +export declare function convertProfile(rootNode: AllocationProfileNode, ignoreSamplePath?: string, sourceMapper?: SourceMapper, generateLabels?: GenerateAllocationLabelsFunction): Profile; +/** + * Starts heap profiling. If heap profiling has already been started with + * the same parameters, this is a noop. If heap profiler has already been + * started with different parameters, this throws an error. + * + * @param intervalBytes - average number of bytes between samples. + * @param stackDepth - maximum stack depth for samples collected. + */ +export declare function start(intervalBytes: number, stackDepth: number): void; +export declare function stop(): void; +export type NearHeapLimitCallback = (profile: Profile) => void; +export declare const CallbackMode: { + Async: number; + Interrupt: number; + Both: number; +}; +/** + * Add monitoring for v8 heap, heap profiler must already be started. + * When an out of heap memory event occurs: + * - an extension of heap memory of |heapLimitExtensionSize| bytes is + * requested to v8. This extension can occur |maxHeapLimitExtensionCount| + * number of times. If the extension amount is not enough to satisfy + * memory allocation that triggers GC and OOM, process will abort. + * - heap profile is dumped as folded stacks on stderr if + * |dumpHeapProfileOnSdterr| is true + * - heap profile is dumped in temporary file and a new process is spawned + * with |exportCommand| arguments and profile path appended at the end. + * - |callback| is called. Callback can be invoked only if + * heapLimitExtensionSize is enough for the process to continue. Invocation + * will be done by a RequestInterrupt if |callbackMode| is Interrupt or Both, + * this might be unsafe since Isolate should not be reentered + * from RequestInterrupt, but this allows to interrupt synchronous code. + * Otherwise the callback is scheduled to be called asynchronously. + * @param heapLimitExtensionSize - amount of bytes heap should be expanded + * with upon OOM + * @param maxHeapLimitExtensionCount - maximum number of times heap size + * extension can occur + * @param dumpHeapProfileOnSdterr - dump heap profile on stderr upon OOM + * @param exportCommand - command to execute upon OOM, filepath of a + * temporary file containing heap profile will be appended + * @param callback - callback to call when OOM occurs + * @param callbackMode + */ +export declare function monitorOutOfMemory(heapLimitExtensionSize: number, maxHeapLimitExtensionCount: number, dumpHeapProfileOnSdterr: boolean, exportCommand?: Array, callback?: NearHeapLimitCallback, callbackMode?: number): void; diff --git a/out/src/heap-profiler.js b/out/src/heap-profiler.js new file mode 100644 index 00000000..cac39bde --- /dev/null +++ b/out/src/heap-profiler.js @@ -0,0 +1,154 @@ +"use strict"; +/** + * Copyright 2017 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +Object.defineProperty(exports, "__esModule", { value: true }); +exports.CallbackMode = void 0; +exports.v8Profile = v8Profile; +exports.profile = profile; +exports.convertProfile = convertProfile; +exports.start = start; +exports.stop = stop; +exports.monitorOutOfMemory = monitorOutOfMemory; +const heap_profiler_bindings_1 = require("./heap-profiler-bindings"); +const profile_serializer_1 = require("./profile-serializer"); +const worker_threads_1 = require("worker_threads"); +const runtime_1 = require("./runtime"); +let enabled = false; +let heapIntervalBytes = 0; +let heapStackDepth = 0; +function hasEquivalentHeapSamplingConfig(intervalBytes, stackDepth) { + if (intervalBytes === heapIntervalBytes && + stackDepth === heapStackDepth) { + return true; + } + if (runtime_1.runtime !== 'bun') { + return false; + } + return (Number(intervalBytes) === Number(heapIntervalBytes) && + Number(stackDepth) === Number(heapStackDepth)); +} +/* + * Collects a heap profile when heapProfiler is enabled. Otherwise throws + * an error. + * + * Data is returned in V8 allocation profile format. + */ +function v8Profile() { + if (!enabled) { + throw new Error('Heap profiler is not enabled.'); + } + return (0, heap_profiler_bindings_1.getAllocationProfile)(); +} +/** + * Collects a profile and returns it serialized in pprof format. + * Throws if heap profiler is not enabled. + * + * @param ignoreSamplePath + * @param sourceMapper + */ +function profile(ignoreSamplePath, sourceMapper, generateLabels) { + return convertProfile(v8Profile(), ignoreSamplePath, sourceMapper, generateLabels); +} +function convertProfile(rootNode, ignoreSamplePath, sourceMapper, generateLabels) { + const startTimeNanos = Date.now() * 1000 * 1000; + // Add node for external memory usage. + // Current type definitions do not have external. + // TODO: remove any once type definition is updated to include external. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const { external } = process.memoryUsage(); + if (external > 0) { + const externalNode = { + name: '(external)', + scriptName: '', + children: [], + allocations: [{ sizeBytes: external, count: 1 }], + }; + rootNode.children.push(externalNode); + } + return (0, profile_serializer_1.serializeHeapProfile)(rootNode, startTimeNanos, heapIntervalBytes, ignoreSamplePath, sourceMapper, generateLabels); +} +/** + * Starts heap profiling. If heap profiling has already been started with + * the same parameters, this is a noop. If heap profiler has already been + * started with different parameters, this throws an error. + * + * @param intervalBytes - average number of bytes between samples. + * @param stackDepth - maximum stack depth for samples collected. + */ +function start(intervalBytes, stackDepth) { + if (enabled) { + if (hasEquivalentHeapSamplingConfig(intervalBytes, stackDepth)) { + return; + } + throw new Error(`Heap profiler is already started with intervalBytes ${heapIntervalBytes} and stackDepth ${heapStackDepth}`); + } + heapIntervalBytes = intervalBytes; + heapStackDepth = stackDepth; + (0, heap_profiler_bindings_1.startSamplingHeapProfiler)(heapIntervalBytes, heapStackDepth); + enabled = true; +} +// Stops heap profiling. If heap profiling has not been started, does nothing. +function stop() { + if (enabled) { + enabled = false; + (0, heap_profiler_bindings_1.stopSamplingHeapProfiler)(); + } +} +exports.CallbackMode = { + Async: 1, + Interrupt: 2, + Both: 3, +}; +/** + * Add monitoring for v8 heap, heap profiler must already be started. + * When an out of heap memory event occurs: + * - an extension of heap memory of |heapLimitExtensionSize| bytes is + * requested to v8. This extension can occur |maxHeapLimitExtensionCount| + * number of times. If the extension amount is not enough to satisfy + * memory allocation that triggers GC and OOM, process will abort. + * - heap profile is dumped as folded stacks on stderr if + * |dumpHeapProfileOnSdterr| is true + * - heap profile is dumped in temporary file and a new process is spawned + * with |exportCommand| arguments and profile path appended at the end. + * - |callback| is called. Callback can be invoked only if + * heapLimitExtensionSize is enough for the process to continue. Invocation + * will be done by a RequestInterrupt if |callbackMode| is Interrupt or Both, + * this might be unsafe since Isolate should not be reentered + * from RequestInterrupt, but this allows to interrupt synchronous code. + * Otherwise the callback is scheduled to be called asynchronously. + * @param heapLimitExtensionSize - amount of bytes heap should be expanded + * with upon OOM + * @param maxHeapLimitExtensionCount - maximum number of times heap size + * extension can occur + * @param dumpHeapProfileOnSdterr - dump heap profile on stderr upon OOM + * @param exportCommand - command to execute upon OOM, filepath of a + * temporary file containing heap profile will be appended + * @param callback - callback to call when OOM occurs + * @param callbackMode + */ +function monitorOutOfMemory(heapLimitExtensionSize, maxHeapLimitExtensionCount, dumpHeapProfileOnSdterr, exportCommand, callback, callbackMode) { + if (!enabled) { + throw new Error('Heap profiler must already be started to call monitorOutOfMemory'); + } + let newCallback; + if (typeof callback !== 'undefined') { + newCallback = (profile) => { + callback(convertProfile(profile)); + }; + } + (0, heap_profiler_bindings_1.monitorOutOfMemory)(heapLimitExtensionSize, maxHeapLimitExtensionCount, dumpHeapProfileOnSdterr, exportCommand || [], newCallback, typeof callbackMode !== 'undefined' ? callbackMode : exports.CallbackMode.Async, worker_threads_1.isMainThread); +} +//# sourceMappingURL=heap-profiler.js.map \ No newline at end of file diff --git a/out/src/heap-profiler.js.map b/out/src/heap-profiler.js.map new file mode 100644 index 00000000..535aec16 --- /dev/null +++ b/out/src/heap-profiler.js.map @@ -0,0 +1 @@ +{"version":3,"file":"heap-profiler.js","sourceRoot":"","sources":["../../ts/src/heap-profiler.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/out/src/index.d.ts b/out/src/index.d.ts new file mode 100644 index 00000000..5a69be54 --- /dev/null +++ b/out/src/index.d.ts @@ -0,0 +1,36 @@ +import * as heapProfiler from './heap-profiler'; +import * as timeProfiler from './time-profiler'; +export { AllocationProfileNode, TimeProfileNode, ProfileNode, LabelSet, } from './v8-types'; +export { encode, encodeSync } from './profile-encoder'; +export { SourceMapper } from './sourcemapper/sourcemapper'; +export { setLogger } from './logger'; +export { getNativeThreadId } from './time-profiler'; +export declare const time: { + profile: typeof timeProfiler.profile; + start: typeof timeProfiler.start; + stop: typeof timeProfiler.stop; + getContext: typeof timeProfiler.getContext; + setContext: typeof timeProfiler.setContext; + isStarted: typeof timeProfiler.isStarted; + v8ProfilerStuckEventLoopDetected: typeof timeProfiler.v8ProfilerStuckEventLoopDetected; + getState: typeof timeProfiler.getState; + getMetrics: typeof timeProfiler.getMetrics; + constants: { + kSampleCount: string; + GARBAGE_COLLECTION_FUNCTION_NAME: string; + NON_JS_THREADS_FUNCTION_NAME: string; + }; +}; +export declare const heap: { + start: typeof heapProfiler.start; + stop: typeof heapProfiler.stop; + profile: typeof heapProfiler.profile; + convertProfile: typeof heapProfiler.convertProfile; + v8Profile: typeof heapProfiler.v8Profile; + monitorOutOfMemory: typeof heapProfiler.monitorOutOfMemory; + CallbackMode: { + Async: number; + Interrupt: number; + Both: number; + }; +}; diff --git a/out/src/index.js b/out/src/index.js new file mode 100644 index 00000000..5206c797 --- /dev/null +++ b/out/src/index.js @@ -0,0 +1,97 @@ +"use strict"; +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); +}) : function(o, v) { + o["default"] = v; +}); +var __importStar = (this && this.__importStar) || (function () { + var ownKeys = function(o) { + ownKeys = Object.getOwnPropertyNames || function (o) { + var ar = []; + for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; + return ar; + }; + return ownKeys(o); + }; + return function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); + __setModuleDefault(result, mod); + return result; + }; +})(); +Object.defineProperty(exports, "__esModule", { value: true }); +exports.heap = exports.time = exports.getNativeThreadId = exports.setLogger = exports.SourceMapper = exports.encodeSync = exports.encode = void 0; +/** + * Copyright 2019 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +const fs_1 = require("fs"); +const heapProfiler = __importStar(require("./heap-profiler")); +const profile_encoder_1 = require("./profile-encoder"); +const timeProfiler = __importStar(require("./time-profiler")); +var profile_encoder_2 = require("./profile-encoder"); +Object.defineProperty(exports, "encode", { enumerable: true, get: function () { return profile_encoder_2.encode; } }); +Object.defineProperty(exports, "encodeSync", { enumerable: true, get: function () { return profile_encoder_2.encodeSync; } }); +var sourcemapper_1 = require("./sourcemapper/sourcemapper"); +Object.defineProperty(exports, "SourceMapper", { enumerable: true, get: function () { return sourcemapper_1.SourceMapper; } }); +var logger_1 = require("./logger"); +Object.defineProperty(exports, "setLogger", { enumerable: true, get: function () { return logger_1.setLogger; } }); +var time_profiler_1 = require("./time-profiler"); +Object.defineProperty(exports, "getNativeThreadId", { enumerable: true, get: function () { return time_profiler_1.getNativeThreadId; } }); +exports.time = { + profile: timeProfiler.profile, + start: timeProfiler.start, + stop: timeProfiler.stop, + getContext: timeProfiler.getContext, + setContext: timeProfiler.setContext, + isStarted: timeProfiler.isStarted, + v8ProfilerStuckEventLoopDetected: timeProfiler.v8ProfilerStuckEventLoopDetected, + getState: timeProfiler.getState, + getMetrics: timeProfiler.getMetrics, + constants: timeProfiler.constants, +}; +exports.heap = { + start: heapProfiler.start, + stop: heapProfiler.stop, + profile: heapProfiler.profile, + convertProfile: heapProfiler.convertProfile, + v8Profile: heapProfiler.v8Profile, + monitorOutOfMemory: heapProfiler.monitorOutOfMemory, + CallbackMode: heapProfiler.CallbackMode, +}; +// If loaded with --require, start profiling. +if (module.parent && module.parent.id === 'internal/preload') { + exports.time.start({}); + process.on('exit', () => { + // The process is going to terminate imminently. All work here needs to + // be synchronous. + const profile = exports.time.stop(); + const buffer = (0, profile_encoder_1.encodeSync)(profile); + (0, fs_1.writeFileSync)(`pprof-profile-${process.pid}.pb.gz`, buffer); + }); +} +//# sourceMappingURL=index.js.map \ No newline at end of file diff --git a/out/src/index.js.map b/out/src/index.js.map new file mode 100644 index 00000000..6c43d602 --- /dev/null +++ b/out/src/index.js.map @@ -0,0 +1 @@ +{"version":3,"file":"index.js","sourceRoot":"","sources":["../../ts/src/index.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;;;;;;;;;;;;;;GAcG;AACH,2BAAiC;AAEjC,8DAAgD;AAChD,uDAA6C;AAC7C,8DAAgD;AAQhD,qDAAqD;AAA7C,yGAAA,MAAM,OAAA;AAAE,6GAAA,UAAU,OAAA;AAC1B,4DAAyD;AAAjD,4GAAA,YAAY,OAAA;AACpB,mCAAmC;AAA3B,mGAAA,SAAS,OAAA;AACjB,iDAAkD;AAA1C,kHAAA,iBAAiB,OAAA;AAEZ,QAAA,IAAI,GAAG;IAClB,OAAO,EAAE,YAAY,CAAC,OAAO;IAC7B,KAAK,EAAE,YAAY,CAAC,KAAK;IACzB,IAAI,EAAE,YAAY,CAAC,IAAI;IACvB,UAAU,EAAE,YAAY,CAAC,UAAU;IACnC,UAAU,EAAE,YAAY,CAAC,UAAU;IACnC,SAAS,EAAE,YAAY,CAAC,SAAS;IACjC,gCAAgC,EAC9B,YAAY,CAAC,gCAAgC;IAC/C,QAAQ,EAAE,YAAY,CAAC,QAAQ;IAC/B,UAAU,EAAE,YAAY,CAAC,UAAU;IACnC,SAAS,EAAE,YAAY,CAAC,SAAS;CAClC,CAAC;AAEW,QAAA,IAAI,GAAG;IAClB,KAAK,EAAE,YAAY,CAAC,KAAK;IACzB,IAAI,EAAE,YAAY,CAAC,IAAI;IACvB,OAAO,EAAE,YAAY,CAAC,OAAO;IAC7B,cAAc,EAAE,YAAY,CAAC,cAAc;IAC3C,SAAS,EAAE,YAAY,CAAC,SAAS;IACjC,kBAAkB,EAAE,YAAY,CAAC,kBAAkB;IACnD,YAAY,EAAE,YAAY,CAAC,YAAY;CACxC,CAAC;AAEF,6CAA6C;AAC7C,IAAI,MAAM,CAAC,MAAM,IAAI,MAAM,CAAC,MAAM,CAAC,EAAE,KAAK,kBAAkB,EAAE,CAAC;IAC7D,YAAI,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC;IACf,OAAO,CAAC,EAAE,CAAC,MAAM,EAAE,GAAG,EAAE;QACtB,uEAAuE;QACvE,kBAAkB;QAClB,MAAM,OAAO,GAAG,YAAI,CAAC,IAAI,EAAE,CAAC;QAC5B,MAAM,MAAM,GAAG,IAAA,4BAAU,EAAC,OAAO,CAAC,CAAC;QACnC,IAAA,kBAAa,EAAC,iBAAiB,OAAO,CAAC,GAAG,QAAQ,EAAE,MAAM,CAAC,CAAC;IAC9D,CAAC,CAAC,CAAC;AACL,CAAC"} \ No newline at end of file diff --git a/out/src/logger.d.ts b/out/src/logger.d.ts new file mode 100644 index 00000000..d4999424 --- /dev/null +++ b/out/src/logger.d.ts @@ -0,0 +1,18 @@ +export interface Logger { + error(...args: Array<{}>): void; + trace(...args: Array<{}>): void; + debug(...args: Array<{}>): void; + info(...args: Array<{}>): void; + warn(...args: Array<{}>): void; + fatal(...args: Array<{}>): void; +} +export declare class NullLogger implements Logger { + info(...args: Array<{}>): void; + error(...args: Array<{}>): void; + trace(...args: Array<{}>): void; + warn(...args: Array<{}>): void; + fatal(...args: Array<{}>): void; + debug(...args: Array<{}>): void; +} +export declare let logger: NullLogger; +export declare function setLogger(newLogger: Logger): void; diff --git a/out/src/logger.js b/out/src/logger.js new file mode 100644 index 00000000..4bff35ce --- /dev/null +++ b/out/src/logger.js @@ -0,0 +1,36 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.logger = exports.NullLogger = void 0; +exports.setLogger = setLogger; +class NullLogger { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + info(...args) { + return; + } + // eslint-disable-next-line @typescript-eslint/no-unused-vars + error(...args) { + return; + } + // eslint-disable-next-line @typescript-eslint/no-unused-vars + trace(...args) { + return; + } + // eslint-disable-next-line @typescript-eslint/no-unused-vars + warn(...args) { + return; + } + // eslint-disable-next-line @typescript-eslint/no-unused-vars + fatal(...args) { + return; + } + // eslint-disable-next-line @typescript-eslint/no-unused-vars + debug(...args) { + return; + } +} +exports.NullLogger = NullLogger; +exports.logger = new NullLogger(); +function setLogger(newLogger) { + exports.logger = newLogger; +} +//# sourceMappingURL=logger.js.map \ No newline at end of file diff --git a/out/src/logger.js.map b/out/src/logger.js.map new file mode 100644 index 00000000..fe205906 --- /dev/null +++ b/out/src/logger.js.map @@ -0,0 +1 @@ +{"version":3,"file":"logger.js","sourceRoot":"","sources":["../../ts/src/logger.ts"],"names":[],"mappings":";;;AAsCA,8BAEC;AA/BD,MAAa,UAAU;IACrB,6DAA6D;IAC7D,IAAI,CAAC,GAAG,IAAe;QACrB,OAAO;IACT,CAAC;IACD,6DAA6D;IAC7D,KAAK,CAAC,GAAG,IAAe;QACtB,OAAO;IACT,CAAC;IACD,6DAA6D;IAC7D,KAAK,CAAC,GAAG,IAAe;QACtB,OAAO;IACT,CAAC;IACD,6DAA6D;IAC7D,IAAI,CAAC,GAAG,IAAe;QACrB,OAAO;IACT,CAAC;IACD,6DAA6D;IAC7D,KAAK,CAAC,GAAG,IAAe;QACtB,OAAO;IACT,CAAC;IACD,6DAA6D;IAC7D,KAAK,CAAC,GAAG,IAAe;QACtB,OAAO;IACT,CAAC;CACF;AAzBD,gCAyBC;AAEU,QAAA,MAAM,GAAG,IAAI,UAAU,EAAE,CAAC;AAErC,SAAgB,SAAS,CAAC,SAAiB;IACzC,cAAM,GAAG,SAAS,CAAC;AACrB,CAAC"} \ No newline at end of file diff --git a/out/src/native-backend-loader.d.ts b/out/src/native-backend-loader.d.ts new file mode 100644 index 00000000..aa1d2c06 --- /dev/null +++ b/out/src/native-backend-loader.d.ts @@ -0,0 +1,40 @@ +/** + * Copyright 2026 Datadog + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + */ +import { TimeProfile, TimeProfilerMetrics } from './v8-types'; +type NativeModule = { + TimeProfiler: new (...args: unknown[]) => TimeProfilerInstance; + constants: { + kSampleCount: string; + }; + getNativeThreadId: () => number; + heapProfiler: { + startSamplingHeapProfiler: (heapIntervalBytes: number, heapStackDepth: number) => void; + stopSamplingHeapProfiler: () => void; + getAllocationProfile: () => unknown; + monitorOutOfMemory: (heapLimitExtensionSize: number, maxHeapLimitExtensionCount: number, dumpHeapProfileOnSdterr: boolean, exportCommand: Array | undefined, callback: unknown, callbackMode: number, isMainThread: boolean) => void; + }; +}; +type TimeProfilerInstance = { + start: () => void; + stop: (restart: boolean) => TimeProfile; + dispose: () => void; + v8ProfilerStuckEventLoopDetected: () => number; + context: object | undefined; + metrics: TimeProfilerMetrics; + state: { + [key: string]: number; + }; +}; +export declare function loadNativeModule(): NativeModule; +declare function loadNodeNativeModule(rootDir: string, findBinding: (rootPath: string) => NativeModule, nodeRequire: (modulePath: string) => unknown): NativeModule; +export declare const __testing: { + loadNodeNativeModule: typeof loadNodeNativeModule; +}; +export {}; diff --git a/out/src/native-backend-loader.js b/out/src/native-backend-loader.js new file mode 100644 index 00000000..d33800ca --- /dev/null +++ b/out/src/native-backend-loader.js @@ -0,0 +1,80 @@ +"use strict"; +/** + * Copyright 2026 Datadog + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + */ +Object.defineProperty(exports, "__esModule", { value: true }); +exports.__testing = void 0; +exports.loadNativeModule = loadNativeModule; +const fs_1 = require("fs"); +const path_1 = require("path"); +const runtime_1 = require("./runtime"); +const bun_native_backend_1 = require("./bun-native-backend"); +const bunModule = { + TimeProfiler: bun_native_backend_1.BunTimeProfiler, + constants: { kSampleCount: 'sampleCount' }, + getNativeThreadId: bun_native_backend_1.bunGetNativeThreadId, + heapProfiler: { + startSamplingHeapProfiler: bun_native_backend_1.bunStartSamplingHeapProfiler, + stopSamplingHeapProfiler: bun_native_backend_1.bunStopSamplingHeapProfiler, + getAllocationProfile: bun_native_backend_1.bunGetAllocationProfile, + monitorOutOfMemory: bun_native_backend_1.bunMonitorOutOfMemory, + }, +}; +let cachedModule; +function loadNativeModule() { + if (cachedModule) { + return cachedModule; + } + if (runtime_1.runtime === 'bun') { + cachedModule = bunModule; + return cachedModule; + } + const rootDir = (0, path_1.join)(__dirname, '..', '..'); + // eslint-disable-next-line @typescript-eslint/no-var-requires + const findBinding = require('node-gyp-build'); + cachedModule = loadNodeNativeModule(rootDir, findBinding, require); + return cachedModule; +} +function loadNodeNativeModule(rootDir, findBinding, nodeRequire) { + try { + return findBinding(rootDir); + } + catch (error) { + if (!isMissingNativeBuildError(error) || + !hasLocalNativeBuildArtifacts(rootDir)) { + throw error; + } + return loadFromBuildRelease(rootDir, nodeRequire); + } +} +function isMissingNativeBuildError(error) { + return (error instanceof Error && + error.message.includes('No native build was found for runtime=')); +} +function hasLocalNativeBuildArtifacts(rootDir) { + return (0, fs_1.existsSync)((0, path_1.join)(rootDir, 'build', 'Release')); +} +function loadFromBuildRelease(rootDir, nodeRequire) { + const releaseDir = (0, path_1.join)(rootDir, 'build', 'Release'); + const preferredPath = (0, path_1.join)(releaseDir, 'dd_pprof.node'); + if ((0, fs_1.existsSync)(preferredPath)) { + return nodeRequire(preferredPath); + } + const candidates = (0, fs_1.readdirSync)(releaseDir) + .filter(name => name.endsWith('.node')) + .sort(); + if (candidates.length === 0) { + throw new Error(`No native .node artifact found under ${releaseDir} after fallback`); + } + return nodeRequire((0, path_1.join)(releaseDir, candidates[0])); +} +exports.__testing = { + loadNodeNativeModule, +}; +//# sourceMappingURL=native-backend-loader.js.map \ No newline at end of file diff --git a/out/src/native-backend-loader.js.map b/out/src/native-backend-loader.js.map new file mode 100644 index 00000000..66364bb1 --- /dev/null +++ b/out/src/native-backend-loader.js.map @@ -0,0 +1 @@ +{"version":3,"file":"native-backend-loader.js","sourceRoot":"","sources":["../../ts/src/native-backend-loader.ts"],"names":[],"mappings":";AAAA;;;;;;;;GAQG;;;AA+DH,4CAkBC;AA/ED,2BAA2C;AAC3C,+BAA0B;AAE1B,uCAAkC;AAClC,6DAO8B;AAoC9B,MAAM,SAAS,GAAiB;IAC9B,YAAY,EAAE,oCAAe;IAC7B,SAAS,EAAE,EAAC,YAAY,EAAE,aAAa,EAAC;IACxC,iBAAiB,EAAE,yCAAoB;IACvC,YAAY,EAAE;QACZ,yBAAyB,EAAE,iDAA4B;QACvD,wBAAwB,EAAE,gDAA2B;QACrD,oBAAoB,EAAE,4CAAuB;QAC7C,kBAAkB,EAAE,0CAAqB;KAC1C;CACF,CAAC;AAEF,IAAI,YAAsC,CAAC;AAE3C,SAAgB,gBAAgB;IAC9B,IAAI,YAAY,EAAE,CAAC;QACjB,OAAO,YAAY,CAAC;IACtB,CAAC;IAED,IAAI,iBAAO,KAAK,KAAK,EAAE,CAAC;QACtB,YAAY,GAAG,SAAS,CAAC;QACzB,OAAO,YAAY,CAAC;IACtB,CAAC;IAED,MAAM,OAAO,GAAG,IAAA,WAAI,EAAC,SAAS,EAAE,IAAI,EAAE,IAAI,CAAC,CAAC;IAE5C,8DAA8D;IAC9D,MAAM,WAAW,GAAG,OAAO,CAAC,gBAAgB,CAE3B,CAAC;IAClB,YAAY,GAAG,oBAAoB,CAAC,OAAO,EAAE,WAAW,EAAE,OAAO,CAAC,CAAC;IACnE,OAAO,YAAY,CAAC;AACtB,CAAC;AAED,SAAS,oBAAoB,CAC3B,OAAe,EACf,WAA+C,EAC/C,WAA4C;IAE5C,IAAI,CAAC;QACH,OAAO,WAAW,CAAC,OAAO,CAAC,CAAC;IAC9B,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,IACE,CAAC,yBAAyB,CAAC,KAAK,CAAC;YACjC,CAAC,4BAA4B,CAAC,OAAO,CAAC,EACtC,CAAC;YACD,MAAM,KAAK,CAAC;QACd,CAAC;QAED,OAAO,oBAAoB,CAAC,OAAO,EAAE,WAAW,CAAC,CAAC;IACpD,CAAC;AACH,CAAC;AAED,SAAS,yBAAyB,CAAC,KAAc;IAC/C,OAAO,CACL,KAAK,YAAY,KAAK;QACtB,KAAK,CAAC,OAAO,CAAC,QAAQ,CAAC,wCAAwC,CAAC,CACjE,CAAC;AACJ,CAAC;AAED,SAAS,4BAA4B,CAAC,OAAe;IACnD,OAAO,IAAA,eAAU,EAAC,IAAA,WAAI,EAAC,OAAO,EAAE,OAAO,EAAE,SAAS,CAAC,CAAC,CAAC;AACvD,CAAC;AAED,SAAS,oBAAoB,CAC3B,OAAe,EACf,WAA4C;IAE5C,MAAM,UAAU,GAAG,IAAA,WAAI,EAAC,OAAO,EAAE,OAAO,EAAE,SAAS,CAAC,CAAC;IACrD,MAAM,aAAa,GAAG,IAAA,WAAI,EAAC,UAAU,EAAE,eAAe,CAAC,CAAC;IACxD,IAAI,IAAA,eAAU,EAAC,aAAa,CAAC,EAAE,CAAC;QAC9B,OAAO,WAAW,CAAC,aAAa,CAAiB,CAAC;IACpD,CAAC;IAED,MAAM,UAAU,GAAG,IAAA,gBAAW,EAAC,UAAU,CAAC;SACvC,MAAM,CAAC,IAAI,CAAC,EAAE,CAAC,IAAI,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC;SACtC,IAAI,EAAE,CAAC;IACV,IAAI,UAAU,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAC5B,MAAM,IAAI,KAAK,CACb,wCAAwC,UAAU,iBAAiB,CACpE,CAAC;IACJ,CAAC;IAED,OAAO,WAAW,CAAC,IAAA,WAAI,EAAC,UAAU,EAAE,UAAU,CAAC,CAAC,CAAC,CAAC,CAAiB,CAAC;AACtE,CAAC;AAEY,QAAA,SAAS,GAAG;IACvB,oBAAoB;CACrB,CAAC"} \ No newline at end of file diff --git a/out/src/pprof-format-loader.d.ts b/out/src/pprof-format-loader.d.ts new file mode 100644 index 00000000..a3d90137 --- /dev/null +++ b/out/src/pprof-format-loader.d.ts @@ -0,0 +1,11 @@ +/** + * Copyright 2026 Datadog + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + */ +import type * as PprofFormat from 'pprof-format'; +export declare function loadPprofFormat(): typeof PprofFormat; diff --git a/out/src/pprof-format-loader.js b/out/src/pprof-format-loader.js new file mode 100644 index 00000000..a56800a1 --- /dev/null +++ b/out/src/pprof-format-loader.js @@ -0,0 +1,38 @@ +"use strict"; +/** + * Copyright 2026 Datadog + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + */ +Object.defineProperty(exports, "__esModule", { value: true }); +exports.loadPprofFormat = loadPprofFormat; +const path_1 = require("path"); +const runtime_1 = require("./runtime"); +let cachedModule; +function loadFromPackageRoot() { + const packageJsonPath = require.resolve('pprof-format/package.json'); + const packageRoot = packageJsonPath.slice(0, packageJsonPath.length - 'package.json'.length); + return require((0, path_1.join)(packageRoot, 'dist/commonjs/index.js')); +} +function loadPprofFormat() { + if (cachedModule) { + return cachedModule; + } + try { + const loaded = require('pprof-format'); + cachedModule = loaded; + return loaded; + } + catch (error) { + if (runtime_1.runtime !== 'bun') { + throw error; + } + } + cachedModule = loadFromPackageRoot(); + return cachedModule; +} +//# sourceMappingURL=pprof-format-loader.js.map \ No newline at end of file diff --git a/out/src/pprof-format-loader.js.map b/out/src/pprof-format-loader.js.map new file mode 100644 index 00000000..7cd0dc6f --- /dev/null +++ b/out/src/pprof-format-loader.js.map @@ -0,0 +1 @@ +{"version":3,"file":"pprof-format-loader.js","sourceRoot":"","sources":["../../ts/src/pprof-format-loader.ts"],"names":[],"mappings":";AAAA;;;;;;;;GAQG;;AAmBH,0CAiBC;AAlCD,+BAA0B;AAI1B,uCAAkC;AAElC,IAAI,YAA4C,CAAC;AAEjD,SAAS,mBAAmB;IAC1B,MAAM,eAAe,GAAG,OAAO,CAAC,OAAO,CAAC,2BAA2B,CAAC,CAAC;IACrE,MAAM,WAAW,GAAG,eAAe,CAAC,KAAK,CACvC,CAAC,EACD,eAAe,CAAC,MAAM,GAAG,cAAc,CAAC,MAAM,CAC/C,CAAC;IACF,OAAO,OAAO,CAAC,IAAA,WAAI,EAAC,WAAW,EAAE,wBAAwB,CAAC,CAAC,CAAC;AAC9D,CAAC;AAED,SAAgB,eAAe;IAC7B,IAAI,YAAY,EAAE,CAAC;QACjB,OAAO,YAAY,CAAC;IACtB,CAAC;IAED,IAAI,CAAC;QACH,MAAM,MAAM,GAAG,OAAO,CAAC,cAAc,CAAuB,CAAC;QAC7D,YAAY,GAAG,MAAM,CAAC;QACtB,OAAO,MAAM,CAAC;IAChB,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,IAAI,iBAAO,KAAK,KAAK,EAAE,CAAC;YACtB,MAAM,KAAK,CAAC;QACd,CAAC;IACH,CAAC;IAED,YAAY,GAAG,mBAAmB,EAAE,CAAC;IACrC,OAAO,YAAY,CAAC;AACtB,CAAC"} \ No newline at end of file diff --git a/out/src/profile-encoder.d.ts b/out/src/profile-encoder.d.ts new file mode 100644 index 00000000..bdbf28eb --- /dev/null +++ b/out/src/profile-encoder.d.ts @@ -0,0 +1,18 @@ +/** + * Copyright 2019 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import type { Profile } from 'pprof-format'; +export declare function encode(profile: Profile): Promise; +export declare function encodeSync(profile: Profile): Buffer; diff --git a/out/src/profile-encoder.js b/out/src/profile-encoder.js new file mode 100644 index 00000000..d62c4f26 --- /dev/null +++ b/out/src/profile-encoder.js @@ -0,0 +1,29 @@ +"use strict"; +/** + * Copyright 2019 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +Object.defineProperty(exports, "__esModule", { value: true }); +exports.encode = encode; +exports.encodeSync = encodeSync; +const util_1 = require("util"); +const zlib_1 = require("zlib"); +const gzipPromise = (0, util_1.promisify)(zlib_1.gzip); +function encode(profile) { + return profile.encodeAsync().then(gzipPromise); +} +function encodeSync(profile) { + return (0, zlib_1.gzipSync)(profile.encode()); +} +//# sourceMappingURL=profile-encoder.js.map \ No newline at end of file diff --git a/out/src/profile-encoder.js.map b/out/src/profile-encoder.js.map new file mode 100644 index 00000000..efdb5d57 --- /dev/null +++ b/out/src/profile-encoder.js.map @@ -0,0 +1 @@ +{"version":3,"file":"profile-encoder.js","sourceRoot":"","sources":["../../ts/src/profile-encoder.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;;GAcG;;AASH,wBAEC;AAED,gCAEC;AAbD,+BAA+B;AAC/B,+BAAoC;AAIpC,MAAM,WAAW,GAAG,IAAA,gBAAS,EAAC,WAAI,CAAC,CAAC;AAEpC,SAAgB,MAAM,CAAC,OAAgB;IACrC,OAAO,OAAO,CAAC,WAAW,EAAE,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC;AACjD,CAAC;AAED,SAAgB,UAAU,CAAC,OAAgB;IACzC,OAAO,IAAA,eAAQ,EAAC,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC;AACpC,CAAC"} \ No newline at end of file diff --git a/out/src/profile-serializer.d.ts b/out/src/profile-serializer.d.ts new file mode 100644 index 00000000..1a2e3ea6 --- /dev/null +++ b/out/src/profile-serializer.d.ts @@ -0,0 +1,39 @@ +/** + * Copyright 2017 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import type { Profile } from 'pprof-format'; +import { SourceMapper } from './sourcemapper/sourcemapper'; +import { AllocationProfileNode, GenerateAllocationLabelsFunction, GenerateTimeLabelsFunction, TimeProfile } from './v8-types'; +export declare const NON_JS_THREADS_FUNCTION_NAME = "Non JS threads activity"; +export declare const GARBAGE_COLLECTION_FUNCTION_NAME = "Garbage Collection"; +/** + * Converts v8 time profile into into a profile proto. + * (https://github.com/google/pprof/blob/master/proto/profile.proto) + * + * @param prof - profile to be converted. + * @param intervalMicros - average time (microseconds) between samples. + */ +export declare function serializeTimeProfile(prof: TimeProfile, intervalMicros: number, sourceMapper?: SourceMapper, recomputeSamplingInterval?: boolean, generateLabels?: GenerateTimeLabelsFunction, lowCardinalityLabels?: string[]): Profile; +/** + * Converts v8 heap profile into into a profile proto. + * (https://github.com/google/pprof/blob/master/proto/profile.proto) + * + * @param prof - profile to be converted. + * @param startTimeNanos - start time of profile, in nanoseconds (POSIX time). + * @param durationsNanos - duration of the profile (wall clock time) in + * nanoseconds. + * @param intervalBytes - bytes allocated between samples. + */ +export declare function serializeHeapProfile(prof: AllocationProfileNode, startTimeNanos: number, intervalBytes: number, ignoreSamplesPath?: string, sourceMapper?: SourceMapper, generateLabels?: GenerateAllocationLabelsFunction): Profile; diff --git a/out/src/profile-serializer.js b/out/src/profile-serializer.js new file mode 100644 index 00000000..29b5b08b --- /dev/null +++ b/out/src/profile-serializer.js @@ -0,0 +1,435 @@ +"use strict"; +/** + * Copyright 2017 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +Object.defineProperty(exports, "__esModule", { value: true }); +exports.GARBAGE_COLLECTION_FUNCTION_NAME = exports.NON_JS_THREADS_FUNCTION_NAME = void 0; +exports.serializeTimeProfile = serializeTimeProfile; +exports.serializeHeapProfile = serializeHeapProfile; +const pprof_format_loader_1 = require("./pprof-format-loader"); +const pprofFormat = (0, pprof_format_loader_1.loadPprofFormat)(); +const { Function: PprofFunction, Label: PprofLabel, Line: PprofLine, Location: PprofLocation, Profile: PprofProfile, Sample: PprofSample, StringTable: PprofStringTable, ValueType: PprofValueType, } = pprofFormat; +exports.NON_JS_THREADS_FUNCTION_NAME = 'Non JS threads activity'; +exports.GARBAGE_COLLECTION_FUNCTION_NAME = 'Garbage Collection'; +function isGeneratedLocation(location) { + return (location.column !== undefined && + location.line !== undefined && + location.line > 0); +} +/** + * Takes v8 profile and populates sample, location, and function fields of + * profile.proto. + * + * @param profile - profile.proto with empty sample, location, and function + * fields. + * @param root - root of v8 profile tree describing samples to be appended + * to profile. + * @param appendToSamples - function which converts entry to sample(s) and + * appends these to end of an array of samples. + * @param stringTable - string table for the existing profile. + */ +function serialize(profile, root, appendToSamples, stringTable, ignoreSamplesPath, sourceMapper) { + const samples = []; + const locations = []; + const functions = []; + const functionIdMap = new Map(); + const locationIdMap = new Map(); + const entries = root.children.map((n) => ({ + node: n, + stack: [], + })); + while (entries.length > 0) { + const entry = entries.pop(); + const node = entry.node; + // mjs files have a `file://` prefix in the scriptName -> remove it + if (node.scriptName.startsWith('file://')) { + node.scriptName = node.scriptName.slice(7); + } + if (ignoreSamplesPath && node.scriptName.indexOf(ignoreSamplesPath) > -1) { + continue; + } + const stack = entry.stack; + const location = getLocation(node, sourceMapper); + stack.unshift(location.id); + appendToSamples(entry, samples); + for (const child of node.children) { + entries.push({ node: child, stack: stack.slice() }); + } + } + profile.sample = samples; + profile.location = locations; + profile.function = functions; + profile.stringTable = stringTable; + function getLocation(node, sourceMapper) { + let profLoc = { + file: node.scriptName || '', + line: node.lineNumber, + column: node.columnNumber, + name: node.name, + }; + if (profLoc.line) { + if (sourceMapper && isGeneratedLocation(profLoc)) { + profLoc = sourceMapper.mappingInfo(profLoc); + } + } + const keyStr = `${node.scriptId}:${profLoc.line}:${profLoc.column}:${profLoc.name}`; + let id = locationIdMap.get(keyStr); + if (id !== undefined) { + // id is index+1, since 0 is not valid id. + return locations[id - 1]; + } + id = locations.length + 1; + locationIdMap.set(keyStr, id); + const line = getLine(profLoc, node.scriptId); + const location = new PprofLocation({ id, line: [line] }); + locations.push(location); + return location; + } + function getLine(loc, scriptId) { + return new PprofLine({ + functionId: getFunction(loc, scriptId).id, + line: loc.line, + }); + } + function getFunction(loc, scriptId) { + let name = loc.name; + const keyStr = name + ? `${scriptId}:${name}` + : `${scriptId}:${loc.line}:${loc.column}`; + let id = functionIdMap.get(keyStr); + if (id !== undefined) { + // id is index+1, since 0 is not valid id. + return functions[id - 1]; + } + id = functions.length + 1; + functionIdMap.set(keyStr, id); + if (!name) { + if (loc.line) { + if (loc.column) { + name = `(anonymous:L#${loc.line}:C#${loc.column})`; + } + else { + name = `(anonymous:L#${loc.line})`; + } + } + else { + name = '(anonymous)'; + } + } + const nameId = stringTable.dedup(name); + const f = new PprofFunction({ + id, + name: nameId, + systemName: nameId, + filename: stringTable.dedup(loc.file || ''), + }); + functions.push(f); + return f; + } +} +/** + * @return value type for sample counts (type:sample, units:count), and + * adds strings used in this value type to the table. + */ +function createSampleCountValueType(table) { + return new PprofValueType({ + type: table.dedup('sample'), + unit: table.dedup('count'), + }); +} +/** + * @return value type for time samples (type:wall, units:nanoseconds), and + * adds strings used in this value type to the table. + */ +function createTimeValueType(table) { + return new PprofValueType({ + type: table.dedup('wall'), + unit: table.dedup('nanoseconds'), + }); +} +/** + * @return value type for cpu samples (type:cpu, units:nanoseconds), and + * adds strings used in this value type to the table. + */ +function createCpuValueType(table) { + return new PprofValueType({ + type: table.dedup('cpu'), + unit: table.dedup('nanoseconds'), + }); +} +/** + * @return value type for object counts (type:objects, units:count), and + * adds strings used in this value type to the table. + */ +function createObjectCountValueType(table) { + return new PprofValueType({ + type: table.dedup('objects'), + unit: table.dedup('count'), + }); +} +/** + * @return value type for memory allocations (type:space, units:bytes), and + * adds strings used in this value type to the table. + */ +function createAllocationValueType(table) { + return new PprofValueType({ + type: table.dedup('space'), + unit: table.dedup('bytes'), + }); +} +function computeTotalHitCount(root) { + return (root.hitCount + + root.children.reduce((sum, node) => sum + computeTotalHitCount(node), 0)); +} +/** Perform some modifications on time profile: + * - Add non-JS thread activity node if available + * - remove `(program)` nodes + * - remove `(idle)` nodes with no context + * - set `(idle)` nodes' wall time to zero when they have a context + * - Convert `(garbage collector)` node to `Garbage Collection` + * - Put `non-JS thread activity` node and `Garbage Collection` under a top level `Node.js` node + * This function does not change the input profile. + */ +function updateTimeProfile(prof) { + const newTopLevelChildren = []; + let runtimeNode; + function getRuntimeNode() { + if (!runtimeNode) { + runtimeNode = { + name: 'Node.js', + scriptName: '', + scriptId: 0, + lineNumber: 0, + columnNumber: 0, + children: [], + hitCount: 0, + }; + newTopLevelChildren.push(runtimeNode); + } + return runtimeNode; + } + for (const child of prof.topDownRoot.children) { + if (child.name === '(program)') { + continue; + } + if (child.name === '(idle)' && child.contexts?.length === 0) { + continue; + } + if (child.name === '(garbage collector)') { + // Create a new node to avoid modifying the input one + const newChild = { + ...child, + name: exports.GARBAGE_COLLECTION_FUNCTION_NAME, + }; + getRuntimeNode().children.push(newChild); + } + else { + newTopLevelChildren.push(child); + } + } + if (prof.hasCpuTime && prof.nonJSThreadsCpuTime) { + const node = { + name: exports.NON_JS_THREADS_FUNCTION_NAME, + scriptName: '', + scriptId: 0, + lineNumber: 0, + columnNumber: 0, + children: [], + hitCount: 0, // 0 because this should not be accounted for wall time + contexts: [ + { + context: {}, + timestamp: BigInt(0), + cpuTime: prof.nonJSThreadsCpuTime, + asyncId: -1, + }, + ], + }; + getRuntimeNode().children.push(node); + } + return { + ...prof, + topDownRoot: { ...prof.topDownRoot, children: newTopLevelChildren }, + }; +} +/** + * Converts v8 time profile into into a profile proto. + * (https://github.com/google/pprof/blob/master/proto/profile.proto) + * + * @param prof - profile to be converted. + * @param intervalMicros - average time (microseconds) between samples. + */ +function serializeTimeProfile(prof, intervalMicros, sourceMapper, recomputeSamplingInterval = false, generateLabels, lowCardinalityLabels = []) { + // If requested, recompute sampling interval from profile duration and total number of hits, + // since profile duration should be #hits x interval. + // Recomputing an average interval is more accurate, since in practice intervals between + // samples are larger than the requested sampling interval (eg. 12.5ms vs 10ms requested). + // For very short durations, computation becomes meaningless (eg. if there is only one hit), + // therefore keep intervalMicros as a lower bound and 2 * intervalMicros as upper bound. + if (recomputeSamplingInterval) { + const totalHitCount = computeTotalHitCount(prof.topDownRoot); + if (totalHitCount > 0) { + intervalMicros = Math.min(Math.max(Math.floor((prof.endTime - prof.startTime) / totalHitCount), intervalMicros), 2 * intervalMicros); + } + } + const intervalNanos = intervalMicros * 1000; + const stringTable = new PprofStringTable(); + const labelCaches = []; + for (const l of lowCardinalityLabels) { + labelCaches[stringTable.dedup(l)] = new Map(); + } + const dedupLabels = (labels) => { + for (let i = 0; i < labels.length; i++) { + const label = labels[i]; + const cache = labelCaches[Number(label.key)]; + if (cache !== undefined) { + const key = label.str ?? label.num; + const exlabel = cache.get(key); + if (exlabel === undefined) { + cache.set(key, label); + } + else if (label.str === exlabel.str && + label.num === exlabel.num && + label.numUnit === exlabel.numUnit) { + labels[i] = exlabel; + } + } + } + return labels; + }; + const appendTimeEntryToSamples = (entry, samples) => { + let unlabelledHits = entry.node.hitCount; + let unlabelledCpuTime = 0; + const isIdle = entry.node.name === '(idle)'; + for (const context of entry.node.contexts || []) { + const labels = generateLabels + ? generateLabels({ node: entry.node, context }) + : context.context ?? {}; + const labelsArr = buildLabels(labels, stringTable); + if (labelsArr.length > 0) { + // Only assign wall time if there are hits, some special nodes such as `(Non-JS threads)` + // have zero hit count (since they do not count as wall time) and should not be assigned any + // wall time. Also, `(idle)` nodes should be assigned zero wall time. + const values = unlabelledHits > 0 ? [1, isIdle ? 0 : intervalNanos] : [0, 0]; + if (prof.hasCpuTime) { + values.push(context.cpuTime ?? 0); + } + const sample = new PprofSample({ + locationId: entry.stack, + value: values, + label: dedupLabels(labelsArr), + }); + samples.push(sample); + unlabelledHits--; + } + else if (prof.hasCpuTime) { + unlabelledCpuTime += context.cpuTime ?? 0; + } + } + if ((!isIdle && unlabelledHits > 0) || unlabelledCpuTime > 0) { + const labels = generateLabels ? generateLabels({ node: entry.node }) : {}; + const values = unlabelledHits > 0 + ? [unlabelledHits, isIdle ? 0 : unlabelledHits * intervalNanos] + : [0, 0]; + if (prof.hasCpuTime) { + values.push(unlabelledCpuTime); + } + const sample = new PprofSample({ + locationId: entry.stack, + value: values, + label: buildLabels(labels, stringTable), + }); + samples.push(sample); + } + }; + const sampleValueType = createSampleCountValueType(stringTable); + const timeValueType = createTimeValueType(stringTable); + const sampleTypes = [sampleValueType, timeValueType]; + if (prof.hasCpuTime) { + const cpuValueType = createCpuValueType(stringTable); + sampleTypes.push(cpuValueType); + } + const profile = { + sampleType: sampleTypes, + timeNanos: Date.now() * 1000 * 1000, + durationNanos: (prof.endTime - prof.startTime) * 1000, + periodType: timeValueType, + period: intervalNanos, + }; + const updatedProf = updateTimeProfile(prof); + serialize(profile, updatedProf.topDownRoot, appendTimeEntryToSamples, stringTable, undefined, sourceMapper); + return new PprofProfile(profile); +} +function buildLabels(labelSet, stringTable) { + const labels = []; + for (const [key, value] of Object.entries(labelSet)) { + const labelInput = { + key: stringTable.dedup(key), + }; + switch (typeof value) { + case 'string': + labelInput.str = stringTable.dedup(value); + break; + case 'number': + case 'bigint': + labelInput.num = value; + break; + default: + continue; + } + labels.push(new PprofLabel(labelInput)); + } + return labels; +} +/** + * Converts v8 heap profile into into a profile proto. + * (https://github.com/google/pprof/blob/master/proto/profile.proto) + * + * @param prof - profile to be converted. + * @param startTimeNanos - start time of profile, in nanoseconds (POSIX time). + * @param durationsNanos - duration of the profile (wall clock time) in + * nanoseconds. + * @param intervalBytes - bytes allocated between samples. + */ +function serializeHeapProfile(prof, startTimeNanos, intervalBytes, ignoreSamplesPath, sourceMapper, generateLabels) { + const appendHeapEntryToSamples = (entry, samples) => { + if (entry.node.allocations.length > 0) { + const labels = generateLabels + ? buildLabels(generateLabels({ node: entry.node }), stringTable) + : []; + for (const alloc of entry.node.allocations) { + const sample = new PprofSample({ + locationId: entry.stack, + value: [alloc.count, alloc.sizeBytes * alloc.count], + label: labels, + // TODO: add tag for allocation size + }); + samples.push(sample); + } + } + }; + const stringTable = new PprofStringTable(); + const sampleValueType = createObjectCountValueType(stringTable); + const allocationValueType = createAllocationValueType(stringTable); + const profile = { + sampleType: [sampleValueType, allocationValueType], + timeNanos: startTimeNanos, + periodType: allocationValueType, + period: intervalBytes, + }; + serialize(profile, prof, appendHeapEntryToSamples, stringTable, ignoreSamplesPath, sourceMapper); + return new PprofProfile(profile); +} +//# sourceMappingURL=profile-serializer.js.map \ No newline at end of file diff --git a/out/src/profile-serializer.js.map b/out/src/profile-serializer.js.map new file mode 100644 index 00000000..d095e492 --- /dev/null +++ b/out/src/profile-serializer.js.map @@ -0,0 +1 @@ +{"version":3,"file":"profile-serializer.js","sourceRoot":"","sources":["../../ts/src/profile-serializer.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/out/src/runtime.d.ts b/out/src/runtime.d.ts new file mode 100644 index 00000000..9d96e0a1 --- /dev/null +++ b/out/src/runtime.d.ts @@ -0,0 +1,21 @@ +/** + * Copyright 2026 Datadog + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + */ +export type Runtime = 'node' | 'bun'; +type RuntimeDetectionInputs = { + envOverride: string | undefined; + bunVersion: string | undefined; + bunGlobal: unknown; +}; +declare function detectRuntimeFromInputs({ envOverride, bunVersion, bunGlobal, }: RuntimeDetectionInputs): Runtime; +export declare const __testing: { + detectRuntimeFromInputs: typeof detectRuntimeFromInputs; +}; +export declare const runtime: Runtime; +export {}; diff --git a/out/src/runtime.js b/out/src/runtime.js new file mode 100644 index 00000000..c0bd06bc --- /dev/null +++ b/out/src/runtime.js @@ -0,0 +1,51 @@ +"use strict"; +/** + * Copyright 2026 Datadog + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + */ +Object.defineProperty(exports, "__esModule", { value: true }); +exports.runtime = exports.__testing = void 0; +const RUNTIME_ENV_KEY = 'DATADOG_PPROF_RUNTIME'; +function env() { + return process.env; +} +function detectRuntimeFromEnv() { + const override = env()[RUNTIME_ENV_KEY]; + if (override === 'node' || override === 'bun') { + return override; + } + return undefined; +} +function detectRuntime() { + const envRuntime = detectRuntimeFromEnv(); + if (envRuntime) { + return envRuntime; + } + return detectRuntimeFromInputs({ + envOverride: undefined, + bunVersion: process.versions.bun, + bunGlobal: globalThis.Bun, + }); +} +function detectRuntimeFromInputs({ envOverride, bunVersion, bunGlobal, }) { + if (envOverride === 'node' || envOverride === 'bun') { + return envOverride; + } + if (typeof bunVersion === 'string') { + return 'bun'; + } + if (typeof bunGlobal !== 'undefined') { + return 'bun'; + } + return 'node'; +} +exports.__testing = { + detectRuntimeFromInputs, +}; +exports.runtime = detectRuntime(); +//# sourceMappingURL=runtime.js.map \ No newline at end of file diff --git a/out/src/runtime.js.map b/out/src/runtime.js.map new file mode 100644 index 00000000..e58060a8 --- /dev/null +++ b/out/src/runtime.js.map @@ -0,0 +1 @@ +{"version":3,"file":"runtime.js","sourceRoot":"","sources":["../../ts/src/runtime.ts"],"names":[],"mappings":";AAAA;;;;;;;;GAQG;;;AAIH,MAAM,eAAe,GAAG,uBAAuB,CAAC;AAEhD,SAAS,GAAG;IACV,OAAO,OAAO,CAAC,GAAG,CAAC;AACrB,CAAC;AAQD,SAAS,oBAAoB;IAC3B,MAAM,QAAQ,GAAG,GAAG,EAAE,CAAC,eAAe,CAAC,CAAC;IACxC,IAAI,QAAQ,KAAK,MAAM,IAAI,QAAQ,KAAK,KAAK,EAAE,CAAC;QAC9C,OAAO,QAAQ,CAAC;IAClB,CAAC;IACD,OAAO,SAAS,CAAC;AACnB,CAAC;AAED,SAAS,aAAa;IACpB,MAAM,UAAU,GAAG,oBAAoB,EAAE,CAAC;IAC1C,IAAI,UAAU,EAAE,CAAC;QACf,OAAO,UAAU,CAAC;IACpB,CAAC;IAED,OAAO,uBAAuB,CAAC;QAC7B,WAAW,EAAE,SAAS;QACtB,UAAU,EAAE,OAAO,CAAC,QAAQ,CAAC,GAAG;QAChC,SAAS,EAAG,UAA8B,CAAC,GAAG;KAC/C,CAAC,CAAC;AACL,CAAC;AAED,SAAS,uBAAuB,CAAC,EAC/B,WAAW,EACX,UAAU,EACV,SAAS,GACc;IACvB,IAAI,WAAW,KAAK,MAAM,IAAI,WAAW,KAAK,KAAK,EAAE,CAAC;QACpD,OAAO,WAAW,CAAC;IACrB,CAAC;IAED,IAAI,OAAO,UAAU,KAAK,QAAQ,EAAE,CAAC;QACnC,OAAO,KAAK,CAAC;IACf,CAAC;IAED,IAAI,OAAO,SAAS,KAAK,WAAW,EAAE,CAAC;QACrC,OAAO,KAAK,CAAC;IACf,CAAC;IAED,OAAO,MAAM,CAAC;AAChB,CAAC;AAEY,QAAA,SAAS,GAAG;IACvB,uBAAuB;CACxB,CAAC;AAEW,QAAA,OAAO,GAAG,aAAa,EAAE,CAAC"} \ No newline at end of file diff --git a/out/src/sourcemapper/sourcemapper.d.ts b/out/src/sourcemapper/sourcemapper.d.ts new file mode 100644 index 00000000..7ff0b856 --- /dev/null +++ b/out/src/sourcemapper/sourcemapper.d.ts @@ -0,0 +1,92 @@ +/** + * Copyright 2016 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import * as sourceMap from 'source-map'; +export interface MapInfoCompiled { + mapFileDir: string; + mapConsumer: sourceMap.RawSourceMap; +} +export interface GeneratedLocation { + file: string; + name?: string; + line: number; + column: number; +} +export interface SourceLocation { + file?: string; + name?: string; + line?: number; + column?: number; +} +export declare class SourceMapper { + infoMap: Map; + debug: boolean; + static create(searchDirs: string[], debug?: boolean): Promise; + /** + * @param {Array.} sourceMapPaths An array of paths to .map source map + * files that should be processed. The paths should be relative to the + * current process's current working directory + * @param {Logger} logger A logger that reports errors that occurred while + * processing the given source map files + * @constructor + */ + constructor(debug?: boolean); + /** + * Used to get the information about the transpiled file from a given input + * source file provided there isn't any ambiguity with associating the input + * path to exactly one output transpiled file. + * + * @param inputPath The (possibly relative) path to the original source file. + * @return The `MapInfoCompiled` object that describes the transpiled file + * associated with the specified input path. `null` is returned if either + * zero files are associated with the input path or if more than one file + * could possibly be associated with the given input path. + */ + private getMappingInfo; + /** + * Used to determine if the source file specified by the given path has + * a .map file and an output file associated with it. + * + * If there is no such mapping, it could be because the input file is not + * the input to a transpilation process or it is the input to a transpilation + * process but its corresponding .map file was not given to the constructor + * of this mapper. + * + * @param {string} inputPath The path to an input file that could + * possibly be the input to a transpilation process. The path should be + * relative to the process's current working directory. + */ + hasMappingInfo(inputPath: string): boolean; + /** + * @param {string} inputPath The path to an input file that could possibly + * be the input to a transpilation process. The path should be relative to + * the process's current working directory + * @param {number} The line number in the input file where the line number is + * zero-based. + * @param {number} (Optional) The column number in the line of the file + * specified where the column number is zero-based. + * @return {Object} The object returned has a "file" attribute for the + * path of the output file associated with the given input file (where the + * path is relative to the process's current working directory), + * a "line" attribute of the line number in the output file associated with + * the given line number for the input file, and an optional "column" number + * of the column number of the output file associated with the given file + * and line information. + * + * If the given input file does not have mapping information associated + * with it then the input location is returned. + */ + mappingInfo(location: GeneratedLocation): SourceLocation; +} diff --git a/out/src/sourcemapper/sourcemapper.js b/out/src/sourcemapper/sourcemapper.js new file mode 100644 index 00000000..eb3a366f --- /dev/null +++ b/out/src/sourcemapper/sourcemapper.js @@ -0,0 +1,338 @@ +"use strict"; +/** + * Copyright 2016 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); +}) : function(o, v) { + o["default"] = v; +}); +var __importStar = (this && this.__importStar) || (function () { + var ownKeys = function(o) { + ownKeys = Object.getOwnPropertyNames || function (o) { + var ar = []; + for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; + return ar; + }; + return ownKeys(o); + }; + return function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); + __setModuleDefault(result, mod); + return result; + }; +})(); +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.SourceMapper = void 0; +// Originally copied from cloud-debug-nodejs's sourcemapper.ts from +// https://github.com/googleapis/cloud-debug-nodejs/blob/7bdc2f1f62a3b45b7b53ea79f9444c8ed50e138b/src/agent/io/sourcemapper.ts +// Modified to map from generated code to source code, rather than from source +// code to generated code. +const fs = __importStar(require("fs")); +const path = __importStar(require("path")); +const sourceMap = __importStar(require("source-map")); +const logger_1 = require("../logger"); +const p_limit_1 = __importDefault(require("p-limit")); +const readFile = fs.promises.readFile; +const CONCURRENCY = 10; +const MAP_EXT = '.map'; +function error(msg) { + logger_1.logger.debug(`Error: ${msg}`); + return new Error(msg); +} +/** + * @param {!Map} infoMap The map that maps input source files to + * SourceMapConsumer objects that are used to calculate mapping information + * @param {string} mapPath The path to the source map file to process. The + * path should be relative to the process's current working directory + * @private + */ +async function processSourceMap(infoMap, mapPath, debug) { + // this handles the case when the path is undefined, null, or + // the empty string + if (!mapPath || !mapPath.endsWith(MAP_EXT)) { + throw error(`The path "${mapPath}" does not specify a source map file`); + } + mapPath = path.normalize(mapPath); + let contents; + try { + contents = await readFile(mapPath, 'utf8'); + } + catch (e) { + throw error('Could not read source map file ' + mapPath + ': ' + e); + } + let consumer; + try { + // TODO: Determine how to reconsile the type conflict where `consumer` + // is constructed as a SourceMapConsumer but is used as a + // RawSourceMap. + // TODO: Resolve the cast of `contents as any` (This is needed because the + // type is expected to be of `RawSourceMap` but the existing + // working code uses a string.) + consumer = (await new sourceMap.SourceMapConsumer(contents)); + } + catch (e) { + throw error('An error occurred while reading the ' + + 'sourceMap file ' + + mapPath + + ': ' + + e); + } + /* If the source map file defines a "file" attribute, use it as + * the output file where the path is relative to the directory + * containing the map file. Otherwise, use the name of the output + * file (with the .map extension removed) as the output file. + + * With nextjs/webpack, when there are subdirectories in `pages` directory, + * the generated source maps do not reference correctly the generated files + * in their `file` property. + * For example if the generated file / source maps have paths: + * /pages/sub/foo.js(.map) + * foo.js.map will have ../pages/sub/foo.js as `file` property instead of + * ../../pages/sub/foo.js + * To workaround this, check first if file referenced in `file` property + * exists and if it does not, check if generated file exists alongside the + * source map file. + */ + const dir = path.dirname(mapPath); + const generatedPathCandidates = []; + if (consumer.file) { + generatedPathCandidates.push(path.resolve(dir, consumer.file)); + } + const samePath = path.resolve(dir, path.basename(mapPath, MAP_EXT)); + if (generatedPathCandidates.length === 0 || + generatedPathCandidates[0] !== samePath) { + generatedPathCandidates.push(samePath); + } + for (const generatedPath of generatedPathCandidates) { + try { + await fs.promises.access(generatedPath, fs.constants.F_OK); + infoMap.set(generatedPath, { mapFileDir: dir, mapConsumer: consumer }); + if (debug) { + logger_1.logger.debug(`Loaded source map for ${generatedPath} => ${mapPath}`); + } + return; + } + catch (err) { + if (debug) { + logger_1.logger.debug(`Generated path ${generatedPath} does not exist`); + } + } + } + if (debug) { + logger_1.logger.debug(`Unable to find generated file for ${mapPath}`); + } +} +class SourceMapper { + static async create(searchDirs, debug = false) { + if (debug) { + logger_1.logger.debug(`Looking for source map files in dirs: [${searchDirs.join(', ')}]`); + } + const mapFiles = []; + for (const dir of searchDirs) { + try { + const mf = await getMapFiles(dir); + mf.forEach(mapFile => { + mapFiles.push(path.resolve(dir, mapFile)); + }); + } + catch (e) { + throw error(`failed to get source maps from ${dir}: ${e}`); + } + } + if (debug) { + logger_1.logger.debug(`Found source map files: [${mapFiles.join(', ')}]`); + } + return createFromMapFiles(mapFiles, debug); + } + /** + * @param {Array.} sourceMapPaths An array of paths to .map source map + * files that should be processed. The paths should be relative to the + * current process's current working directory + * @param {Logger} logger A logger that reports errors that occurred while + * processing the given source map files + * @constructor + */ + constructor(debug = false) { + this.infoMap = new Map(); + this.debug = debug; + } + /** + * Used to get the information about the transpiled file from a given input + * source file provided there isn't any ambiguity with associating the input + * path to exactly one output transpiled file. + * + * @param inputPath The (possibly relative) path to the original source file. + * @return The `MapInfoCompiled` object that describes the transpiled file + * associated with the specified input path. `null` is returned if either + * zero files are associated with the input path or if more than one file + * could possibly be associated with the given input path. + */ + getMappingInfo(inputPath) { + const normalizedPath = path.normalize(inputPath); + if (this.infoMap.has(normalizedPath)) { + return this.infoMap.get(normalizedPath); + } + return null; + } + /** + * Used to determine if the source file specified by the given path has + * a .map file and an output file associated with it. + * + * If there is no such mapping, it could be because the input file is not + * the input to a transpilation process or it is the input to a transpilation + * process but its corresponding .map file was not given to the constructor + * of this mapper. + * + * @param {string} inputPath The path to an input file that could + * possibly be the input to a transpilation process. The path should be + * relative to the process's current working directory. + */ + hasMappingInfo(inputPath) { + return this.getMappingInfo(inputPath) !== null; + } + /** + * @param {string} inputPath The path to an input file that could possibly + * be the input to a transpilation process. The path should be relative to + * the process's current working directory + * @param {number} The line number in the input file where the line number is + * zero-based. + * @param {number} (Optional) The column number in the line of the file + * specified where the column number is zero-based. + * @return {Object} The object returned has a "file" attribute for the + * path of the output file associated with the given input file (where the + * path is relative to the process's current working directory), + * a "line" attribute of the line number in the output file associated with + * the given line number for the input file, and an optional "column" number + * of the column number of the output file associated with the given file + * and line information. + * + * If the given input file does not have mapping information associated + * with it then the input location is returned. + */ + mappingInfo(location) { + const inputPath = path.normalize(location.file); + const entry = this.getMappingInfo(inputPath); + if (entry === null) { + if (this.debug) { + logger_1.logger.debug(`Source map lookup failed: no map found for ${location.file} (normalized: ${inputPath})`); + } + return location; + } + const generatedPos = { + line: location.line, + column: location.column > 0 ? location.column - 1 : 0, // SourceMapConsumer expects column to be 0-based + }; + // TODO: Determine how to remove the explicit cast here. + const consumer = entry.mapConsumer; + // When column is 0, we don't have real column info (e.g., from V8's LineTick + // which only provides line numbers). Use LEAST_UPPER_BOUND to find the first + // mapping on this line instead of failing because there's nothing at column 0. + const bias = generatedPos.column === 0 + ? sourceMap.SourceMapConsumer.LEAST_UPPER_BOUND + : sourceMap.SourceMapConsumer.GREATEST_LOWER_BOUND; + const pos = consumer.originalPositionFor({ ...generatedPos, bias }); + if (pos.source === null) { + if (this.debug) { + logger_1.logger.debug(`Source map lookup failed for ${location.name}(${location.file}:${location.line}:${location.column})`); + } + return location; + } + const loc = { + file: path.resolve(entry.mapFileDir, pos.source), + line: pos.line || undefined, + name: pos.name || location.name, + column: pos.column === null ? undefined : pos.column + 1, // convert column back to 1-based + }; + if (this.debug) { + logger_1.logger.debug(`Source map lookup succeeded for ${location.name}(${location.file}:${location.line}:${location.column}) => ${loc.name}(${loc.file}:${loc.line}:${loc.column})`); + } + return loc; + } +} +exports.SourceMapper = SourceMapper; +async function createFromMapFiles(mapFiles, debug) { + const limit = (0, p_limit_1.default)(CONCURRENCY); + const mapper = new SourceMapper(debug); + const promises = mapFiles.map(mapPath => limit(() => processSourceMap(mapper.infoMap, mapPath, debug))); + try { + await Promise.all(promises); + } + catch (err) { + throw error('An error occurred while processing the source map files' + err); + } + return mapper; +} +function isErrnoException(e) { + return e instanceof Error && 'code' in e; +} +function isNonFatalError(error) { + const nonFatalErrors = ['ENOENT', 'EPERM', 'EACCES', 'ELOOP']; + return (isErrnoException(error) && error.code && nonFatalErrors.includes(error.code)); +} +async function* walk(dir, +// eslint-disable-next-line @typescript-eslint/no-unused-vars +fileFilter = (filename) => true, +// eslint-disable-next-line @typescript-eslint/no-unused-vars +directoryFilter = (root, dirname) => true) { + async function* walkRecursive(dir) { + try { + for await (const d of await fs.promises.opendir(dir)) { + const entry = path.join(dir, d.name); + if (d.isDirectory() && directoryFilter(dir, d.name)) { + yield* walkRecursive(entry); + } + else if (d.isFile() && fileFilter(d.name)) { + // check that the file is readable + await fs.promises.access(entry, fs.constants.R_OK); + yield entry; + } + } + } + catch (error) { + if (!isNonFatalError(error)) { + throw error; + } + else { + logger_1.logger.debug(() => `Non fatal error: ${error}`); + } + } + } + yield* walkRecursive(dir); +} +async function getMapFiles(baseDir) { + const mapFiles = []; + for await (const entry of walk(baseDir, filename => /\.[cm]?js\.map$/.test(filename), (root, dirname) => root !== '/proc' && dirname !== '.git' && dirname !== 'node_modules')) { + mapFiles.push(path.relative(baseDir, entry)); + } + return mapFiles; +} +//# sourceMappingURL=sourcemapper.js.map \ No newline at end of file diff --git a/out/src/sourcemapper/sourcemapper.js.map b/out/src/sourcemapper/sourcemapper.js.map new file mode 100644 index 00000000..14110dbd --- /dev/null +++ b/out/src/sourcemapper/sourcemapper.js.map @@ -0,0 +1 @@ +{"version":3,"file":"sourcemapper.js","sourceRoot":"","sources":["../../../ts/src/sourcemapper/sourcemapper.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/out/src/time-profiler-bindings.d.ts b/out/src/time-profiler-bindings.d.ts new file mode 100644 index 00000000..ea2552ba --- /dev/null +++ b/out/src/time-profiler-bindings.d.ts @@ -0,0 +1,15 @@ +export declare const TimeProfiler: new (...args: unknown[]) => { + start: () => void; + stop: (restart: boolean) => import("./v8-types").TimeProfile; + dispose: () => void; + v8ProfilerStuckEventLoopDetected: () => number; + context: object | undefined; + metrics: import("./v8-types").TimeProfilerMetrics; + state: { + [key: string]: number; + }; +}; +export declare const constants: { + kSampleCount: string; +}; +export declare const getNativeThreadId: () => number; diff --git a/out/src/time-profiler-bindings.js b/out/src/time-profiler-bindings.js new file mode 100644 index 00000000..b301048b --- /dev/null +++ b/out/src/time-profiler-bindings.js @@ -0,0 +1,24 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.getNativeThreadId = exports.constants = exports.TimeProfiler = void 0; +/** + * Copyright 2018 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +const native_backend_loader_1 = require("./native-backend-loader"); +const profiler = (0, native_backend_loader_1.loadNativeModule)(); +exports.TimeProfiler = profiler.TimeProfiler; +exports.constants = profiler.constants; +exports.getNativeThreadId = profiler.getNativeThreadId; +//# sourceMappingURL=time-profiler-bindings.js.map \ No newline at end of file diff --git a/out/src/time-profiler-bindings.js.map b/out/src/time-profiler-bindings.js.map new file mode 100644 index 00000000..8cd89bc5 --- /dev/null +++ b/out/src/time-profiler-bindings.js.map @@ -0,0 +1 @@ +{"version":3,"file":"time-profiler-bindings.js","sourceRoot":"","sources":["../../ts/src/time-profiler-bindings.ts"],"names":[],"mappings":";;;AAAA;;;;;;;;;;;;;;GAcG;AACH,mEAAyD;AAEzD,MAAM,QAAQ,GAAG,IAAA,wCAAgB,GAAE,CAAC;AAEvB,QAAA,YAAY,GAAG,QAAQ,CAAC,YAAY,CAAC;AACrC,QAAA,SAAS,GAAG,QAAQ,CAAC,SAAS,CAAC;AAC/B,QAAA,iBAAiB,GAAG,QAAQ,CAAC,iBAAiB,CAAC"} \ No newline at end of file diff --git a/out/src/time-profiler.d.ts b/out/src/time-profiler.d.ts new file mode 100644 index 00000000..497a0236 --- /dev/null +++ b/out/src/time-profiler.d.ts @@ -0,0 +1,56 @@ +/** + * Copyright 2017 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { SourceMapper } from './sourcemapper/sourcemapper'; +import { getNativeThreadId } from './time-profiler-bindings'; +import { GenerateTimeLabelsFunction, TimeProfilerMetrics } from './v8-types'; +type Microseconds = number; +type Milliseconds = number; +export interface TimeProfilerOptions { + /** time in milliseconds for which to collect profile. */ + durationMillis?: Milliseconds; + /** average time in microseconds between samples */ + intervalMicros?: Microseconds; + sourceMapper?: SourceMapper; + /** + * This configuration option is experimental. + * When set to true, functions will be aggregated at the line level, rather + * than at the function level. + * This defaults to false. + */ + lineNumbers?: boolean; + withContexts?: boolean; + workaroundV8Bug?: boolean; + collectCpuTime?: boolean; + collectAsyncId?: boolean; + useCPED?: boolean; +} +export declare function profile(options?: TimeProfilerOptions): Promise; +export declare function start(options?: TimeProfilerOptions): void; +export declare function stop(restart?: boolean, generateLabels?: GenerateTimeLabelsFunction, lowCardinalityLabels?: string[]): import("pprof-format").Profile; +export declare function getState(): { + [key: string]: number; +}; +export declare function setContext(context?: object): void; +export declare function getContext(): object | undefined; +export declare function getMetrics(): TimeProfilerMetrics; +export declare function isStarted(): boolean; +export declare function v8ProfilerStuckEventLoopDetected(): number; +export declare const constants: { + kSampleCount: string; + GARBAGE_COLLECTION_FUNCTION_NAME: string; + NON_JS_THREADS_FUNCTION_NAME: string; +}; +export { getNativeThreadId }; diff --git a/out/src/time-profiler.js b/out/src/time-profiler.js new file mode 100644 index 00000000..2801a32f --- /dev/null +++ b/out/src/time-profiler.js @@ -0,0 +1,144 @@ +"use strict"; +/** + * Copyright 2017 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.getNativeThreadId = exports.constants = void 0; +exports.profile = profile; +exports.start = start; +exports.stop = stop; +exports.getState = getState; +exports.setContext = setContext; +exports.getContext = getContext; +exports.getMetrics = getMetrics; +exports.isStarted = isStarted; +exports.v8ProfilerStuckEventLoopDetected = v8ProfilerStuckEventLoopDetected; +const delay_1 = __importDefault(require("delay")); +const profile_serializer_1 = require("./profile-serializer"); +const time_profiler_bindings_1 = require("./time-profiler-bindings"); +Object.defineProperty(exports, "getNativeThreadId", { enumerable: true, get: function () { return time_profiler_bindings_1.getNativeThreadId; } }); +const worker_threads_1 = require("worker_threads"); +const { kSampleCount } = time_profiler_bindings_1.constants; +const DEFAULT_INTERVAL_MICROS = 1000; +const DEFAULT_DURATION_MILLIS = 60000; +let gProfiler; +let gSourceMapper; +let gIntervalMicros; +let gV8ProfilerStuckEventLoopDetected = 0; +/** Make sure to stop profiler before node shuts down, otherwise profiling + * signal might cause a crash if it occurs during shutdown */ +process.once('exit', () => { + if (isStarted()) + stop(); +}); +const DEFAULT_OPTIONS = { + durationMillis: DEFAULT_DURATION_MILLIS, + intervalMicros: DEFAULT_INTERVAL_MICROS, + lineNumbers: false, + withContexts: false, + workaroundV8Bug: true, + collectCpuTime: false, + collectAsyncId: false, + useCPED: false, +}; +async function profile(options = {}) { + options = { ...DEFAULT_OPTIONS, ...options }; + start(options); + await (0, delay_1.default)(options.durationMillis); + return stop(); +} +// Temporarily retained for backwards compatibility with older tracer +function start(options = {}) { + options = { ...DEFAULT_OPTIONS, ...options }; + if (gProfiler) { + throw new Error('Wall profiler is already started'); + } + gProfiler = new time_profiler_bindings_1.TimeProfiler({ ...options, isMainThread: worker_threads_1.isMainThread }); + gSourceMapper = options.sourceMapper; + gIntervalMicros = options.intervalMicros; + gV8ProfilerStuckEventLoopDetected = 0; + gProfiler.start(); + // If contexts are enabled without using CPED, set an initial empty context + if (options.withContexts && !options.useCPED) { + setContext({}); + } +} +function stop(restart = false, generateLabels, lowCardinalityLabels) { + if (!gProfiler) { + throw new Error('Wall profiler is not started'); + } + const profile = gProfiler.stop(restart); + if (restart) { + gV8ProfilerStuckEventLoopDetected = + gProfiler.v8ProfilerStuckEventLoopDetected(); + // Workaround for v8 bug, where profiler event processor thread is stuck in + // a loop eating 100% CPU, leading to empty profiles. + // Fully stop and restart the profiler to reset the profile to a valid state. + if (gV8ProfilerStuckEventLoopDetected > 0) { + gProfiler.stop(false); + gProfiler.start(); + } + } + else { + gV8ProfilerStuckEventLoopDetected = 0; + } + const serializedProfile = (0, profile_serializer_1.serializeTimeProfile)(profile, gIntervalMicros, gSourceMapper, true, generateLabels, lowCardinalityLabels); + if (!restart) { + gProfiler.dispose(); + gProfiler = undefined; + gSourceMapper = undefined; + } + return serializedProfile; +} +function getState() { + if (!gProfiler) { + throw new Error('Wall profiler is not started'); + } + return gProfiler.state; +} +function setContext(context) { + if (!gProfiler) { + throw new Error('Wall profiler is not started'); + } + gProfiler.context = context; +} +function getContext() { + if (!gProfiler) { + throw new Error('Wall profiler is not started'); + } + return gProfiler.context; +} +function getMetrics() { + if (!gProfiler) { + throw new Error('Wall profiler is not started'); + } + return gProfiler.metrics; +} +function isStarted() { + return !!gProfiler; +} +// Return 0 if no issue detected, 1 if possible issue, 2 if issue detected for certain +function v8ProfilerStuckEventLoopDetected() { + return gV8ProfilerStuckEventLoopDetected; +} +exports.constants = { + kSampleCount, + GARBAGE_COLLECTION_FUNCTION_NAME: profile_serializer_1.GARBAGE_COLLECTION_FUNCTION_NAME, + NON_JS_THREADS_FUNCTION_NAME: profile_serializer_1.NON_JS_THREADS_FUNCTION_NAME, +}; +//# sourceMappingURL=time-profiler.js.map \ No newline at end of file diff --git a/out/src/time-profiler.js.map b/out/src/time-profiler.js.map new file mode 100644 index 00000000..4263ac75 --- /dev/null +++ b/out/src/time-profiler.js.map @@ -0,0 +1 @@ +{"version":3,"file":"time-profiler.js","sourceRoot":"","sources":["../../ts/src/time-profiler.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/out/src/v8-types.d.ts b/out/src/v8-types.d.ts new file mode 100644 index 00000000..16c92bff --- /dev/null +++ b/out/src/v8-types.d.ts @@ -0,0 +1,69 @@ +/** + * Copyright 2017 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +export interface TimeProfile { + /** Time in nanoseconds at which profile was stopped. */ + endTime: number; + topDownRoot: TimeProfileNode; + /** Time in nanoseconds at which profile was started. */ + startTime: number; + hasCpuTime?: boolean; + /** CPU time of non-JS threads, only reported for the main worker thread */ + nonJSThreadsCpuTime?: number; +} +export interface ProfileNode { + name?: string; + scriptName: string; + scriptId?: number; + lineNumber?: number; + columnNumber?: number; + children: ProfileNode[]; +} +export interface TimeProfileNodeContext { + context?: object; + timestamp: bigint; + cpuTime?: number; + asyncId?: number; +} +export interface TimeProfileNode extends ProfileNode { + hitCount: number; + contexts?: TimeProfileNodeContext[]; +} +export interface AllocationProfileNode extends ProfileNode { + allocations: Allocation[]; +} +export interface Allocation { + sizeBytes: number; + count: number; +} +export interface LabelSet { + [key: string]: string | number; +} +export interface GenerateAllocationLabelsFunction { + ({ node }: { + node: AllocationProfileNode; + }): LabelSet; +} +export interface GenerateTimeLabelsArgs { + node: TimeProfileNode; + context?: TimeProfileNodeContext; +} +export interface GenerateTimeLabelsFunction { + (args: GenerateTimeLabelsArgs): LabelSet; +} +export interface TimeProfilerMetrics { + usedAsyncContextCount: number; + totalAsyncContextCount: number; +} diff --git a/out/src/v8-types.js b/out/src/v8-types.js new file mode 100644 index 00000000..6c8ddfba --- /dev/null +++ b/out/src/v8-types.js @@ -0,0 +1,18 @@ +"use strict"; +/** + * Copyright 2017 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +Object.defineProperty(exports, "__esModule", { value: true }); +//# sourceMappingURL=v8-types.js.map \ No newline at end of file diff --git a/out/src/v8-types.js.map b/out/src/v8-types.js.map new file mode 100644 index 00000000..6c8239e3 --- /dev/null +++ b/out/src/v8-types.js.map @@ -0,0 +1 @@ +{"version":3,"file":"v8-types.js","sourceRoot":"","sources":["../../ts/src/v8-types.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;;GAcG"} \ No newline at end of file diff --git a/package.json b/package.json index a6cc6fd6..f62812d5 100644 --- a/package.json +++ b/package.json @@ -9,26 +9,28 @@ "main": "out/src/index.js", "types": "out/src/index.d.ts", "scripts": { - "build:asan": "node-gyp configure build --jobs=max --address_sanitizer", - "build:tsan": "node-gyp configure build --jobs=max --thread_sanitizer", - "build": "node-gyp configure build --jobs=max", + "build:asan": "node ./scripts/node-gyp-parallel.js configure build --address_sanitizer", + "build:tsan": "node ./scripts/node-gyp-parallel.js configure build --thread_sanitizer", + "build": "node ./scripts/node-gyp-parallel.js configure build", + "bun:smoke": "npm run compile && bun ./scripts/bun-smoke.mjs", + "bun:smoke:pack": "npm run compile && bun ./scripts/bun-smoke-pack.mjs", "codecov": "nyc report --reporter=json && codecov -f coverage/*.json", "compile": "tsc -p .", "fix": "gts fix", "format": "clang-format --style file -i --glob='bindings/**/*.{h,hh,cpp,cc}'", - "install": "exit 0", + "install": "node ./scripts/install.js", "lint": "jsgl --local . && gts check && clang-format --style file -n -Werror --glob='bindings/**/*.{h,hh,cpp,cc}'", "prepare": "npm run compile && npm run rebuild", "pretest:js-asan": "npm run compile && npm run build:asan", "pretest:js-tsan": "npm run compile && npm run build:tsan", "pretest:js-valgrind": "npm run pretest", "pretest": "npm run compile", - "rebuild": "node-gyp rebuild --jobs=max", + "rebuild": "node ./scripts/node-gyp-parallel.js rebuild", "test:cpp": "node scripts/cctest.js", "test:js-asan": "LSAN_OPTIONS='suppressions=./suppressions/lsan_suppr.txt' LD_PRELOAD=`gcc -print-file-name=libasan.so` mocha out/test/test-*.js", "test:js-tsan": "LD_PRELOAD=`gcc -print-file-name=libtsan.so` mocha out/test/test-*.js", "test:js-valgrind": "valgrind --leak-check=full mocha out/test/test-*.js", - "test:js": "nyc mocha -r source-map-support/register out/test/test-*.js", + "test:js": "node -r ./scripts/nyc-os-cpus-compat.js ./node_modules/nyc/bin/nyc.js mocha -r source-map-support/register out/test/test-*.js", "test": "npm run test:js" }, "author": { @@ -70,12 +72,11 @@ "out/src", "out/third_party/cloud-debug-nodejs", "proto", + "build/Release/*.node", "package-lock.json", "package.json", "README.md", - "scripts/preinstall.js", - "scripts/require-package-json.js", - "scripts/should_rebuild.js", + "scripts/install.js", "prebuilds" ], "nyc": { diff --git a/scripts/bun-smoke-pack.mjs b/scripts/bun-smoke-pack.mjs new file mode 100644 index 00000000..7f86b7d5 --- /dev/null +++ b/scripts/bun-smoke-pack.mjs @@ -0,0 +1,71 @@ +#!/usr/bin/env bun + +import {mkdtempSync, rmSync, unlinkSync, writeFileSync} from 'node:fs'; +import os from 'node:os'; +import path from 'node:path'; +import {spawnSync} from 'node:child_process'; +import {fileURLToPath, pathToFileURL} from 'node:url'; + +const scriptDir = path.dirname(fileURLToPath(import.meta.url)); +const repoRoot = path.resolve(scriptDir, '..'); +const tmpDir = mkdtempSync(path.join(os.tmpdir(), 'pprof-bun-pack-')); +const smokeScriptPath = path.join(tmpDir, 'bun-smoke-pack-check.mjs'); +const smokeRunnerPath = path.join(scriptDir, 'bun-smoke-runner.mjs'); +const npmCacheDir = path.join(tmpDir, 'npm-cache'); +const baseEnv = { + ...process.env, + NPM_CONFIG_CACHE: process.env.NPM_CONFIG_CACHE ?? npmCacheDir, +}; + +try { + const pack = spawnSync('npm', ['pack', '--silent'], { + cwd: repoRoot, + encoding: 'utf8', + env: baseEnv, + stdio: ['ignore', 'pipe', 'inherit'], + }); + if (pack.status !== 0) { + process.exit(pack.status ?? 1); + } + + const tgzName = pack.stdout.trim().split('\n').at(-1); + if (!tgzName) { + throw new Error('npm pack did not produce a tarball name'); + } + const tgzPath = path.join(repoRoot, tgzName); + + spawnOrThrow('npm', ['init', '-y'], tmpDir, baseEnv); + spawnOrThrow('npm', ['install', tgzPath], tmpDir, baseEnv); + + writeFileSync( + smokeScriptPath, + [ + "import * as pprof from '@datadog/pprof';", + `import {runSmoke} from '${pathToFileUrl(smokeRunnerPath)}';`, + "console.log(JSON.stringify(await runSmoke(pprof)));", + '', + ].join('\n') + ); + + spawnOrThrow('bun', [smokeScriptPath], tmpDir, process.env); + + unlinkSync(tgzPath); +} finally { + rmSync(smokeScriptPath, {force: true}); + rmSync(tmpDir, {recursive: true, force: true}); +} + +function spawnOrThrow(command, args, cwd, env) { + const result = spawnSync(command, args, { + cwd, + stdio: 'inherit', + env, + }); + if (result.status !== 0) { + throw new Error(`${command} ${args.join(' ')} failed with status ${result.status ?? 'null'}`); + } +} + +function pathToFileUrl(filePath) { + return pathToFileURL(filePath).href; +} diff --git a/scripts/bun-smoke-runner.mjs b/scripts/bun-smoke-runner.mjs new file mode 100644 index 00000000..8077f968 --- /dev/null +++ b/scripts/bun-smoke-runner.mjs @@ -0,0 +1,39 @@ +import assert from 'node:assert/strict'; +import {setTimeout as delay} from 'node:timers/promises'; + +export async function runSmoke(pprof) { + assert.equal(typeof pprof.time.start, 'function'); + assert.equal(typeof pprof.time.stop, 'function'); + assert.equal(typeof pprof.heap.start, 'function'); + assert.equal(typeof pprof.heap.profile, 'function'); + assert.equal(typeof pprof.encode, 'function'); + + pprof.time.start({ + durationMillis: 25, + intervalMicros: 1_000, + withContexts: true, + }); + pprof.time.setContext({runtime: 'bun-smoke'}); + await delay(25); + const wallProfile = pprof.time.stop(); + assert.ok(Array.isArray(wallProfile.sample)); + assert.ok(wallProfile.sample.length > 0); + + const encodedWallProfile = await pprof.encode(wallProfile); + assert.ok(encodedWallProfile.length > 0); + + pprof.heap.start(128 * 1024, 64); + pprof.heap.start(128 * 1024, 64); + const heapProfile = pprof.heap.profile(); + pprof.heap.stop(); + assert.ok(heapProfile.sample.length > 0); + + const encodedHeapProfile = await pprof.encode(heapProfile); + assert.ok(encodedHeapProfile.length > 0); + + return { + ok: true, + wallBytes: encodedWallProfile.length, + heapBytes: encodedHeapProfile.length, + }; +} diff --git a/scripts/bun-smoke.mjs b/scripts/bun-smoke.mjs new file mode 100644 index 00000000..f478c31e --- /dev/null +++ b/scripts/bun-smoke.mjs @@ -0,0 +1,6 @@ +#!/usr/bin/env bun + +import * as pprof from '../out/src/index.js'; +import {runSmoke} from './bun-smoke-runner.mjs'; + +console.log(JSON.stringify(await runSmoke(pprof))); diff --git a/scripts/install.js b/scripts/install.js new file mode 100644 index 00000000..61d8a08e --- /dev/null +++ b/scripts/install.js @@ -0,0 +1,41 @@ +#!/usr/bin/env node + +/** + * Copyright 2026 Datadog + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + */ + +const {spawnSync} = require('child_process'); +const {existsSync} = require('fs'); +const {join} = require('path'); + +const packageRoot = join(__dirname, '..'); +const sourceEntry = join(packageRoot, 'ts', 'src', 'index.ts'); +const compiledEntry = join(packageRoot, 'out', 'src', 'index.js'); +const compiledTypes = join(packageRoot, 'out', 'src', 'index.d.ts'); + +function runNpmScript(scriptName) { + const npmCmd = process.platform === 'win32' ? 'npm.cmd' : 'npm'; + const result = spawnSync(npmCmd, ['run', scriptName], { + cwd: packageRoot, + stdio: 'inherit', + env: process.env, + }); + if (result.status !== 0) { + process.exit(result.status ?? 1); + } +} + +// For source installs (e.g. git dependencies), out/src can be absent. +// Compile once so downstream packages can resolve JS + type artifacts. +if ( + existsSync(sourceEntry) && + (!existsSync(compiledEntry) || !existsSync(compiledTypes)) +) { + runNpmScript('compile'); +} diff --git a/scripts/node-gyp-parallel.js b/scripts/node-gyp-parallel.js new file mode 100644 index 00000000..0a2f533c --- /dev/null +++ b/scripts/node-gyp-parallel.js @@ -0,0 +1,25 @@ +'use strict' + +const { cpus, availableParallelism } = require('os') +const { spawnSync } = require('child_process') + +function detectParallelism () { + if (typeof availableParallelism === 'function') { + return availableParallelism() + } + + const cores = cpus() + return Array.isArray(cores) && cores.length > 0 ? cores.length : 1 +} + +const command = process.platform === 'win32' ? 'node-gyp.cmd' : 'node-gyp' +const jobs = Math.max(detectParallelism(), 1) +const args = [...process.argv.slice(2), '--jobs', String(jobs)] + +const child = spawnSync(command, args, { stdio: 'inherit' }) + +if (child.error) { + throw child.error +} + +process.exit(child.status ?? 1) diff --git a/scripts/nyc-os-cpus-compat.js b/scripts/nyc-os-cpus-compat.js new file mode 100644 index 00000000..aad2e7da --- /dev/null +++ b/scripts/nyc-os-cpus-compat.js @@ -0,0 +1,36 @@ +'use strict'; + +const os = require('os'); + +const originalCpus = os.cpus.bind(os); + +/** + * Some environments report an empty CPU list, which makes nyc derive a + * zero concurrency and crash. Keep Node's original result when present and + * synthesize a minimum-safe fallback only for the empty-list case. + */ +os.cpus = function patchedCpus() { + const cpus = originalCpus(); + if (Array.isArray(cpus) && cpus.length > 0) { + return cpus; + } + + const fallbackCount = Math.max( + typeof os.availableParallelism === 'function' + ? os.availableParallelism() + : 1, + 1 + ); + + return Array.from({length: fallbackCount}, () => ({ + model: 'unknown', + speed: 0, + times: { + user: 0, + nice: 0, + sys: 0, + idle: 0, + irq: 0, + }, + })); +}; diff --git a/ts/src/bun-native-backend.ts b/ts/src/bun-native-backend.ts new file mode 100644 index 00000000..a8343db2 --- /dev/null +++ b/ts/src/bun-native-backend.ts @@ -0,0 +1,440 @@ +/** + * Copyright 2026 Datadog + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + */ + +import { + AllocationProfileNode, + TimeProfile, + TimeProfileNodeContext, + TimeProfilerMetrics, +} from './v8-types'; + +const MICROS_PER_MILLI = 1000; +const NANOS_PER_MICRO = 1000; +const EPOCH_ORIGIN_MICROS = BigInt(Date.now()) * BigInt(MICROS_PER_MILLI); +const HRTIME_ORIGIN_NANOS = process.hrtime.bigint(); + +function nowMicros(): number { + return Number(process.hrtime.bigint() / 1000n); +} + +function nowMicrosBigInt(): bigint { + return ( + EPOCH_ORIGIN_MICROS + + (process.hrtime.bigint() - HRTIME_ORIGIN_NANOS) / BigInt(NANOS_PER_MICRO) + ); +} + +type TimeProfilerCtorArgs = { + withContexts?: boolean; + intervalMicros?: number; + collectCpuTime?: boolean; +}; + +type ContextTimelineEntry = TimeProfileNodeContext & { + signature: string; +}; + +function cloneContext(context: object | undefined): object | undefined { + if (!context) { + return context; + } + return {...(context as Record)}; +} + +function cpuUsageDeltaNanos( + current: NodeJS.CpuUsage, + previous: NodeJS.CpuUsage +): number { + const userMicros = Math.max(current.user - previous.user, 0); + const systemMicros = Math.max(current.system - previous.system, 0); + return (userMicros + systemMicros) * NANOS_PER_MICRO; +} + +function stableContextValue( + value: unknown, + seen?: WeakSet +): string { + switch (typeof value) { + case 'string': + return `s:${value}`; + case 'number': + return `n:${value}`; + case 'bigint': + return `bi:${value.toString(10)}`; + case 'boolean': + return `b:${value ? 1 : 0}`; + case 'undefined': + return 'u:'; + case 'symbol': + return `sy:${String(value)}`; + case 'function': + return `fn:${(value as Function).name ?? ''}`; + case 'object': + if (value === null) { + return 'null:'; + } + + if (Array.isArray(value)) { + const nextSeen = seen ?? new WeakSet(); + if (nextSeen.has(value)) { + return 'a:[circular]'; + } + nextSeen.add(value); + const encodedItems = value.map(item => + stableContextValue(item, nextSeen) + ); + nextSeen.delete(value); + return `a:[${encodedItems.join(',')}]`; + } + + const objectValue = value as Record; + const nextSeen = seen ?? new WeakSet(); + if (nextSeen.has(objectValue)) { + return 'o:[circular]'; + } + nextSeen.add(objectValue); + + const keys = Object.keys(objectValue).sort(); + let encoded = 'o:{'; + for (const key of keys) { + encoded += `${key}:${stableContextValue(objectValue[key], nextSeen)};`; + } + encoded += '}'; + + nextSeen.delete(objectValue); + return encoded; + default: + return `${typeof value}:`; + } +} + +function contextSignature(context: object | undefined): string { + if (typeof context === 'undefined') { + return '__undefined__'; + } + if (context === null) { + return '__null__'; + } + const contextObject = context as Record; + const contextKeys = Object.keys(contextObject).sort(); + let signature = ''; + for (const key of contextKeys) { + signature += `${key}\u0000${stableContextValue(contextObject[key])}\u0000`; + } + return signature; +} + +export class BunTimeProfiler { + public metrics: TimeProfilerMetrics = { + usedAsyncContextCount: 0, + totalAsyncContextCount: 0, + }; + public state: {[key: string]: number} = {sampleCount: 0}; + + private readonly withContexts: boolean; + private readonly intervalMicros: number; + private readonly collectCpuTime: boolean; + private started = false; + private startTime = 0; + private contextTimeline: ContextTimelineEntry[] = []; + private currentContext: object | undefined; + private currentContextSignature = contextSignature(undefined); + private lastRecordedContextSignature = contextSignature(undefined); + private lastContextCpuUsage: NodeJS.CpuUsage | undefined; + + constructor(...args: unknown[]) { + const options = + (args[0] as TimeProfilerCtorArgs | undefined) ?? + ({} as TimeProfilerCtorArgs); + this.withContexts = options.withContexts === true; + this.intervalMicros = Math.max(options.intervalMicros ?? 1000, 1); + this.collectCpuTime = options.collectCpuTime === true; + } + + get context(): object | undefined { + return this.currentContext; + } + + set context(context: object | undefined) { + const nextContext = cloneContext(context); + this.currentContext = nextContext; + this.currentContextSignature = contextSignature(nextContext); + if (this.started && this.withContexts) { + if (this.lastRecordedContextSignature === this.currentContextSignature) { + return; + } + this.recordContext(nextContext, this.currentContextSignature); + } + } + + private recordContext(context: object | undefined, signature: string) { + const nextContext: ContextTimelineEntry = { + context, + timestamp: nowMicrosBigInt(), + signature, + }; + if (this.collectCpuTime) { + const currentCpuUsage = process.cpuUsage(); + if (this.lastContextCpuUsage) { + nextContext.cpuTime = cpuUsageDeltaNanos( + currentCpuUsage, + this.lastContextCpuUsage + ); + } else { + nextContext.cpuTime = 0; + } + this.lastContextCpuUsage = currentCpuUsage; + } + this.contextTimeline.push(nextContext); + this.lastRecordedContextSignature = signature; + } + + private normalizedContextTimeline( + endTimestampMicros: bigint + ): TimeProfileNodeContext[] | undefined { + if (!this.withContexts || this.contextTimeline.length === 0) { + return undefined; + } + + const minSampleWindow = BigInt(this.intervalMicros); + const normalized: TimeProfileNodeContext[] = []; + let lastNormalizedSignature: string | undefined; + let pendingCpuTime = 0; + + for (let i = 0; i < this.contextTimeline.length; i++) { + const current = this.contextTimeline[i]; + const nextTimestamp = + i + 1 < this.contextTimeline.length + ? this.contextTimeline[i + 1].timestamp + : endTimestampMicros; + const durationMicros = + nextTimestamp > current.timestamp + ? nextTimestamp - current.timestamp + : 0n; + + // Ignore ultra-short context flips that are below one sampling period. + if (durationMicros < minSampleWindow) { + if (typeof current.cpuTime === 'number') { + pendingCpuTime += current.cpuTime; + } + continue; + } + + const normalizedCpuTime = + pendingCpuTime + (typeof current.cpuTime === 'number' ? current.cpuTime : 0); + pendingCpuTime = 0; + + const last = normalized[normalized.length - 1]; + if (last && lastNormalizedSignature === current.signature) { + if (normalizedCpuTime > 0) { + last.cpuTime = (last.cpuTime ?? 0) + normalizedCpuTime; + } + continue; + } + + normalized.push({ + context: current.context, + timestamp: current.timestamp, + cpuTime: + normalizedCpuTime > 0 + ? normalizedCpuTime + : typeof current.cpuTime === 'number' + ? current.cpuTime + : undefined, + }); + lastNormalizedSignature = current.signature; + } + + if (normalized.length > 0) { + return normalized; + } + + const lastRawContext = this.contextTimeline[this.contextTimeline.length - 1]; + const fallbackCpuTime = + pendingCpuTime > 0 + ? pendingCpuTime + : typeof lastRawContext.cpuTime === 'number' + ? lastRawContext.cpuTime + : undefined; + return [ + { + context: lastRawContext.context, + timestamp: lastRawContext.timestamp, + cpuTime: fallbackCpuTime, + }, + ]; + } + + start() { + this.started = true; + this.startTime = nowMicros(); + this.state.sampleCount = 0; + this.contextTimeline = []; + this.lastRecordedContextSignature = contextSignature(undefined); + this.lastContextCpuUsage = this.collectCpuTime + ? process.cpuUsage() + : undefined; + if (this.withContexts && this.currentContext) { + this.recordContext(this.currentContext, this.currentContextSignature); + } + } + + stop(restart: boolean): TimeProfile { + if (!this.started) { + throw new Error('Wall profiler is not started'); + } + + const windowStartTime = this.startTime; + const endTime = nowMicros(); + const elapsedMicros = Math.max(endTime - windowStartTime, 1); + const estimatedSamples = Math.max( + Math.floor(elapsedMicros / this.intervalMicros), + 1 + ); + this.state.sampleCount = estimatedSamples; + + const stopTimestampMicros = nowMicrosBigInt(); + const context = this.normalizedContextTimeline(stopTimestampMicros); + + if (this.collectCpuTime) { + const currentCpuUsage = process.cpuUsage(); + if (context && context.length > 0 && this.lastContextCpuUsage) { + const lastContext = context[context.length - 1]; + const cpuTime = cpuUsageDeltaNanos( + currentCpuUsage, + this.lastContextCpuUsage + ); + lastContext.cpuTime = (lastContext.cpuTime ?? 0) + cpuTime; + } + this.lastContextCpuUsage = currentCpuUsage; + } + + const timeNode = { + name: 'Bun runtime', + scriptName: '', + scriptId: 0, + lineNumber: 0, + columnNumber: 0, + hitCount: estimatedSamples, + contexts: context, + children: [], + }; + + const result = { + startTime: windowStartTime, + endTime, + hasCpuTime: this.collectCpuTime, + topDownRoot: { + name: '(root)', + scriptName: '', + scriptId: 0, + lineNumber: 0, + columnNumber: 0, + hitCount: 0, + children: [timeNode], + }, + }; + + if (restart) { + this.startTime = endTime; + this.state.sampleCount = 0; + this.contextTimeline = []; + this.lastRecordedContextSignature = contextSignature(undefined); + this.lastContextCpuUsage = this.collectCpuTime + ? process.cpuUsage() + : undefined; + if (this.withContexts && this.currentContext) { + this.recordContext(this.currentContext, this.currentContextSignature); + } + } + + return result; + } + + dispose() { + this.started = false; + } + + v8ProfilerStuckEventLoopDetected() { + return 0; + } +} + +export function bunGetNativeThreadId() { + try { + // Keep worker identities distinct when worker_threads is available. + // Use a process-scoped numeric space to avoid collisions with threadId=0 + // on the main thread. + // eslint-disable-next-line @typescript-eslint/no-var-requires + const {threadId} = require('worker_threads') as {threadId?: unknown}; + if (typeof threadId === 'number' && Number.isInteger(threadId)) { + return process.pid * 1000 + threadId; + } + } catch { + // Ignore resolution failures and fall back to pid-only identity. + } + + return process.pid * 1000; +} + +let heapSamplingEnabled = false; + +export function bunStartSamplingHeapProfiler() { + heapSamplingEnabled = true; +} + +export function bunStopSamplingHeapProfiler() { + heapSamplingEnabled = false; +} + +export function bunGetAllocationProfile(): AllocationProfileNode { + if (!heapSamplingEnabled) { + throw new Error('Heap profiler is not enabled.'); + } + + const usage = process.memoryUsage(); + const heapNode: AllocationProfileNode = { + name: 'Bun heap', + scriptName: '', + scriptId: 0, + lineNumber: 0, + columnNumber: 0, + allocations: [ + { + sizeBytes: Math.max(usage.heapUsed, 1), + count: 1, + }, + ], + children: [], + }; + + return { + name: '(root)', + scriptName: '', + scriptId: 0, + lineNumber: 0, + columnNumber: 0, + allocations: [], + children: [heapNode], + }; +} + +export function bunMonitorOutOfMemory( + _heapLimitExtensionSize: number, + _maxHeapLimitExtensionCount: number, + _dumpHeapProfileOnSdterr: boolean, + _exportCommand: Array | undefined, + _callback: unknown, + _callbackMode: number, + _isMainThread: boolean +) { + // Bun does not expose a near-heap-limit callback hook equivalent to V8's native API. + // Keep this as a no-op to avoid emitting synthetic OOM events or spurious profiles. +} diff --git a/ts/src/heap-profiler-bindings.ts b/ts/src/heap-profiler-bindings.ts index 77522577..a9a2389c 100644 --- a/ts/src/heap-profiler-bindings.ts +++ b/ts/src/heap-profiler-bindings.ts @@ -13,13 +13,10 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - -import * as path from 'path'; - import {AllocationProfileNode} from './v8-types'; +import {loadNativeModule} from './native-backend-loader'; -const findBinding = require('node-gyp-build'); -const profiler = findBinding(path.join(__dirname, '..', '..')); +const profiler = loadNativeModule(); // Wrappers around native heap profiler functions. @@ -38,7 +35,7 @@ export function stopSamplingHeapProfiler() { } export function getAllocationProfile(): AllocationProfileNode { - return profiler.heapProfiler.getAllocationProfile(); + return profiler.heapProfiler.getAllocationProfile() as AllocationProfileNode; } export type NearHeapLimitCallback = (profile: AllocationProfileNode) => void; diff --git a/ts/src/heap-profiler.ts b/ts/src/heap-profiler.ts index b6c64d0f..1ef9f442 100644 --- a/ts/src/heap-profiler.ts +++ b/ts/src/heap-profiler.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import {Profile} from 'pprof-format'; +import type {Profile} from 'pprof-format'; import { getAllocationProfile, @@ -29,11 +29,33 @@ import { GenerateAllocationLabelsFunction, } from './v8-types'; import {isMainThread} from 'worker_threads'; +import {runtime} from './runtime'; let enabled = false; let heapIntervalBytes = 0; let heapStackDepth = 0; +function hasEquivalentHeapSamplingConfig( + intervalBytes: number, + stackDepth: number +): boolean { + if ( + intervalBytes === heapIntervalBytes && + stackDepth === heapStackDepth + ) { + return true; + } + + if (runtime !== 'bun') { + return false; + } + + return ( + Number(intervalBytes) === Number(heapIntervalBytes) && + Number(stackDepth) === Number(heapStackDepth) + ); +} + /* * Collects a heap profile when heapProfiler is enabled. Otherwise throws * an error. @@ -108,8 +130,11 @@ export function convertProfile( */ export function start(intervalBytes: number, stackDepth: number) { if (enabled) { + if (hasEquivalentHeapSamplingConfig(intervalBytes, stackDepth)) { + return; + } throw new Error( - `Heap profiler is already started with intervalBytes ${heapIntervalBytes} and stackDepth ${stackDepth}` + `Heap profiler is already started with intervalBytes ${heapIntervalBytes} and stackDepth ${heapStackDepth}` ); } heapIntervalBytes = intervalBytes; diff --git a/ts/src/native-backend-loader.ts b/ts/src/native-backend-loader.ts new file mode 100644 index 00000000..1525f396 --- /dev/null +++ b/ts/src/native-backend-loader.ts @@ -0,0 +1,146 @@ +/** + * Copyright 2026 Datadog + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + */ + +import {existsSync, readdirSync} from 'fs'; +import {join} from 'path'; + +import {runtime} from './runtime'; +import { + BunTimeProfiler, + bunGetAllocationProfile, + bunGetNativeThreadId, + bunMonitorOutOfMemory, + bunStartSamplingHeapProfiler, + bunStopSamplingHeapProfiler, +} from './bun-native-backend'; +import {TimeProfile, TimeProfilerMetrics} from './v8-types'; + +type NativeModule = { + TimeProfiler: new (...args: unknown[]) => TimeProfilerInstance; + constants: {kSampleCount: string}; + getNativeThreadId: () => number; + heapProfiler: { + startSamplingHeapProfiler: ( + heapIntervalBytes: number, + heapStackDepth: number + ) => void; + stopSamplingHeapProfiler: () => void; + getAllocationProfile: () => unknown; + monitorOutOfMemory: ( + heapLimitExtensionSize: number, + maxHeapLimitExtensionCount: number, + dumpHeapProfileOnSdterr: boolean, + exportCommand: Array | undefined, + callback: unknown, + callbackMode: number, + isMainThread: boolean + ) => void; + }; +}; + +type TimeProfilerInstance = { + start: () => void; + stop: (restart: boolean) => TimeProfile; + dispose: () => void; + v8ProfilerStuckEventLoopDetected: () => number; + context: object | undefined; + metrics: TimeProfilerMetrics; + state: {[key: string]: number}; +}; + +const bunModule: NativeModule = { + TimeProfiler: BunTimeProfiler, + constants: {kSampleCount: 'sampleCount'}, + getNativeThreadId: bunGetNativeThreadId, + heapProfiler: { + startSamplingHeapProfiler: bunStartSamplingHeapProfiler, + stopSamplingHeapProfiler: bunStopSamplingHeapProfiler, + getAllocationProfile: bunGetAllocationProfile, + monitorOutOfMemory: bunMonitorOutOfMemory, + }, +}; + +let cachedModule: NativeModule | undefined; + +export function loadNativeModule(): NativeModule { + if (cachedModule) { + return cachedModule; + } + + if (runtime === 'bun') { + cachedModule = bunModule; + return cachedModule; + } + + const rootDir = join(__dirname, '..', '..'); + + // eslint-disable-next-line @typescript-eslint/no-var-requires + const findBinding = require('node-gyp-build') as ( + rootPath: string + ) => NativeModule; + cachedModule = loadNodeNativeModule(rootDir, findBinding, require); + return cachedModule; +} + +function loadNodeNativeModule( + rootDir: string, + findBinding: (rootPath: string) => NativeModule, + nodeRequire: (modulePath: string) => unknown +): NativeModule { + try { + return findBinding(rootDir); + } catch (error) { + if ( + !isMissingNativeBuildError(error) || + !hasLocalNativeBuildArtifacts(rootDir) + ) { + throw error; + } + + return loadFromBuildRelease(rootDir, nodeRequire); + } +} + +function isMissingNativeBuildError(error: unknown): boolean { + return ( + error instanceof Error && + error.message.includes('No native build was found for runtime=') + ); +} + +function hasLocalNativeBuildArtifacts(rootDir: string): boolean { + return existsSync(join(rootDir, 'build', 'Release')); +} + +function loadFromBuildRelease( + rootDir: string, + nodeRequire: (modulePath: string) => unknown +): NativeModule { + const releaseDir = join(rootDir, 'build', 'Release'); + const preferredPath = join(releaseDir, 'dd_pprof.node'); + if (existsSync(preferredPath)) { + return nodeRequire(preferredPath) as NativeModule; + } + + const candidates = readdirSync(releaseDir) + .filter(name => name.endsWith('.node')) + .sort(); + if (candidates.length === 0) { + throw new Error( + `No native .node artifact found under ${releaseDir} after fallback` + ); + } + + return nodeRequire(join(releaseDir, candidates[0])) as NativeModule; +} + +export const __testing = { + loadNodeNativeModule, +}; diff --git a/ts/src/pprof-format-loader.ts b/ts/src/pprof-format-loader.ts new file mode 100644 index 00000000..2c783df0 --- /dev/null +++ b/ts/src/pprof-format-loader.ts @@ -0,0 +1,45 @@ +/** + * Copyright 2026 Datadog + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + */ + +import {join} from 'path'; + +import type * as PprofFormat from 'pprof-format'; + +import {runtime} from './runtime'; + +let cachedModule: typeof PprofFormat | undefined; + +function loadFromPackageRoot(): typeof PprofFormat { + const packageJsonPath = require.resolve('pprof-format/package.json'); + const packageRoot = packageJsonPath.slice( + 0, + packageJsonPath.length - 'package.json'.length + ); + return require(join(packageRoot, 'dist/commonjs/index.js')); +} + +export function loadPprofFormat(): typeof PprofFormat { + if (cachedModule) { + return cachedModule; + } + + try { + const loaded = require('pprof-format') as typeof PprofFormat; + cachedModule = loaded; + return loaded; + } catch (error) { + if (runtime !== 'bun') { + throw error; + } + } + + cachedModule = loadFromPackageRoot(); + return cachedModule; +} diff --git a/ts/src/profile-encoder.ts b/ts/src/profile-encoder.ts index 64df94b4..4e9a44eb 100644 --- a/ts/src/profile-encoder.ts +++ b/ts/src/profile-encoder.ts @@ -17,7 +17,7 @@ import {promisify} from 'util'; import {gzip, gzipSync} from 'zlib'; -import {Profile} from 'pprof-format'; +import type {Profile} from 'pprof-format'; const gzipPromise = promisify(gzip); diff --git a/ts/src/profile-serializer.ts b/ts/src/profile-serializer.ts index 802bc968..dc7d01e5 100644 --- a/ts/src/profile-serializer.ts +++ b/ts/src/profile-serializer.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { +import type { Function, Label, LabelInput, @@ -26,6 +26,7 @@ import { StringTable, ProfileInput, } from 'pprof-format'; +import {loadPprofFormat} from './pprof-format-loader'; import { GeneratedLocation, SourceLocation, @@ -40,6 +41,18 @@ import { TimeProfileNode, } from './v8-types'; +const pprofFormat = loadPprofFormat(); +const { + Function: PprofFunction, + Label: PprofLabel, + Line: PprofLine, + Location: PprofLocation, + Profile: PprofProfile, + Sample: PprofSample, + StringTable: PprofStringTable, + ValueType: PprofValueType, +} = pprofFormat; + export const NON_JS_THREADS_FUNCTION_NAME = 'Non JS threads activity'; export const GARBAGE_COLLECTION_FUNCTION_NAME = 'Garbage Collection'; @@ -156,13 +169,13 @@ function serialize( id = locations.length + 1; locationIdMap.set(keyStr, id); const line = getLine(profLoc, node.scriptId); - const location = new Location({id, line: [line]}); + const location = new PprofLocation({id, line: [line]}); locations.push(location); return location; } function getLine(loc: SourceLocation, scriptId?: number): Line { - return new Line({ + return new PprofLine({ functionId: getFunction(loc, scriptId).id, line: loc.line, }); @@ -192,7 +205,7 @@ function serialize( } } const nameId = stringTable.dedup(name); - const f = new Function({ + const f = new PprofFunction({ id, name: nameId, systemName: nameId, @@ -208,7 +221,7 @@ function serialize( * adds strings used in this value type to the table. */ function createSampleCountValueType(table: StringTable): ValueType { - return new ValueType({ + return new PprofValueType({ type: table.dedup('sample'), unit: table.dedup('count'), }); @@ -219,7 +232,7 @@ function createSampleCountValueType(table: StringTable): ValueType { * adds strings used in this value type to the table. */ function createTimeValueType(table: StringTable): ValueType { - return new ValueType({ + return new PprofValueType({ type: table.dedup('wall'), unit: table.dedup('nanoseconds'), }); @@ -230,7 +243,7 @@ function createTimeValueType(table: StringTable): ValueType { * adds strings used in this value type to the table. */ function createCpuValueType(table: StringTable): ValueType { - return new ValueType({ + return new PprofValueType({ type: table.dedup('cpu'), unit: table.dedup('nanoseconds'), }); @@ -241,7 +254,7 @@ function createCpuValueType(table: StringTable): ValueType { * adds strings used in this value type to the table. */ function createObjectCountValueType(table: StringTable): ValueType { - return new ValueType({ + return new PprofValueType({ type: table.dedup('objects'), unit: table.dedup('count'), }); @@ -252,7 +265,7 @@ function createObjectCountValueType(table: StringTable): ValueType { * adds strings used in this value type to the table. */ function createAllocationValueType(table: StringTable): ValueType { - return new ValueType({ + return new PprofValueType({ type: table.dedup('space'), unit: table.dedup('bytes'), }); @@ -377,7 +390,7 @@ export function serializeTimeProfile( } } const intervalNanos = intervalMicros * 1000; - const stringTable = new StringTable(); + const stringTable = new PprofStringTable(); const labelCaches: Map[] = []; for (const l of lowCardinalityLabels) { labelCaches[stringTable.dedup(l)] = new Map(); @@ -424,7 +437,7 @@ export function serializeTimeProfile( if (prof.hasCpuTime) { values.push(context.cpuTime ?? 0); } - const sample = new Sample({ + const sample = new PprofSample({ locationId: entry.stack, value: values, label: dedupLabels(labelsArr), @@ -444,7 +457,7 @@ export function serializeTimeProfile( if (prof.hasCpuTime) { values.push(unlabelledCpuTime); } - const sample = new Sample({ + const sample = new PprofSample({ locationId: entry.stack, value: values, label: buildLabels(labels, stringTable), @@ -481,7 +494,7 @@ export function serializeTimeProfile( sourceMapper ); - return new Profile(profile); + return new PprofProfile(profile); } function buildLabels(labelSet: object, stringTable: StringTable): Label[] { @@ -502,7 +515,7 @@ function buildLabels(labelSet: object, stringTable: StringTable): Label[] { default: continue; } - labels.push(new Label(labelInput)); + labels.push(new PprofLabel(labelInput)); } return labels; @@ -534,7 +547,7 @@ export function serializeHeapProfile( ? buildLabels(generateLabels({node: entry.node}), stringTable) : []; for (const alloc of entry.node.allocations) { - const sample = new Sample({ + const sample = new PprofSample({ locationId: entry.stack, value: [alloc.count, alloc.sizeBytes * alloc.count], label: labels, @@ -545,7 +558,7 @@ export function serializeHeapProfile( } }; - const stringTable = new StringTable(); + const stringTable = new PprofStringTable(); const sampleValueType = createObjectCountValueType(stringTable); const allocationValueType = createAllocationValueType(stringTable); @@ -565,5 +578,5 @@ export function serializeHeapProfile( sourceMapper ); - return new Profile(profile); + return new PprofProfile(profile); } diff --git a/ts/src/runtime.ts b/ts/src/runtime.ts new file mode 100644 index 00000000..51a150c3 --- /dev/null +++ b/ts/src/runtime.ts @@ -0,0 +1,70 @@ +/** + * Copyright 2026 Datadog + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + */ + +export type Runtime = 'node' | 'bun'; + +const RUNTIME_ENV_KEY = 'DATADOG_PPROF_RUNTIME'; + +function env(): NodeJS.ProcessEnv { + return process.env; +} + +type RuntimeDetectionInputs = { + envOverride: string | undefined; + bunVersion: string | undefined; + bunGlobal: unknown; +}; + +function detectRuntimeFromEnv(): Runtime | undefined { + const override = env()[RUNTIME_ENV_KEY]; + if (override === 'node' || override === 'bun') { + return override; + } + return undefined; +} + +function detectRuntime(): Runtime { + const envRuntime = detectRuntimeFromEnv(); + if (envRuntime) { + return envRuntime; + } + + return detectRuntimeFromInputs({ + envOverride: undefined, + bunVersion: process.versions.bun, + bunGlobal: (globalThis as {Bun?: unknown}).Bun, + }); +} + +function detectRuntimeFromInputs({ + envOverride, + bunVersion, + bunGlobal, +}: RuntimeDetectionInputs): Runtime { + if (envOverride === 'node' || envOverride === 'bun') { + return envOverride; + } + + if (typeof bunVersion === 'string') { + return 'bun'; + } + + if (typeof bunGlobal !== 'undefined') { + return 'bun'; + } + + return 'node'; +} + +export const __testing = { + detectRuntimeFromInputs, +}; + +export const runtime = detectRuntime(); diff --git a/ts/src/time-profiler-bindings.ts b/ts/src/time-profiler-bindings.ts index 61e66ed6..0a4fd43c 100644 --- a/ts/src/time-profiler-bindings.ts +++ b/ts/src/time-profiler-bindings.ts @@ -13,10 +13,9 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import {join} from 'path'; +import {loadNativeModule} from './native-backend-loader'; -const findBinding = require('node-gyp-build'); -const profiler = findBinding(join(__dirname, '..', '..')); +const profiler = loadNativeModule(); export const TimeProfiler = profiler.TimeProfiler; export const constants = profiler.constants; diff --git a/ts/test/cped-freelist-regression-child.ts b/ts/test/cped-freelist-regression-child.ts index 988a7a8d..55c11fa4 100644 --- a/ts/test/cped-freelist-regression-child.ts +++ b/ts/test/cped-freelist-regression-child.ts @@ -42,7 +42,8 @@ async function main() { time.start({ intervalMicros: 1000, - durationMillis: 10_000, + // Allow enough wall time for instrumented/coverage runs to finish the churn. + durationMillis: 60_000, withContexts: true, lineNumbers: false, useCPED: true, @@ -52,8 +53,8 @@ async function main() { const waveSize = 20_000; const maxWaves = 6; + const minWavesBeforeGc = 3; const minDelta = 5_000; - const minTotalBeforeGc = 40_000; const debug = process.env.DEBUG_CPED_TEST === '1'; const log = (...args: unknown[]) => { if (debug) { @@ -84,33 +85,45 @@ async function main() { } const baseline = time.getMetrics().totalAsyncContextCount; - let totalBeforeGc = baseline; - let wavesRun = 0; - while (wavesRun < maxWaves && totalBeforeGc < minTotalBeforeGc) { - await runWave(waveSize); - totalBeforeGc = time.getMetrics().totalAsyncContextCount; - wavesRun++; - log('wave', wavesRun, 'totalBeforeGc', totalBeforeGc); + + async function churnAndCollect(phase: string) { + let totalBeforeGc = time.getMetrics().totalAsyncContextCount; + let wavesRun = 0; + while ( + wavesRun < maxWaves && + (wavesRun < minWavesBeforeGc || totalBeforeGc - baseline < minDelta) + ) { + await runWave(waveSize); + totalBeforeGc = time.getMetrics().totalAsyncContextCount; + wavesRun++; + log(phase, 'wave', wavesRun, 'totalBeforeGc', totalBeforeGc); + } + + assert( + totalBeforeGc - baseline >= minDelta, + `test did not create enough async contexts (baseline=${baseline}, total=${totalBeforeGc})` + ); + + await gcAndYield(6); + const afterGc = time.getMetrics(); + log(phase, 'metricsAfterGc', afterGc); + + return { + beforeGc: totalBeforeGc, + afterGc: afterGc.totalAsyncContextCount, + }; } - const metricsBeforeGc = time.getMetrics(); - log('baseline', baseline, 'metricsBeforeGc', metricsBeforeGc); - assert( - totalBeforeGc - baseline >= minDelta, - `test did not create enough async contexts (baseline=${baseline}, total=${totalBeforeGc})` - ); - assert( - totalBeforeGc >= minTotalBeforeGc, - `test did not reach target async context count (total=${totalBeforeGc})` - ); - await gcAndYield(6); - const metricsAfterGc = time.getMetrics(); - const totalAfterGc = metricsAfterGc.totalAsyncContextCount; - log('metricsAfterGc', metricsAfterGc); - const maxAllowed = Math.floor(totalBeforeGc * 0.75); + const phase1 = await churnAndCollect('phase1'); + const phase2 = await churnAndCollect('phase2'); + + // The key regression signal is unbounded growth over repeated churn/GC cycles. + // Use a relative + floor allowance to tolerate runtime/coverage variability. + const growth = phase2.afterGc - phase1.afterGc; + const maxGrowth = Math.max(2_000, Math.floor(phase1.afterGc * 0.25)); assert( - totalAfterGc <= maxAllowed, - `expected trimming; before=${totalBeforeGc}, after=${totalAfterGc}, max=${maxAllowed}` + growth <= maxGrowth, + `expected plateau across churn cycles; phase1=${phase1.afterGc}, phase2=${phase2.afterGc}, growth=${growth}, maxGrowth=${maxGrowth}` ); time.stop(false); diff --git a/ts/test/test-bun-native-backend.ts b/ts/test/test-bun-native-backend.ts new file mode 100644 index 00000000..8b1c0100 --- /dev/null +++ b/ts/test/test-bun-native-backend.ts @@ -0,0 +1,286 @@ +/** + * Copyright 2026 Datadog + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + */ + +import assert from 'assert'; +import delay from 'delay'; +import sinon from 'sinon'; +import {Worker} from 'worker_threads'; + +import { + BunTimeProfiler, + bunGetNativeThreadId, + bunMonitorOutOfMemory, +} from '../src/bun-native-backend'; +import {TimeProfileNode} from '../src/v8-types'; + +describe('BunTimeProfiler', () => { + it('uses microseconds for profile start/end timestamps', async () => { + const profiler = new BunTimeProfiler({intervalMicros: 1000}); + profiler.start(); + await delay(20); + + const profile = profiler.stop(false); + const durationMicros = profile.endTime - profile.startTime; + + assert.ok(durationMicros > 10_000); + assert.ok(durationMicros < 500_000); + }); + + it('advances start time when stop is called with restart=true', async () => { + const profiler = new BunTimeProfiler({intervalMicros: 1000}); + profiler.start(); + await delay(10); + const first = profiler.stop(true); + await delay(10); + const second = profiler.stop(true); + profiler.dispose(); + + assert.ok(first.startTime < first.endTime); + assert.ok(second.startTime >= first.endTime); + assert.ok(second.startTime < second.endTime); + }); + + it('records context transitions while sampling', async () => { + const profiler = new BunTimeProfiler({ + intervalMicros: 1000, + withContexts: true, + }); + profiler.start(); + profiler.context = {}; + await delay(5); + profiler.context = {vehicle: 'car'}; + await delay(5); + profiler.context = {vehicle: 'car', brand: 'mercedes'}; + await delay(5); + + const profile = profiler.stop(false); + const node = profile.topDownRoot.children[0] as TimeProfileNode; + const timeline = node.contexts ?? []; + const labels = timeline.map(timeContext => timeContext.context ?? {}); + + assert.ok(timeline.length >= 3); + assert.deepEqual(labels[0], {}); + assert.deepEqual(labels[1], {vehicle: 'car'}); + assert.deepEqual(labels[2], {vehicle: 'car', brand: 'mercedes'}); + }); + + it('marks profiles with CPU sample metadata when collectCpuTime is enabled', async () => { + const profiler = new BunTimeProfiler({ + intervalMicros: 1000, + withContexts: true, + collectCpuTime: true, + }); + profiler.start(); + profiler.context = {service: 'bun'}; + await delay(10); + + const profile = profiler.stop(false); + const node = profile.topDownRoot.children[0] as TimeProfileNode; + const firstContext = node.contexts?.[0]; + + assert.equal(profile.hasCpuTime, true); + assert.ok(typeof firstContext?.cpuTime === 'number'); + assert.ok((firstContext?.cpuTime ?? 0) >= 0); + }); + + it('preserves CPU-time totals when sub-interval context transitions are filtered', () => { + const hrtimeStub = sinon + .stub(process.hrtime, 'bigint') + .onCall(0) + .returns(1_000_000_000n) + .onCall(1) + .returns(1_000_100_000n) + .onCall(2) + .returns(1_000_200_000n) + .onCall(3) + .returns(1_000_300_000n) + .onCall(4) + .returns(1_002_000_000n) + .onCall(5) + .returns(1_002_000_000n); + const cpuUsageStub = sinon + .stub(process, 'cpuUsage') + .onCall(0) + .returns({user: 0, system: 0}) + .onCall(1) + .returns({user: 100, system: 50}) + .onCall(2) + .returns({user: 250, system: 100}) + .onCall(3) + .returns({user: 400, system: 150}) + .onCall(4) + .returns({user: 700, system: 200}); + + try { + const profiler = new BunTimeProfiler({ + intervalMicros: 1000, + withContexts: true, + collectCpuTime: true, + }); + + profiler.start(); + profiler.context = {route: '/a'}; + profiler.context = {route: '/b'}; + profiler.context = {route: '/c'}; + + const profile = profiler.stop(false); + const node = profile.topDownRoot.children[0] as TimeProfileNode; + const timeline = node.contexts ?? []; + + assert.equal(timeline.length, 1); + assert.deepEqual(timeline[0].context, {route: '/c'}); + assert.equal(timeline[0].cpuTime, 900_000); + } finally { + cpuUsageStub.restore(); + hrtimeStub.restore(); + } + }); + + it('handles bigint context labels and deduplicates equivalent context objects', async () => { + const profiler = new BunTimeProfiler({ + intervalMicros: 1000, + withContexts: true, + }); + profiler.start(); + profiler.context = {requestId: 123n, route: '/health'}; + await delay(5); + profiler.context = {route: '/health', requestId: 123n}; + await delay(5); + + const profile = profiler.stop(false); + const node = profile.topDownRoot.children[0] as TimeProfileNode; + const timeline = node.contexts ?? []; + + assert.equal(timeline.length, 1); + assert.deepEqual(timeline[0].context, { + requestId: 123n, + route: '/health', + }); + }); + + it('deduplicates semantically equivalent nested contexts', async () => { + const profiler = new BunTimeProfiler({ + intervalMicros: 1000, + withContexts: true, + }); + profiler.start(); + profiler.context = { + request: { + path: '/health', + tags: {service: 'api', team: 'profiling'}, + }, + spans: [{name: 'db', durationMicros: 10n}], + }; + await delay(5); + profiler.context = { + spans: [{durationMicros: 10n, name: 'db'}], + request: { + tags: {team: 'profiling', service: 'api'}, + path: '/health', + }, + }; + await delay(5); + + const profile = profiler.stop(false); + const node = profile.topDownRoot.children[0] as TimeProfileNode; + const timeline = node.contexts ?? []; + + assert.equal(timeline.length, 1); + }); + + it('keeps sub-millisecond context transitions distinct', () => { + const dateNowStub = sinon.stub(Date, 'now').returns(1_700_000_000_000); + const hrtimeStub = sinon + .stub(process.hrtime, 'bigint') + .onCall(0) + .returns(1_000_000_000n) + .onCall(1) + .returns(1_000_100_000n) + .onCall(2) + .returns(1_000_250_000n) + .onCall(3) + .returns(1_000_400_000n) + .onCall(4) + .returns(1_000_500_000n); + + try { + const profiler = new BunTimeProfiler({ + intervalMicros: 100, + withContexts: true, + }); + profiler.start(); + profiler.context = {route: '/a'}; + profiler.context = {route: '/b'}; + + const profile = profiler.stop(false); + const node = profile.topDownRoot.children[0] as TimeProfileNode; + const timeline = node.contexts ?? []; + + assert.equal(timeline.length, 2); + assert.deepEqual(timeline[0].context, {route: '/a'}); + assert.deepEqual(timeline[1].context, {route: '/b'}); + assert.ok(timeline[1].timestamp > timeline[0].timestamp); + } finally { + hrtimeStub.restore(); + dateNowStub.restore(); + } + }); +}); + +describe('bunMonitorOutOfMemory', () => { + it('is a no-op on Bun and does not invoke callback', () => { + let callbackInvocations = 0; + + bunMonitorOutOfMemory( + 1024, + 1, + false, + [], + () => { + callbackInvocations++; + }, + 1, + true + ); + + assert.equal(callbackInvocations, 0); + }); +}); + +describe('bunGetNativeThreadId', () => { + it('returns different ids for main thread and worker thread', async function () { + this.timeout(5000); + + const mainThreadId = bunGetNativeThreadId(); + const backendPath = require.resolve('../src/bun-native-backend'); + const worker = new Worker( + ` + const {parentPort} = require('worker_threads'); + const {bunGetNativeThreadId} = require(${JSON.stringify(backendPath)}); + parentPort.postMessage(bunGetNativeThreadId()); + `, + {eval: true} + ); + + const workerThreadId = await new Promise((resolve, reject) => { + worker.once('message', message => resolve(message as number)); + worker.once('error', reject); + worker.once('exit', code => { + if (code !== 0) { + reject(new Error('Worker exited with code ' + code)); + } + }); + }); + + assert.equal(typeof mainThreadId, 'number'); + assert.equal(typeof workerThreadId, 'number'); + assert.notEqual(mainThreadId, workerThreadId); + }); +}); diff --git a/ts/test/test-heap-profiler.ts b/ts/test/test-heap-profiler.ts index 178ca283..0e5af992 100644 --- a/ts/test/test-heap-profiler.ts +++ b/ts/test/test-heap-profiler.ts @@ -168,6 +168,21 @@ describe('HeapProfiler', () => { 'expected startSamplingHeapProfiler to be called' ); }); + it('should be a noop when enabled and started with same parameters', () => { + const intervalBytes = 1024 * 512; + const stackDepth = 32; + heapProfiler.start(intervalBytes, stackDepth); + assert.ok( + startStub.calledWith(intervalBytes, stackDepth), + 'expected startSamplingHeapProfiler to be called' + ); + startStub.resetHistory(); + heapProfiler.start(intervalBytes, stackDepth); + assert.ok( + !startStub.called, + 'expected startSamplingHeapProfiler not to be called second time' + ); + }); it('should throw error when enabled and started with different parameters', () => { const intervalBytes1 = 1024 * 512; const stackDepth1 = 32; @@ -185,7 +200,7 @@ describe('HeapProfiler', () => { assert.strictEqual( (e as Error).message, 'Heap profiler is already started with intervalBytes 524288 and' + - ' stackDepth 64' + ' stackDepth 32' ); } assert.ok( diff --git a/ts/test/test-runtime-loader.ts b/ts/test/test-runtime-loader.ts new file mode 100644 index 00000000..882df647 --- /dev/null +++ b/ts/test/test-runtime-loader.ts @@ -0,0 +1,125 @@ +/** + * Copyright 2026 Datadog + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + */ + +import assert from 'assert'; +import {mkdtempSync, mkdirSync, writeFileSync} from 'fs'; +import {tmpdir} from 'os'; +import path from 'path'; + +const RUNTIME_ENV_KEY = 'DATADOG_PPROF_RUNTIME'; + +function clearModule(modulePath: string) { + const resolved = require.resolve(modulePath); + delete require.cache[resolved]; +} + +describe('Runtime Loader', () => { + const originalRuntime = process.env[RUNTIME_ENV_KEY]; + + function restoreRuntimeEnv() { + if (typeof originalRuntime !== 'undefined') { + process.env[RUNTIME_ENV_KEY] = originalRuntime; + return; + } + + delete process.env[RUNTIME_ENV_KEY]; + } + + function clearAllProfileModules() { + clearModule('../src/runtime'); + clearModule('../src/native-backend-loader'); + clearModule('../src/time-profiler-bindings'); + clearModule('../src/heap-profiler-bindings'); + clearModule('../src/time-profiler'); + clearModule('../src/heap-profiler'); + clearModule('../src/index'); + } + + afterEach(() => { + restoreRuntimeEnv(); + clearAllProfileModules(); + }); + + it('loads a bun-compatible backend when runtime is forced to bun', async () => { + process.env[RUNTIME_ENV_KEY] = 'bun'; + clearAllProfileModules(); + const pprof = require('../src'); + + assert.equal(typeof pprof.time.start, 'function'); + assert.equal(typeof pprof.heap.start, 'function'); + + pprof.time.start({withContexts: true, intervalMicros: 1_000}); + pprof.time.setContext({service: 'bun-smoke'}); + const timeProfile = pprof.time.stop(); + assert.ok(timeProfile.sample.length > 0); + const encodedTime = await pprof.encode(timeProfile); + assert.ok(encodedTime.length > 0); + + pprof.heap.start(128 * 1024, 64); + const heapProfile = pprof.heap.profile(); + pprof.heap.stop(); + assert.ok(heapProfile.sample.length > 0); + const encodedHeap = await pprof.encode(heapProfile); + assert.ok(encodedHeap.length > 0); + }); + + it('falls back to build/Release when node-gyp-build cannot resolve', () => { + process.env[RUNTIME_ENV_KEY] = 'node'; + clearAllProfileModules(); + + const {__testing} = require('../src/native-backend-loader'); + + const root = mkdtempSync(path.join(tmpdir(), 'pprof-loader-fallback-')); + const release = path.join(root, 'build', 'Release'); + mkdirSync(release, {recursive: true}); + const fallbackModulePath = path.join(release, 'dd_pprof.node'); + writeFileSync(fallbackModulePath, ''); + + let requiredPath: string | undefined; + const requiredValue = {fromFallback: true}; + const loaded = __testing.loadNodeNativeModule( + root, + () => { + throw new Error( + 'No native build was found for runtime=node abi=141 platform=darwinglibc arch=arm64' + ); + }, + ((modulePath: string) => { + requiredPath = modulePath; + return requiredValue; + }) as (modulePath: string) => unknown + ); + + assert.equal(loaded, requiredValue); + assert.equal(requiredPath, fallbackModulePath); + }); + + it('rethrows original node-gyp-build error when no local build artifacts exist', () => { + process.env[RUNTIME_ENV_KEY] = 'node'; + clearAllProfileModules(); + + const {__testing} = require('../src/native-backend-loader'); + const root = mkdtempSync(path.join(tmpdir(), 'pprof-loader-no-fallback-')); + + assert.throws( + () => + __testing.loadNodeNativeModule( + root, + () => { + throw new Error( + 'No native build was found for runtime=node abi=141 platform=darwinglibc arch=arm64' + ); + }, + require + ), + /No native build was found for runtime=node/ + ); + }); +}); diff --git a/ts/test/test-runtime.ts b/ts/test/test-runtime.ts new file mode 100644 index 00000000..9d1b08f2 --- /dev/null +++ b/ts/test/test-runtime.ts @@ -0,0 +1,65 @@ +/** + * Copyright 2026 Datadog + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + */ + +import assert from 'assert'; + +import {__testing} from '../src/runtime'; + +describe('Runtime detection', () => { + it('prefers explicit node override', () => { + const runtime = __testing.detectRuntimeFromInputs({ + envOverride: 'node', + bunVersion: '1.3.5', + bunGlobal: {}, + }); + + assert.equal(runtime, 'node'); + }); + + it('prefers explicit bun override', () => { + const runtime = __testing.detectRuntimeFromInputs({ + envOverride: 'bun', + bunVersion: undefined, + bunGlobal: undefined, + }); + + assert.equal(runtime, 'bun'); + }); + + it('detects bun from process.versions.bun', () => { + const runtime = __testing.detectRuntimeFromInputs({ + envOverride: undefined, + bunVersion: '1.3.5', + bunGlobal: undefined, + }); + + assert.equal(runtime, 'bun'); + }); + + it('detects bun from globalThis.Bun when bunVersion is unavailable', () => { + const runtime = __testing.detectRuntimeFromInputs({ + envOverride: undefined, + bunVersion: undefined, + bunGlobal: {version: '1.3.5'}, + }); + + assert.equal(runtime, 'bun'); + }); + + it('defaults to node when bun markers are absent', () => { + const runtime = __testing.detectRuntimeFromInputs({ + envOverride: undefined, + bunVersion: undefined, + bunGlobal: undefined, + }); + + assert.equal(runtime, 'node'); + }); +});