Skip to content

Commit

Permalink
v8: support gc profile
Browse files Browse the repository at this point in the history
PR-URL: #46255
Reviewed-By: Anna Henningsen <anna@addaleax.net>
Reviewed-By: Rafael Gonzaga <rafael.nunu@hotmail.com>
Reviewed-By: Franziska Hinkelmann <franziska.hinkelmann@gmail.com>
  • Loading branch information
theanarkh authored and juanarbol committed Mar 5, 2023
1 parent 63e92ea commit 02632b4
Show file tree
Hide file tree
Showing 8 changed files with 460 additions and 1 deletion.
106 changes: 106 additions & 0 deletions doc/api/v8.md
Original file line number Diff line number Diff line change
Expand Up @@ -1021,6 +1021,112 @@ added: v18.6.0

Returns true if the Node.js instance is run to build a snapshot.

## Class: `v8.GCProfiler`

<!-- YAML
added: REPLACEME
-->

This API collects GC data in current thread.

### `new v8.GCProfiler()`

<!-- YAML
added: REPLACEME
-->

Create a new instance of the `v8.GCProfiler` class.

### `profiler.start()`

<!-- YAML
added: REPLACEME
-->

Start collecting GC data.

### `profiler.stop()`

<!-- YAML
added: REPLACEME
-->

Stop collecting GC data and return a object.The content of object
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
}
]
}
}
],
"endTime": 1674059036865
}
```

Here's an example.

```js
const { GCProfiler } = require('v8');
const profiler = new GCProfiler();
profiler.start();
setTimeout(() => {
console.log(profiler.stop());
}, 1000);
```

[HTML structured clone algorithm]: https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Structured_clone_algorithm
[Hook Callbacks]: #hook-callbacks
[V8]: https://developers.google.com/v8/
Expand Down
22 changes: 21 additions & 1 deletion lib/v8.js
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ const {
const { HeapSnapshotStream } = require('internal/heap_utils');
const promiseHooks = require('internal/promise_hooks');
const { getOptionValue } = require('internal/options');

const { JSONParse } = primordials;
/**
* Generates a snapshot of the current V8 heap
* and writes it to a JSON file.
Expand Down Expand Up @@ -384,6 +384,25 @@ function deserialize(buffer) {
return der.readValue();
}

class GCProfiler {
#profiler = null;

start() {
if (!this.#profiler) {
this.#profiler = new binding.GCProfiler();
this.#profiler.start();
}
}

stop() {
if (this.#profiler) {
const data = this.#profiler.stop();
this.#profiler = null;
return JSONParse(data);
}
}
}

module.exports = {
cachedDataVersionTag,
getHeapSnapshot,
Expand All @@ -403,4 +422,5 @@ module.exports = {
promiseHooks,
startupSnapshot,
setHeapSnapshotNearHeapLimit,
GCProfiler,
};
190 changes: 190 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,184 @@ 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 "Unknown";
}
}

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;
}
JSONWriter* writer = profiler->writer();
writer->json_start();
writer->json_keyvalue("gcType", GetGCTypeName(gc_type));
writer->json_objectstart("beforeGC");
SetHeapStatistics(writer, isolate);
writer->json_objectend();
profiler->current_gc_type = gc_type;
profiler->start_time = uv_hrtime();
}

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->current_gc_type = 0;
writer->json_keyvalue("cost", (uv_hrtime() - profiler->start_time) / 1e3);
profiler->start_time = 0;
writer->json_objectstart("afterGC");
SetHeapStatistics(writer, isolate);
writer->json_objectend();
writer->json_end();
}

GCProfiler::GCProfiler(Environment* env, Local<Object> object)
: BaseObject(env, object),
start_time(0),
current_gc_type(0),
state(GCProfilerState::kInitialized),
writer_(out_stream_, false) {
MakeWeak();
}

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

JSONWriter* GCProfiler::writer() {
return &writer_;
}

std::ostringstream* GCProfiler::out_stream() {
return &out_stream_;
}

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

void GCProfiler::Start(const FunctionCallbackInfo<Value>& args) {
Environment* env = Environment::GetCurrent(args);
GCProfiler* profiler;
ASSIGN_OR_RETURN_UNWRAP(&profiler, args.Holder());
if (profiler->state != GCProfiler::GCProfilerState::kInitialized) {
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");
env->isolate()->AddGCPrologueCallback(BeforeGCCallback,
static_cast<void*>(profiler));
env->isolate()->AddGCEpilogueCallback(AfterGCCallback,
static_cast<void*>(profiler));
profiler->state = GCProfiler::GCProfilerState::kStarted;
}

void GCProfiler::Stop(const FunctionCallbackInfo<v8::Value>& args) {
Environment* env = Environment::GetCurrent(args);
GCProfiler* profiler;
ASSIGN_OR_RETURN_UNWRAP(&profiler, args.Holder());
if (profiler->state != GCProfiler::GCProfilerState::kStarted) {
return;
}
profiler->writer()->json_arrayend();
uv_timeval64_t ts;
if (uv_gettimeofday(&ts) == 0) {
profiler->writer()->json_keyvalue("endTime",
ts.tv_sec * 1000 + ts.tv_usec / 1000);
} else {
profiler->writer()->json_keyvalue("endTime", 0);
}
profiler->writer()->json_end();
profiler->state = GCProfiler::GCProfilerState::kStopped;
auto string = profiler->out_stream()->str();
args.GetReturnValue().Set(String::NewFromUtf8(env->isolate(),
string.data(),
v8::NewStringType::kNormal,
string.size())
.ToLocalChecked());
}

void Initialize(Local<Object> target,
Local<Value> unused,
Local<Context> context,
Expand Down Expand Up @@ -272,6 +451,14 @@ 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, "start", GCProfiler::Start);
SetProtoMethod(env->isolate(), t, "stop", GCProfiler::Stop);
SetConstructorFunction(context, target, "GCProfiler", t);
}

void RegisterExternalReferences(ExternalReferenceRegistry* registry) {
Expand All @@ -281,6 +468,9 @@ void RegisterExternalReferences(ExternalReferenceRegistry* registry) {
registry->Register(UpdateHeapSpaceStatisticsBuffer);
registry->Register(SetFlagsFromString);
registry->Register(SetHeapSnapshotNearHeapLimit);
registry->Register(GCProfiler::New);
registry->Register(GCProfiler::Start);
registry->Register(GCProfiler::Stop);
}

} // namespace v8_utils
Expand Down
Loading

0 comments on commit 02632b4

Please sign in to comment.