Skip to content

Commit

Permalink
v8: support gc profile
Browse files Browse the repository at this point in the history
  • Loading branch information
theanarkh committed Jan 18, 2023
1 parent 5d50b84 commit c54c984
Show file tree
Hide file tree
Showing 8 changed files with 480 additions and 2 deletions.
77 changes: 77 additions & 0 deletions doc/api/v8.md
Original file line number Diff line number Diff line change
Expand Up @@ -390,6 +390,83 @@ The API is a no-op if `--heapsnapshot-near-heap-limit` is already set from the
command line or the API is called more than once. `limit` must be a positive
integer. See [`--heapsnapshot-near-heap-limit`][] for more information.

## `v8.takeGCProfile(options)`

<!-- YAML
added: REPLACEME
-->

* `options` {Object}
* `filename` {string} Node.js will write data to `filename`.
* `duration` {number} how long you want to collect the gc data.

* Returns: {Promise}

This API collects gc data over a period of time and write it to the `filename`.
The content is as follows.

```json
{
"version": 1,
"startTime": 1674059033862,
"statistics": [
{
"gcType": "Scavenge",
"beforeGC": {
"heapStatistics": {
"totalHeapSize": 5005312,
"totalHeapSizeExecutable": 524288,
"totalPhysicalSize": 5226496,
"totalAvailableSize": 4341325216,
"totalGlobalHandlesSize": 8192,
"usedGlobalHandlesSize": 2112,
"usedHeapSize": 4883840,
"heapSizeLimit": 4345298944,
"mallocedMemory": 254128,
"externalMemory": 225138,
"peakMallocedMemory": 181760
},
"heapSpaceStatistics": [
{
"spaceName": "read_only_space",
"spaceSize": 0,
"spaceUsedSize": 0,
"spaceAvailableSize": 0,
"physicalSpaceSize": 0
}
]
},
"cost": 1574.14,
"afterGC": {
"heapStatistics": {
"totalHeapSize": 6053888,
"totalHeapSizeExecutable": 524288,
"totalPhysicalSize": 5500928,
"totalAvailableSize": 4341101384,
"totalGlobalHandlesSize": 8192,
"usedGlobalHandlesSize": 2112,
"usedHeapSize": 4059096,
"heapSizeLimit": 4345298944,
"mallocedMemory": 254128,
"externalMemory": 225138,
"peakMallocedMemory": 181760
},
"heapSpaceStatistics": [
{
"spaceName": "read_only_space",
"spaceSize": 0,
"spaceUsedSize": 0,
"spaceAvailableSize": 0,
"physicalSpaceSize": 0
}
]
}
}
],
"endtTime": 1674059036865
}
```

## Serialization API

The serialization API provides means of serializing JavaScript values in a way
Expand Down
31 changes: 29 additions & 2 deletions lib/v8.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ const {
} = primordials;

const { Buffer } = require('buffer');
const { validateString, validateUint32 } = require('internal/validators');
const { validateString, validateUint32, validateObject, validateNumber } = require('internal/validators');
const {
Serializer,
Deserializer
Expand Down Expand Up @@ -63,7 +63,8 @@ const {
} = require('internal/heap_utils');
const promiseHooks = require('internal/promise_hooks');
const { getOptionValue } = require('internal/options');

const { setUnrefTimeout } = require('internal/timers');
const { Promise } = primordials;
/**
* Generates a snapshot of the current V8 heap
* and writes it to a JSON file.
Expand Down Expand Up @@ -397,6 +398,31 @@ function deserialize(buffer) {
return der.readValue();
}

/**
* @param {{
* filename: string,
* duration: number,
* }} [options]
*/
function takeGCProfile(options) {
validateObject(options, 'options');
validateString(options.filename, 'options.filename');
validateNumber(options.duration, 'options.duration', 1);
return new Promise((resolve, reject) => {
const profiler = new binding.GCProfiler();
try {
profiler.startGCProfile(options.filename);
} catch (e) {
reject(e);
return;
}
setUnrefTimeout(() => {
profiler.stopGCProfile();
resolve();
}, options.duration);
});
}

module.exports = {
cachedDataVersionTag,
getHeapSnapshot,
Expand All @@ -416,4 +442,5 @@ module.exports = {
promiseHooks,
startupSnapshot,
setHeapSnapshotNearHeapLimit,
takeGCProfile,
};
188 changes: 188 additions & 0 deletions src/node_v8.cc
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ namespace v8_utils {
using v8::Array;
using v8::Context;
using v8::FunctionCallbackInfo;
using v8::FunctionTemplate;
using v8::HandleScope;
using v8::HeapCodeStatistics;
using v8::HeapSpaceStatistics;
Expand Down Expand Up @@ -210,6 +211,180 @@ void SetFlagsFromString(const FunctionCallbackInfo<Value>& args) {
V8::SetFlagsFromString(*flags, static_cast<size_t>(flags.length()));
}

static const char * GetGCTypeName(v8::GCType gc_type) {
switch (gc_type) {
case v8::GCType::kGCTypeScavenge:
return "Scavenge";
case v8::GCType::kGCTypeMarkSweepCompact:
return "MarkSweepCompact";
case v8::GCType::kGCTypeIncrementalMarking:
return "IncrementalMarking";
case v8::GCType::kGCTypeProcessWeakCallbacks:
return "ProcessWeakCallbacks";
default:
return "UnKnow";
}
}

static void SetHeapStatistics(JSONWriter* writer, Isolate * isolate) {
HeapStatistics heap_statistics;
isolate->GetHeapStatistics(&heap_statistics);
writer->json_objectstart("heapStatistics");
writer->json_keyvalue("totalHeapSize", heap_statistics.total_heap_size());
writer->json_keyvalue("totalHeapSizeExecutable",
heap_statistics.total_heap_size_executable());
writer->json_keyvalue("totalPhysicalSize",
heap_statistics.total_physical_size());
writer->json_keyvalue("totalAvailableSize",
heap_statistics.total_available_size());
writer->json_keyvalue("totalGlobalHandlesSize",
heap_statistics.total_global_handles_size());
writer->json_keyvalue("usedGlobalHandlesSize",
heap_statistics.used_global_handles_size());
writer->json_keyvalue("usedHeapSize",
heap_statistics.used_heap_size());
writer->json_keyvalue("heapSizeLimit",
heap_statistics.heap_size_limit());
writer->json_keyvalue("mallocedMemory",
heap_statistics.malloced_memory());
writer->json_keyvalue("externalMemory",
heap_statistics.external_memory());
writer->json_keyvalue("peakMallocedMemory",
heap_statistics.peak_malloced_memory());
writer->json_objectend();

int space_count = isolate->NumberOfHeapSpaces();
writer->json_arraystart("heapSpaceStatistics");
for (int i = 0; i < space_count; i++) {
HeapSpaceStatistics heap_space_statistics;
isolate->GetHeapSpaceStatistics(&heap_space_statistics, i);
writer->json_start();
writer->json_keyvalue("spaceName",
heap_space_statistics.space_name());
writer->json_keyvalue("spaceSize",
heap_space_statistics.space_size());
writer->json_keyvalue("spaceUsedSize",
heap_space_statistics.space_used_size());
writer->json_keyvalue("spaceAvailableSize",
heap_space_statistics.space_available_size());
writer->json_keyvalue("physicalSpaceSize",
heap_space_statistics.physical_space_size());
writer->json_end();
}
writer->json_arrayend();
}

static void BeforeGCCallback(Isolate* isolate,
v8::GCType gc_type,
v8::GCCallbackFlags flags,
void* data) {
GCProfiler* profiler = static_cast<GCProfiler *>(data);
if (profiler->current_gc_type() != 0) {
return;
}
profiler->set_current_gc_type(gc_type);
profiler->set_start_time(uv_hrtime());
JSONWriter* writer = profiler->writer();
writer->json_start();
writer->json_keyvalue("gcType", GetGCTypeName(gc_type));
writer->json_objectstart("beforeGC");
SetHeapStatistics(writer, isolate);
writer->json_objectend();
}

static void AfterGCCallback(Isolate* isolate,
v8::GCType gc_type,
v8::GCCallbackFlags flags,
void* data) {
GCProfiler* profiler = static_cast<GCProfiler *>(data);
if (profiler->current_gc_type() != gc_type) {
return;
}
JSONWriter* writer = profiler->writer();
profiler->set_current_gc_type(0);
u_int64_t start_time = profiler->start_time();
profiler->set_start_time(0);
writer->json_keyvalue("cost", (uv_hrtime() - start_time) / 1e3);
writer->json_objectstart("afterGC");
SetHeapStatistics(writer, isolate);
writer->json_objectend();
writer->json_end();
}

GCProfiler::GCProfiler(Environment* env, Local<Object> object):
BaseObject(env, object), writer_(outfile_, false) {
MakeWeak();
}

// This function will be called when
// 1. StartGCProfile and StopGCProfile are called and
// JS land do not keep the object any more.
// 2. StartGCProfile is called then the env exits before
// StopGCProfile is called.
GCProfiler::~GCProfiler() {
if (state_ != GCProfilerState::kInitialized) {
env()->isolate()->RemoveGCPrologueCallback(BeforeGCCallback, this);
env()->isolate()->RemoveGCEpilogueCallback(AfterGCCallback, this);
}
}

void GCProfiler::New(const FunctionCallbackInfo<Value>& args) {
CHECK(args.IsConstructCall());
Environment* env = Environment::GetCurrent(args);
new GCProfiler(env, args.This());
}

void GCProfiler::StartGCProfile(const FunctionCallbackInfo<Value>& args) {
GCProfiler* profiler;
ASSIGN_OR_RETURN_UNWRAP(&profiler, args.Holder());
if (profiler->state() != GCProfilerState::kInitialized) {
return;
}
Environment* env = Environment::GetCurrent(args);
Isolate* isolate = args.GetIsolate();
node::Utf8Value filename(env->isolate(), args[0]);
profiler->outfile()->open(*filename, std::ios::out | std::ios::binary);
if (!profiler->outfile()->is_open()) {
env->ThrowError("failed to open file");
return;
}
profiler->writer()->json_start();
profiler->writer()->json_keyvalue("version", 1);

uv_timeval64_t ts;
if (uv_gettimeofday(&ts) == 0) {
profiler->writer()->json_keyvalue("startTime",
ts.tv_sec * 1000 + ts.tv_usec / 1000);
} else {
profiler->writer()->json_keyvalue("startTime", 0);
}
profiler->writer()->json_arraystart("statistics");
isolate->AddGCPrologueCallback(BeforeGCCallback,
static_cast<void *>(profiler));
isolate->AddGCEpilogueCallback(AfterGCCallback,
static_cast<void *>(profiler));
profiler->set_state(GCProfilerState::kStarted);
}

void GCProfiler::StopGCProfile(const FunctionCallbackInfo<v8::Value>& args) {
GCProfiler* profiler;
ASSIGN_OR_RETURN_UNWRAP(&profiler, args.Holder());
if (profiler->state() != GCProfilerState::kStarted) {
return;
}
profiler->writer()->json_arrayend();
uv_timeval64_t ts;
if (uv_gettimeofday(&ts) == 0) {
profiler->writer()->json_keyvalue("endtTime",
ts.tv_sec * 1000 + ts.tv_usec / 1000);
} else {
profiler->writer()->json_keyvalue("endtTime", 0);
}
profiler->writer()->json_end();
profiler->outfile()->close();
profiler->set_state(GCProfilerState::kStopped);
}

void Initialize(Local<Object> target,
Local<Value> unused,
Local<Context> context,
Expand Down Expand Up @@ -272,6 +447,17 @@ void Initialize(Local<Object> target,

// Export symbols used by v8.setFlagsFromString()
SetMethod(context, target, "setFlagsFromString", SetFlagsFromString);

// GCProfiler
Local<FunctionTemplate> t = NewFunctionTemplate(env->isolate(),
GCProfiler::New);
t->InstanceTemplate()->SetInternalFieldCount(
BaseObject::kInternalFieldCount);
SetProtoMethod(env->isolate(), t, "startGCProfile",
GCProfiler::StartGCProfile);
SetProtoMethod(env->isolate(), t, "stopGCProfile",
GCProfiler::StopGCProfile);
SetConstructorFunction(context, target, "GCProfiler", t);
}

void RegisterExternalReferences(ExternalReferenceRegistry* registry) {
Expand All @@ -281,6 +467,8 @@ void RegisterExternalReferences(ExternalReferenceRegistry* registry) {
registry->Register(UpdateHeapSpaceStatisticsBuffer);
registry->Register(SetFlagsFromString);
registry->Register(SetHeapSnapshotNearHeapLimit);
registry->Register(GCProfiler::StartGCProfile);
registry->Register(GCProfiler::StopGCProfile);
}

} // namespace v8_utils
Expand Down
Loading

0 comments on commit c54c984

Please sign in to comment.