Skip to content
Draft
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
2 changes: 0 additions & 2 deletions bindings/allocation-profile.cc
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@
using namespace v8;

namespace dd {
namespace {
Local<Object> CreateAllocationObject(Isolate* isolate,
const AllocationProfileNodeStats& stats) {
Local<Object> alloc_obj = Object::New(isolate);
Expand All @@ -41,7 +40,6 @@ Local<Object> CreateAllocationObject(Isolate* isolate,
Number::New(isolate, static_cast<double>(stats.alloc_space_bytes)));
return alloc_obj;
}
} // namespace

AllocationProfileNodeStatsMap BuildAllocationStatsByNodeId(
const std::vector<AllocationProfile::Sample>& samples) {
Expand Down
3 changes: 3 additions & 0 deletions bindings/allocation-profile.hh
Original file line number Diff line number Diff line change
Expand Up @@ -45,4 +45,7 @@ v8::Local<v8::Array> TranslateAllocationStats(
v8::Isolate* isolate,
const AllocationProfileSizeStatsMap* allocation_stats);

v8::Local<v8::Object> CreateAllocationObject(
v8::Isolate* isolate, const AllocationProfileNodeStats& stats);

} // namespace dd
98 changes: 57 additions & 41 deletions bindings/profilers/near-oom.cc
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

#include "near-oom.hh"

#include "allocation-profile.hh"
#include "defer.hh"
#include "heap.hh"
#include "per-isolate-data.hh"
Expand Down Expand Up @@ -162,7 +163,7 @@ static int CreateTempFile(uv_loop_t& loop, std::string& filepath) {
nullptr);
uv_fs_req_cleanup(&fs_req);
if (fd >= 0) {
return r;
return fd;
}
if (fd != UV_EEXIST) {
fprintf(stderr, "Failed to create temp file: %s\n", uv_strerror(fd));
Expand Down Expand Up @@ -193,6 +194,10 @@ static void ExportProfile(HeapProfilerState& state) {
return;
}
FILE* file = fdopen(fd, "w");
if (!file) {
fprintf(stderr, "Failed to open temp file: %s\n", strerror(errno));

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since you can't call fclose here, you'll leak fd and leave a temp file around. I think you should call close(fd) and delete the temp file.

If you want to be comprehensive, the other "if" statements below have the same problem (and had them before your change :-).) The one on line 231 should also delete the temp file. Then you have the ifs for uv_timer_init and uv_timer_start – I'd write it so that failure in either init or start doesn't return, but falls through to uv_run and unlink (obviously, don't call start if init failed.) I think that'd make sure neither FD is leaked nor a temp file is left around. Since we're in a low memory situation, any of these can run into an ENOMEM.

return;
}
dumpAllocationProfileAsJSON(file, state.profile.get());
fclose(file);
std::vector<char*> args;
Expand Down Expand Up @@ -259,6 +264,47 @@ static size_t ExtendedHeapLimit(size_t current_heap_limit, size_t extension) {
: current_heap_limit + extension;
}

static void CaptureProfile(v8::Isolate* isolate,
const std::shared_ptr<HeapProfilerState>& state) {
// Drop the superseded capture before v8 allocates the next one.
state->profile.reset();
std::unique_ptr<v8::AllocationProfile> profile{
isolate->GetHeapProfiler()->GetAllocationProfile()};
if (!profile) {
fprintf(stderr,
"NearHeapLimit: heap profiler is not enabled, no allocation "
"profile to report\n");
return;
}

// Only the JS callback reads stats; dump and export use Node::allocations.
const bool with_stats = state->allocations && !state->callback.IsEmpty();
AllocationProfileNodeStatsMap allocation_stats;
if (with_stats) {
allocation_stats = BuildAllocationStatsByNodeId(profile->GetSamples());
}
state->profile = TranslateAllocationProfileToCpp(
profile->GetRootNode(), with_stats ? &allocation_stats : nullptr);

if (state->dumpProfileOnStderr) {
dumpAllocationProfile(stderr, state->profile.get());
}
if (!state->export_command.empty()) {
ExportProfile(*state);
}

if (state->callback.IsEmpty()) {
state->profile.reset();
return;
}
if (state->callbackMode & kInterruptCallback) {
isolate->RequestInterrupt(InterruptCallback, nullptr);
}
if (state->callbackMode & kAsyncCallback) {
uv_async_send(state->async);
}
}

size_t NearHeapLimit(void* data,
size_t current_heap_limit,
size_t initial_heap_limit) {
Expand Down Expand Up @@ -337,42 +383,8 @@ size_t NearHeapLimit(void* data,
stats.object_count());
}
}
// GetAllocationProfile returns null when V8's sampling heap profiler isn't
// running, and that can happen while this callback is still installed:
// HeapProfilerCleanupHook stops V8's sampler without touching our state, so
// between that hook and the isolate actually going away we stay registered
// with nothing to sample. The heap-limit bookkeeping below still has to run,
// so skip only the profile-dependent work.
std::unique_ptr<v8::AllocationProfile> profile{
isolate->GetHeapProfiler()->GetAllocationProfile()};
if (profile) {
state->profile = TranslateAllocationProfileToCpp(profile->GetRootNode());
if (state->dumpProfileOnStderr) {
dumpAllocationProfile(stderr, state->profile.get());
}

if (!state->export_command.empty()) {
ExportProfile(*state);
}

if (!state->callback.IsEmpty()) {
if (state->callbackMode & kInterruptCallback) {
isolate->RequestInterrupt(InterruptCallback, nullptr);
}
if (state->callbackMode & kAsyncCallback) {
uv_async_send(state->async);
}
} else {
state->profile.reset();
}
} else {
// Drop any profile retained from an earlier invocation: it is stale, and
// nothing below is going to consume or replace it.
state->profile.reset();
fprintf(stderr,
"NearHeapLimit: heap profiler is not enabled, no allocation "
"profile to report\n");
}
// Capture now; the event loop may never run. insideCallback guards re-entry.
CaptureProfile(isolate, state);

if (!state->isMainThread) {
// In worker thread, OOM is not fatal to the whole process and will only
Expand Down Expand Up @@ -441,6 +453,9 @@ NAN_METHOD(HeapProfiler::MonitorOutOfMemory) {
state = std::make_shared<HeapProfilerState>(isolate);
}

// Uninstall while reconfiguring: a hit here would see a half-applied config.
state->UninstallNearHeapLimitCallback();

state->current_heap_extension_count = 0;
state->automatic_heap_extension_size.reset();
state->profile.reset();
Expand All @@ -453,7 +468,6 @@ NAN_METHOD(HeapProfiler::MonitorOutOfMemory) {
state->callbackMode = info[5].As<v8::Integer>()->Value();
state->isMainThread = info[6].As<v8::Boolean>()->Value();
state->automatic_heap_extension = info[7].As<v8::Boolean>()->Value();
state->InstallNearHeapLimitCallback();
if (!info[4]->IsNullOrUndefined() && state->callbackMode != kNoCallback) {
state->callback.Reset(Nan::To<v8::Function>(info[4]).ToLocalChecked());
}
Expand All @@ -470,6 +484,7 @@ NAN_METHOD(HeapProfiler::MonitorOutOfMemory) {
if (!state->callback.IsEmpty() && (state->callbackMode & kAsyncCallback)) {
state->RegisterAsyncCallback();
}
state->InstallNearHeapLimitCallback();
}

void InterruptCallback(v8::Isolate* isolate, void* data) {
Expand All @@ -480,12 +495,13 @@ void InterruptCallback(v8::Isolate* isolate, void* data) {
if (!state || !state->profile) {
return;
}
// Own it first: translating and the callback can re-enter NearHeapLimit.
auto profile = std::move(state->profile);

v8::Local<v8::Value> argv[1] = {
dd::TranslateAllocationProfile(state->profile.get())};
dd::TranslateAllocationProfile(profile.get())};
Nan::AsyncResource resource("NearHeapLimit");
state->callback.Call(1, argv, &resource);
// Release the retained native profile once the callback has been invoked.
state->profile.reset();
}

void AsyncCallback(uv_async_t* handle) {
Expand Down
112 changes: 66 additions & 46 deletions bindings/translate-heap-profile.cc
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,12 @@
namespace dd {

namespace {
const AllocationProfileSizeStatsMap* FindNodeStats(
const AllocationProfileNodeStatsMap& allocation_stats, uint32_t node_id) {
auto node_stats = allocation_stats.find(node_id);
return node_stats == allocation_stats.end() ? nullptr : &node_stats->second;
}

class HeapProfileTranslator : ProfileTranslator {
#define NODE_FIELDS \
X(name) \
Expand All @@ -41,56 +47,27 @@ 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(
v8::AllocationProfile::Node* node,
const AllocationProfileNodeStatsMap* allocation_stats) {
if (!allocation_stats) {
return TranslateAllocationProfile(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], allocation_stats));
}

auto node_stats = allocation_stats->find(node->node_id);
v8::Local<v8::Array> allocations = TranslateAllocationStats(
isolate,
node_stats == allocation_stats->end() ? nullptr : &node_stats->second);

return CreateNode(node->name,
node->script_name,
NewInteger(node->script_id),
NewInteger(node->line_number),
NewInteger(node->column_number),
children,
allocations);
return CreateNode(
node->name,
node->script_name,
NewInteger(node->script_id),
NewInteger(node->line_number),
NewInteger(node->column_number),
children,
allocation_stats
? TranslateAllocationStats(
isolate, FindNodeStats(*allocation_stats, node->node_id))
: TranslateAllocations(node->allocations));
}

v8::Local<v8::Value> TranslateAllocationProfile(Node* node) {
Expand All @@ -101,10 +78,12 @@ class HeapProfileTranslator : ProfileTranslator {

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

return CreateNode(NewString(node->name.c_str()),
Expand All @@ -117,6 +96,18 @@ class HeapProfileTranslator : ProfileTranslator {
}

private:
v8::Local<v8::Array> TranslateAllocations(
const std::vector<v8::AllocationProfile::Allocation>& node_allocations) {
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 allocations;
}

v8::Local<v8::Object> CreateNode(v8::Local<v8::String> name,
v8::Local<v8::String> scriptName,
v8::Local<v8::Integer> scriptId,
Expand All @@ -142,13 +133,24 @@ class HeapProfileTranslator : ProfileTranslator {
return js_alloc;
}

// Shares the object shape with the non-detached path in allocation-profile.
v8::Local<v8::Object> CreateAllocationStats(const Node::Allocation& alloc) {
AllocationProfileNodeStats stats;
stats.inuse_objects = alloc.inuse_objects;
stats.alloc_objects = alloc.alloc_objects;
stats.inuse_space_bytes = alloc.inuse_objects * alloc.size;
stats.alloc_space_bytes = alloc.alloc_objects * alloc.size;
return CreateAllocationObject(isolate, stats);
}

public:
explicit HeapProfileTranslator() {}
};
} // namespace

std::shared_ptr<Node> TranslateAllocationProfileToCpp(
v8::AllocationProfile::Node* node) {
v8::AllocationProfile::Node* node,
const AllocationProfileNodeStatsMap* allocation_stats) {
auto new_node = std::make_shared<Node>();
new_node->line_number = node->line_number;
new_node->column_number = node->column_number;
Expand All @@ -160,12 +162,30 @@ std::shared_ptr<Node> TranslateAllocationProfileToCpp(

new_node->children.reserve(node->children.size());
for (auto& child : node->children) {
new_node->children.push_back(TranslateAllocationProfileToCpp(child));
new_node->children.push_back(
TranslateAllocationProfileToCpp(child, allocation_stats));
}

// Join now: the samples these node_ids key into are freed on return.
new_node->has_allocation_stats = allocation_stats != nullptr;
const auto* stats = allocation_stats
? FindNodeStats(*allocation_stats, node->node_id)
: nullptr;
new_node->allocations.reserve(node->allocations.size());
for (auto& allocation : node->allocations) {
new_node->allocations.push_back(allocation);
for (const auto& alloc : node->allocations) {
Node::Allocation out;
out.size = alloc.size;
out.count = alloc.count;
out.inuse_objects = alloc.count;
out.alloc_objects = alloc.count;
if (stats) {
auto size_stats = stats->find(alloc.size);
if (size_stats != stats->end()) {
out.inuse_objects = size_stats->second.inuse_objects;
out.alloc_objects = size_stats->second.alloc_objects;
}
}
new_node->allocations.push_back(out);
}
return new_node;
}
Expand Down
16 changes: 14 additions & 2 deletions bindings/translate-heap-profile.hh
Original file line number Diff line number Diff line change
Expand Up @@ -27,18 +27,30 @@
namespace dd {

struct Node {
using Allocation = v8::AllocationProfile::Allocation;
struct Allocation {
size_t size = 0;
// v8's per-size count, which the stderr dump and the export render.
uint32_t count = 0;
// Rebuilt from the profile's samples, and equal to count outside
// allocation mode, where v8 reports no live/allocated split.
uint64_t inuse_objects = 0;
uint64_t alloc_objects = 0;
};

std::string name;
std::string script_name;
int line_number;
int column_number;
int script_id;
std::vector<std::shared_ptr<Node>> children;
std::vector<Allocation> allocations;
// Set on every node iff the profile was captured in allocation mode.
bool has_allocation_stats = false;
};

std::shared_ptr<Node> TranslateAllocationProfileToCpp(
v8::AllocationProfile::Node* node);
v8::AllocationProfile::Node* node,
const AllocationProfileNodeStatsMap* allocation_stats);

v8::Local<v8::Value> TranslateAllocationProfile(Node* node);
v8::Local<v8::Value> TranslateAllocationProfile(
Expand Down
Loading
Loading