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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 2 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -97,14 +97,10 @@ Install [`pprof`][npm-url] with `npm` or add to your `package.json`.
pprof -http=: heap.pb.gz
```

* Collecting a heap profile with V8 allocation profile format:
* Collecting a heap profile with V8 allocation profile format:
```javascript
const profile = pprof.heap.v8Profile(pprof.heap.convertProfile);
const profile = await pprof.heap.v8Profile();
```
`v8Profile` accepts a callback and returns its result. Allocation nodes
are only valid during the callback, so copy/transform what you need
before returning. `heap.convertProfile` performs that conversion during
the callback, and `heap.profile()` uses it under the hood.

[build-image]: https://github.com/Datadog/pprof-nodejs/actions/workflows/build.yml/badge.svg?branch=main
[build-url]: https://github.com/Datadog/pprof-nodejs/actions/workflows/build.yml
Expand Down
18 changes: 18 additions & 0 deletions bindings/profilers/heap.cc
Original file line number Diff line number Diff line change
Expand Up @@ -488,6 +488,23 @@ NAN_METHOD(HeapProfiler::StopSamplingHeapProfiler) {
}
}

// Signature:
// getAllocationProfile(): AllocationProfileNode
NAN_METHOD(HeapProfiler::GetAllocationProfile) {
auto isolate = info.GetIsolate();
std::unique_ptr<v8::AllocationProfile> profile(
isolate->GetHeapProfiler()->GetAllocationProfile());
if (!profile) {
return Nan::ThrowError("Heap profiler is not enabled.");
}
v8::AllocationProfile::Node* root = profile->GetRootNode();
auto state = PerIsolateData::For(isolate)->GetHeapProfilerState();
if (state) {
state->OnNewProfile();
}
info.GetReturnValue().Set(TranslateAllocationProfile(root));
}

// mapAllocationProfile(callback): callback result
NAN_METHOD(HeapProfiler::MapAllocationProfile) {
if (info.Length() < 1 || !info[0]->IsFunction()) {
Expand Down Expand Up @@ -579,6 +596,7 @@ NAN_MODULE_INIT(HeapProfiler::Init) {
heapProfiler, "startSamplingHeapProfiler", StartSamplingHeapProfiler);
Nan::SetMethod(
heapProfiler, "stopSamplingHeapProfiler", StopSamplingHeapProfiler);
Nan::SetMethod(heapProfiler, "getAllocationProfile", GetAllocationProfile);
Nan::SetMethod(heapProfiler, "mapAllocationProfile", MapAllocationProfile);
Nan::SetMethod(heapProfiler, "monitorOutOfMemory", MonitorOutOfMemory);
Nan::Set(target,
Expand Down
4 changes: 4 additions & 0 deletions bindings/profilers/heap.hh
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,10 @@ class HeapProfiler {
// stopSamplingHeapProfiler()
static NAN_METHOD(StopSamplingHeapProfiler);

// Signature:
// getAllocationProfile(): AllocationProfileNode
static NAN_METHOD(GetAllocationProfile);

// Signature:
// mapAllocationProfile(callback): callback result
static NAN_METHOD(MapAllocationProfile);
Expand Down
37 changes: 37 additions & 0 deletions bindings/profilers/wall.cc
Original file line number Diff line number Diff line change
Expand Up @@ -920,6 +920,28 @@ v8::ProfilerId WallProfiler::StartInternal() {
return result.id;
}

NAN_METHOD(WallProfiler::Stop) {
if (info.Length() != 1) {
return Nan::ThrowTypeError("Stop must have one argument.");
}
if (!info[0]->IsBoolean()) {
return Nan::ThrowTypeError("Restart must be a boolean.");
}

bool restart = info[0].As<Boolean>()->Value();

WallProfiler* wallProfiler =
Nan::ObjectWrap::Unwrap<WallProfiler>(info.This());

v8::Local<v8::Value> profile;
auto err = wallProfiler->StopImpl(restart, profile);

if (!err.success) {
return Nan::ThrowTypeError(err.msg.c_str());
}
info.GetReturnValue().Set(profile);
}

// stopAndCollect(restart, callback): callback result
NAN_METHOD(WallProfiler::StopAndCollect) {
if (info.Length() != 2) {
Expand Down Expand Up @@ -1078,6 +1100,20 @@ Result WallProfiler::StopCore(bool restart, ProfileBuilder&& buildProfile) {
return {};
}

Result WallProfiler::StopImpl(bool restart, v8::Local<v8::Value>& profile) {
return StopCore(restart,
[&](const v8::CpuProfile* v8_profile,
bool hasCpuTime,
int64_t nonJSThreadsCpuTime,
ContextsByNode* contextsByNodePtr) {
profile = TranslateTimeProfile(v8_profile,
includeLines_,
contextsByNodePtr,
hasCpuTime,
nonJSThreadsCpuTime);
});
}

Result WallProfiler::StopAndCollectImpl(bool restart,
v8::Local<v8::Function> callback,
v8::Local<v8::Value>& result) {
Expand Down Expand Up @@ -1112,6 +1148,7 @@ NAN_MODULE_INIT(WallProfiler::Init) {
SetContext);

Nan::SetPrototypeMethod(tpl, "start", Start);
Nan::SetPrototypeMethod(tpl, "stop", Stop);
Nan::SetPrototypeMethod(tpl, "stopAndCollect", StopAndCollect);
Nan::SetPrototypeMethod(tpl, "dispose", Dispose);
Nan::SetPrototypeMethod(tpl,
Expand Down
2 changes: 2 additions & 0 deletions bindings/profilers/wall.hh
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,7 @@ class WallProfiler : public Nan::ObjectWrap {
v8::ProfilerId StartInternal();
template <typename ProfileBuilder>
Result StopCore(bool restart, ProfileBuilder&& buildProfile);
Result StopImpl(bool restart, v8::Local<v8::Value>& profile);
Result StopAndCollectImpl(bool restart,
v8::Local<v8::Function> callback,
v8::Local<v8::Value>& result);
Expand Down Expand Up @@ -188,6 +189,7 @@ class WallProfiler : public Nan::ObjectWrap {

static NAN_METHOD(New);
static NAN_METHOD(Start);
static NAN_METHOD(Stop);
static NAN_METHOD(StopAndCollect);
static NAN_METHOD(V8ProfilerStuckEventLoopDetected);
static NAN_METHOD(Dispose);
Expand Down
29 changes: 29 additions & 0 deletions bindings/translate-heap-profile.cc
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,30 @@ class HeapProfileTranslator : ProfileTranslator {
#undef X

public:
v8::Local<v8::Value> TranslateAllocationProfile(
v8::AllocationProfile::Node* node) {
v8::Local<v8::Array> children = NewArray(node->children.size());
for (size_t i = 0; i < node->children.size(); i++) {
Set(children, i, TranslateAllocationProfile(node->children[i]));
}

v8::Local<v8::Array> allocations = NewArray(node->allocations.size());
for (size_t i = 0; i < node->allocations.size(); i++) {
auto alloc = node->allocations[i];
Set(allocations,
i,
CreateAllocation(NewNumber(alloc.count), NewNumber(alloc.size)));
}

return CreateNode(node->name,
node->script_name,
NewInteger(node->script_id),
NewInteger(node->line_number),
NewInteger(node->column_number),
children,
allocations);
}

v8::Local<v8::Value> TranslateAllocationProfile(Node* node) {
v8::Local<v8::Array> children = NewArray(node->children.size());
for (size_t i = 0; i < node->children.size(); i++) {
Expand Down Expand Up @@ -118,6 +142,11 @@ std::shared_ptr<Node> TranslateAllocationProfileToCpp(
return new_node;
}

v8::Local<v8::Value> TranslateAllocationProfile(
v8::AllocationProfile::Node* node) {
return HeapProfileTranslator().TranslateAllocationProfile(node);
}

v8::Local<v8::Value> TranslateAllocationProfile(Node* node) {
return HeapProfileTranslator().TranslateAllocationProfile(node);
}
Expand Down
3 changes: 3 additions & 0 deletions bindings/translate-heap-profile.hh
Original file line number Diff line number Diff line change
Expand Up @@ -39,4 +39,7 @@ std::shared_ptr<Node> TranslateAllocationProfileToCpp(
v8::AllocationProfile::Node* node);

v8::Local<v8::Value> TranslateAllocationProfile(Node* node);
v8::Local<v8::Value> TranslateAllocationProfile(
v8::AllocationProfile::Node* node);

} // namespace dd
4 changes: 4 additions & 0 deletions ts/src/heap-profiler-bindings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,10 @@ export function stopSamplingHeapProfiler() {
profiler.heapProfiler.stopSamplingHeapProfiler();
}

export function getAllocationProfile(): AllocationProfileNode {
return profiler.heapProfiler.getAllocationProfile();
}

export function mapAllocationProfile<T>(
callback: (root: AllocationProfileNode) => T,
): T {
Expand Down
43 changes: 40 additions & 3 deletions ts/src/heap-profiler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
import {Profile} from 'pprof-format';

import {
getAllocationProfile,
mapAllocationProfile,
startSamplingHeapProfiler,
stopSamplingHeapProfiler,
Expand All @@ -33,6 +34,20 @@ import {isMainThread} from 'worker_threads';
let enabled = false;
let heapIntervalBytes = 0;
let heapStackDepth = 0;

/*
* Collects a heap profile when heapProfiler is enabled. Otherwise throws
* an error.
*
* Data is returned in V8 allocation profile format.
*/
export function v8Profile(): AllocationProfileNode {
if (!enabled) {
throw new Error('Heap profiler is not enabled.');
}
return getAllocationProfile();
}

/**
* Collects a heap profile when heapProfiler is enabled. Otherwise throws
* an error.
Expand All @@ -44,13 +59,35 @@ let heapStackDepth = 0;
* @param callback - function to convert the heap profiler to a converted profile
* @returns <T> converted profile
*/
export function v8Profile<T>(callback: (root: AllocationProfileNode) => T): T {
export function v8ProfileV2<T>(
callback: (root: AllocationProfileNode) => T,
): T {
if (!enabled) {
throw new Error('Heap profiler is not enabled.');
}
return mapAllocationProfile(callback);
}

/**
* Collects a profile and returns it serialized in pprof format.
* Throws if heap profiler is not enabled.
*
* @param ignoreSamplePath
* @param sourceMapper
*/
export function profile(
ignoreSamplePath?: string,
sourceMapper?: SourceMapper,
generateLabels?: GenerateAllocationLabelsFunction,
): Profile {
return convertProfile(
v8Profile(),
ignoreSamplePath,
sourceMapper,
generateLabels,
);
}

export function convertProfile(
rootNode: AllocationProfileNode,
ignoreSamplePath?: string,
Expand Down Expand Up @@ -93,12 +130,12 @@ export function convertProfile(
* @param sourceMapper
* @param generateLabels
*/
export function profile(
export function profileV2(
ignoreSamplePath?: string,
sourceMapper?: SourceMapper,
generateLabels?: GenerateAllocationLabelsFunction,
): Profile {
return v8Profile(root => {
return v8ProfileV2(root => {
return convertProfile(root, ignoreSamplePath, sourceMapper, generateLabels);
});
}
Expand Down
2 changes: 2 additions & 0 deletions ts/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ export const time = {
profile: timeProfiler.profile,
start: timeProfiler.start,
stop: timeProfiler.stop,
profileV2: timeProfiler.profileV2,
getContext: timeProfiler.getContext,
setContext: timeProfiler.setContext,
runWithContext: timeProfiler.runWithContext,
Expand All @@ -49,6 +50,7 @@ export const heap = {
start: heapProfiler.start,
stop: heapProfiler.stop,
profile: heapProfiler.profile,
profileV2: heapProfiler.profileV2,
convertProfile: heapProfiler.convertProfile,
v8Profile: heapProfiler.v8Profile,
monitorOutOfMemory: heapProfiler.monitorOutOfMemory,
Expand Down
50 changes: 44 additions & 6 deletions ts/src/time-profiler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

import {setTimeout} from 'timers/promises';

import {Profile} from 'pprof-format';
import {
serializeTimeProfile,
GARBAGE_COLLECTION_FUNCTION_NAME,
Expand Down Expand Up @@ -43,7 +44,7 @@ type Microseconds = number;
type Milliseconds = number;

type NativeTimeProfiler = InstanceType<typeof TimeProfiler> & {
stopAndCollect: <T>(
stopAndCollect?: <T>(
restart: boolean,
callback: (profile: TimeProfile) => T,
) => T;
Expand All @@ -65,7 +66,7 @@ function handleStopRestart() {
// 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.stopAndCollect(false, () => undefined);
gProfiler.stop(false);
gProfiler.start();
}
}
Expand Down Expand Up @@ -119,13 +120,22 @@ const DEFAULT_OPTIONS: TimeProfilerOptions = {
useCPED: false,
};

export async function profile(options: TimeProfilerOptions = {}) {
export async function profile(
options: TimeProfilerOptions = {},
): Promise<Profile> {
options = {...DEFAULT_OPTIONS, ...options};
start(options);
await setTimeout(options.durationMillis!);
return stop();
}

export async function profileV2(options: TimeProfilerOptions = {}) {
options = {...DEFAULT_OPTIONS, ...options};
start(options);
await setTimeout(options.durationMillis!);
return stopV2();
}

// Temporarily retained for backwards compatibility with older tracer
export function start(options: TimeProfilerOptions = {}) {
options = {...DEFAULT_OPTIONS, ...options};
Expand All @@ -148,11 +158,39 @@ export function start(options: TimeProfilerOptions = {}) {
}
}

export function stop(
restart = false,
generateLabels?: GenerateTimeLabelsFunction,
lowCardinalityLabels?: string[],
): Profile {
if (!gProfiler) {
throw new Error('Wall profiler is not started');
}

const profile = gProfiler.stop(restart);
if (restart) {
handleStopRestart();
} else {
handleStopNoRestart();
}

const serializedProfile = serializeTimeProfile(
profile,
gIntervalMicros,
gSourceMapper,
true,
generateLabels,
lowCardinalityLabels,
);
return serializedProfile;
}

/**
* Serializes the profile inside a native callback while the V8 profile is
* still alive. This reduces memory overhead.
* Same as stop() but uses the lazy callback path: serialization happens inside
* a native callback while the V8 profile is still alive.
* This reduces memory overhead.
*/
export function stop(
export function stopV2(
restart = false,
generateLabels?: GenerateTimeLabelsFunction,
lowCardinalityLabels?: string[],
Expand Down
Loading
Loading