Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

v8: implement v8.takeCoverage() and v8.stopCoverage() #33807

Closed
wants to merge 2 commits into from
Closed
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
28 changes: 28 additions & 0 deletions doc/api/v8.md
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,32 @@ v8.setFlagsFromString('--trace_gc');
setTimeout(() => { v8.setFlagsFromString('--notrace_gc'); }, 60e3);
```

## `v8.takeCoverage()`
joyeecheung marked this conversation as resolved.
Show resolved Hide resolved

<!-- YAML
added: REPLACEME
-->

The `v8.takeCoverage()` method allows the user to write the coverage started by
[`NODE_V8_COVERAGE`][] to disk on demand. This method can be invoked multiple
times during the lifetime of the process, each time the execution counter will
be reset and a new coverage report will be written to the directory specified
by [`NODE_V8_COVERAGE`][].

When the process is about to exit, one last coverage will still be written to
disk, unless [`v8.stopCoverage()`][] is invoked before the process exits.

## `v8.stopCoverage()`

<!-- YAML
added: REPLACEME
-->

The `v8.stopCoverage()` method allows the user to stop the coverage collection
started by [`NODE_V8_COVERAGE`][], so that V8 can release the execution count
records and optimize code. This can be used in conjunction with
`v8.takeCoverage()` if the user wants to collect the coverage on demand.

## `v8.writeHeapSnapshot([filename])`
<!-- YAML
added: v11.13.0
Expand Down Expand Up @@ -511,6 +537,7 @@ A subclass of [`Deserializer`][] corresponding to the format written by
[`Deserializer`]: #v8_class_v8_deserializer
[`Error`]: errors.md#errors_class_error
[`GetHeapSpaceStatistics`]: https://v8docs.nodesource.com/node-13.2/d5/dda/classv8_1_1_isolate.html#ac673576f24fdc7a33378f8f57e1d13a4
[`NODE_V8_COVERAGE`]: cli.md#cli_node_v8_coverage_dir
[`Serializer`]: #v8_class_v8_serializer
[`deserializer._readHostObject()`]: #v8_deserializer_readhostobject
[`deserializer.transferArrayBuffer()`]: #v8_deserializer_transferarraybuffer_id_arraybuffer
Expand All @@ -520,5 +547,6 @@ A subclass of [`Deserializer`][] corresponding to the format written by
[`serializer.releaseBuffer()`]: #v8_serializer_releasebuffer
[`serializer.transferArrayBuffer()`]: #v8_serializer_transferarraybuffer_id_arraybuffer
[`serializer.writeRawBytes()`]: #v8_serializer_writerawbytes_buffer
[`v8.stopCoverage()`]: #v8_v8_stopcoverage
[`vm.Script`]: vm.md#vm_new_vm_script_code_options
[worker threads]: worker_threads.md
8 changes: 8 additions & 0 deletions lib/v8.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,12 @@ const {
Serializer: _Serializer,
Deserializer: _Deserializer
} = internalBinding('serdes');

let profiler = {};
if (internalBinding('config').hasInspector) {
profiler = internalBinding('profiler');
}

const assert = require('internal/assert');
const { copy } = internalBinding('buffer');
const { inspect } = require('internal/util/inspect');
Expand Down Expand Up @@ -275,6 +281,8 @@ module.exports = {
DefaultSerializer,
DefaultDeserializer,
deserialize,
takeCoverage: profiler.takeCoverage,
stopCoverage: profiler.stopCoverage,
serialize,
writeHeapSnapshot,
};
207 changes: 132 additions & 75 deletions src/inspector_profiler.cc
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
#include "util-inl.h"
#include "v8-inspector.h"

#include <cinttypes>
#include <sstream>

namespace node {
Expand Down Expand Up @@ -36,10 +37,11 @@ V8ProfilerConnection::V8ProfilerConnection(Environment* env)
false)),
env_(env) {}

size_t V8ProfilerConnection::DispatchMessage(const char* method,
const char* params) {
uint32_t V8ProfilerConnection::DispatchMessage(const char* method,
const char* params,
bool is_profile_request) {
std::stringstream ss;
size_t id = next_id();
uint32_t id = next_id();
ss << R"({ "id": )" << id;
DCHECK(method != nullptr);
ss << R"(, "method": ")" << method << '"';
Expand All @@ -50,12 +52,15 @@ size_t V8ProfilerConnection::DispatchMessage(const char* method,
std::string message = ss.str();
const uint8_t* message_data =
reinterpret_cast<const uint8_t*>(message.c_str());
// Save the id of the profile request to identify its response.
if (is_profile_request) {
profile_ids_.insert(id);
}
Debug(env(),
DebugCategory::INSPECTOR_PROFILER,
"Dispatching message %s\n",
message.c_str());
session_->Dispatch(StringView(message_data, message.length()));
// TODO(joyeecheung): use this to identify the ending message.
return id;
}

Expand All @@ -77,33 +82,73 @@ void V8ProfilerConnection::V8ProfilerSessionDelegate::SendMessageToFrontend(
Environment* env = connection_->env();
Isolate* isolate = env->isolate();
HandleScope handle_scope(isolate);
Context::Scope context_scope(env->context());
Local<Context> context = env->context();
Context::Scope context_scope(context);

// TODO(joyeecheung): always parse the message so that we can use the id to
// identify ending messages as well as printing the message in the debug
// output when there is an error.
const char* type = connection_->type();
Debug(env,
DebugCategory::INSPECTOR_PROFILER,
"Receive %s profile message, ending = %s\n",
type,
connection_->ending() ? "true" : "false");
if (!connection_->ending()) {
return;
}

// Convert StringView to a Local<String>.
Local<String> message_str;
if (!String::NewFromTwoByte(isolate,
message.characters16(),
NewStringType::kNormal,
message.length())
.ToLocal(&message_str)) {
fprintf(stderr, "Failed to convert %s profile message\n", type);
fprintf(
Copy link
Member

Choose a reason for hiding this comment

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

it would be nice if this was available as an error callback or something, since the calling code can't really read stderr

Copy link
Member

Choose a reason for hiding this comment

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

The only reason I could see it failing is when message.length() > kStringMaxLength which:

  1. I don't think can realistically happen, and
  2. Isn't actionable for users when it does (except by opening a bug report here)

Maybe the error message could include the size and perhaps the first N bytes of the message.

Similar reasoning applies to the error code paths below. Failure likely means we need to fix something on our end.

stderr, "Failed to convert %s profile message to V8 string\n", type);
return;
}

Debug(env,
DebugCategory::INSPECTOR_PROFILER,
"Receive %s profile message\n",
type);

Local<Value> parsed;
if (!v8::JSON::Parse(context, message_str).ToLocal(&parsed) ||
!parsed->IsObject()) {
fprintf(stderr, "Failed to parse %s profile result as JSON object\n", type);
return;
}

Local<Object> response = parsed.As<Object>();
Local<Value> id_v;
if (!response->Get(context, FIXED_ONE_BYTE_STRING(isolate, "id"))
.ToLocal(&id_v) ||
!id_v->IsUint32()) {
Utf8Value str(isolate, message_str);
fprintf(
stderr, "Cannot retrieve id from the response message:\n%s\n", *str);
return;
}
uint32_t id = id_v.As<v8::Uint32>()->Value();

connection_->WriteProfile(message_str);
if (!connection_->HasProfileId(id)) {
Utf8Value str(isolate, message_str);
Debug(env, DebugCategory::INSPECTOR_PROFILER, "%s\n", *str);
return;
} else {
Debug(env,
DebugCategory::INSPECTOR_PROFILER,
"Writing profile response (id = %" PRIu64 ")\n",
static_cast<uint64_t>(id));
}

// Get message.result from the response.
Local<Value> result_v;
if (!response->Get(context, FIXED_ONE_BYTE_STRING(isolate, "result"))
.ToLocal(&result_v)) {
fprintf(stderr, "Failed to get 'result' from %s profile response\n", type);
return;
}

if (!result_v->IsObject()) {
fprintf(
stderr, "'result' from %s profile response is not an object\n", type);
return;
}

connection_->WriteProfile(result_v.As<Object>());
connection_->RemoveProfileId(id);
}

static bool EnsureDirectory(const std::string& directory, const char* type) {
Expand Down Expand Up @@ -138,45 +183,9 @@ std::string V8CoverageConnection::GetFilename() const {
return filename;
}

static MaybeLocal<Object> ParseProfile(Environment* env,
Local<String> message,
const char* type) {
Local<Context> context = env->context();
Isolate* isolate = env->isolate();

// Get message.result from the response
Local<Value> parsed;
if (!v8::JSON::Parse(context, message).ToLocal(&parsed) ||
!parsed->IsObject()) {
fprintf(stderr, "Failed to parse %s profile result as JSON object\n", type);
return MaybeLocal<Object>();
}

Local<Value> result_v;
if (!parsed.As<Object>()
->Get(context, FIXED_ONE_BYTE_STRING(isolate, "result"))
.ToLocal(&result_v)) {
fprintf(stderr, "Failed to get 'result' from %s profile message\n", type);
return MaybeLocal<Object>();
}

if (!result_v->IsObject()) {
fprintf(
stderr, "'result' from %s profile message is not an object\n", type);
return MaybeLocal<Object>();
}

return result_v.As<Object>();
}

void V8ProfilerConnection::WriteProfile(Local<String> message) {
void V8ProfilerConnection::WriteProfile(Local<Object> result) {
Local<Context> context = env_->context();

// Get message.result from the response.
Local<Object> result;
if (!ParseProfile(env_, message, type()).ToLocal(&result)) {
return;
}
// Generate the profile output from the subclass.
Local<Object> profile;
if (!GetProfile(result).ToLocal(&profile)) {
Expand All @@ -203,7 +212,7 @@ void V8ProfilerConnection::WriteProfile(Local<String> message) {
WriteResult(env_, path.c_str(), result_s);
}

void V8CoverageConnection::WriteProfile(Local<String> message) {
void V8CoverageConnection::WriteProfile(Local<Object> result) {
Isolate* isolate = env_->isolate();
Local<Context> context = env_->context();
HandleScope handle_scope(isolate);
Expand All @@ -219,11 +228,6 @@ void V8CoverageConnection::WriteProfile(Local<String> message) {
return;
}

// Get message.result from the response.
Local<Object> result;
if (!ParseProfile(env_, message, type()).ToLocal(&result)) {
return;
}
// Generate the profile output from the subclass.
Local<Object> profile;
if (!GetProfile(result).ToLocal(&profile)) {
Expand Down Expand Up @@ -287,10 +291,23 @@ void V8CoverageConnection::Start() {
R"({ "callCount": true, "detailed": true })");
}

void V8CoverageConnection::TakeCoverage() {
DispatchMessage("Profiler.takePreciseCoverage", nullptr, true);
}

void V8CoverageConnection::StopCoverage() {
DispatchMessage("Profiler.stopPreciseCoverage");
}

void V8CoverageConnection::End() {
CHECK_EQ(ending_, false);
Debug(env_,
DebugCategory::INSPECTOR_PROFILER,
"V8CoverageConnection::End(), ending = %d\n", ending_);
if (ending_) {
return;
}
ending_ = true;
DispatchMessage("Profiler.takePreciseCoverage");
TakeCoverage();
}

std::string V8CpuProfilerConnection::GetDirectory() const {
Expand Down Expand Up @@ -327,9 +344,14 @@ void V8CpuProfilerConnection::Start() {
}

void V8CpuProfilerConnection::End() {
CHECK_EQ(ending_, false);
Debug(env_,
DebugCategory::INSPECTOR_PROFILER,
"V8CpuProfilerConnection::End(), ending = %d\n", ending_);
if (ending_) {
return;
}
ending_ = true;
DispatchMessage("Profiler.stop");
DispatchMessage("Profiler.stop", nullptr, true);
}

std::string V8HeapProfilerConnection::GetDirectory() const {
Expand Down Expand Up @@ -365,31 +387,33 @@ void V8HeapProfilerConnection::Start() {
}

void V8HeapProfilerConnection::End() {
CHECK_EQ(ending_, false);
Debug(env_,
DebugCategory::INSPECTOR_PROFILER,
"V8HeapProfilerConnection::End(), ending = %d\n", ending_);
if (ending_) {
return;
}
ending_ = true;
DispatchMessage("HeapProfiler.stopSampling");
DispatchMessage("HeapProfiler.stopSampling", nullptr, true);
}

// For now, we only support coverage profiling, but we may add more
// in the future.
static void EndStartedProfilers(Environment* env) {
// TODO(joyeechueng): merge these connections and use one session per env.
Debug(env, DebugCategory::INSPECTOR_PROFILER, "EndStartedProfilers\n");
V8ProfilerConnection* connection = env->cpu_profiler_connection();
if (connection != nullptr && !connection->ending()) {
Debug(env, DebugCategory::INSPECTOR_PROFILER, "Ending cpu profiling\n");
if (connection != nullptr) {
connection->End();
}

connection = env->heap_profiler_connection();
if (connection != nullptr && !connection->ending()) {
Debug(env, DebugCategory::INSPECTOR_PROFILER, "Ending heap profiling\n");
if (connection != nullptr) {
connection->End();
}

connection = env->coverage_connection();
if (connection != nullptr && !connection->ending()) {
Debug(
env, DebugCategory::INSPECTOR_PROFILER, "Ending coverage collection\n");
if (connection != nullptr) {
connection->End();
}
}
Expand Down Expand Up @@ -469,13 +493,46 @@ static void SetSourceMapCacheGetter(const FunctionCallbackInfo<Value>& args) {
env->set_source_map_cache_getter(args[0].As<Function>());
}

static void TakeCoverage(const FunctionCallbackInfo<Value>& args) {
Environment* env = Environment::GetCurrent(args);
V8CoverageConnection* connection = env->coverage_connection();

Debug(
env,
DebugCategory::INSPECTOR_PROFILER,
"TakeCoverage, connection %s nullptr\n",
connection == nullptr ? "==" : "!=");

if (connection != nullptr) {
Debug(env, DebugCategory::INSPECTOR_PROFILER, "taking coverage\n");
connection->TakeCoverage();
}
}

static void StopCoverage(const FunctionCallbackInfo<Value>& args) {
Environment* env = Environment::GetCurrent(args);
V8CoverageConnection* connection = env->coverage_connection();

Debug(env,
DebugCategory::INSPECTOR_PROFILER,
"StopCoverage, connection %s nullptr\n",
connection == nullptr ? "==" : "!=");

if (connection != nullptr) {
Debug(env, DebugCategory::INSPECTOR_PROFILER, "Stopping coverage\n");
connection->StopCoverage();
}
}

static void Initialize(Local<Object> target,
Local<Value> unused,
Local<Context> context,
void* priv) {
Environment* env = Environment::GetCurrent(context);
env->SetMethod(target, "setCoverageDirectory", SetCoverageDirectory);
env->SetMethod(target, "setSourceMapCacheGetter", SetSourceMapCacheGetter);
env->SetMethod(target, "takeCoverage", TakeCoverage);
env->SetMethod(target, "stopCoverage", StopCoverage);
}

} // namespace profiler
Expand Down
Loading