src: parse inspector profiles with simdjson

This allows us to start the profilers before context creation
so that more samples can be collected.

PR-URL: https://github.com/nodejs/node/pull/51783
Reviewed-By: Daniel Lemire <daniel@lemire.me>
Reviewed-By: Chengzhong Wu <legendecas@gmail.com>
This commit is contained in:
Joyee Cheung 2024-02-16 04:45:37 +01:00
parent a30ae50860
commit a6b80c7267
3 changed files with 175 additions and 119 deletions

View File

@ -1257,6 +1257,8 @@
'deps/histogram/histogram.gyp:histogram',
'deps/uvwasi/uvwasi.gyp:uvwasi',
'deps/ada/ada.gyp:ada',
'deps/simdjson/simdjson.gyp:simdjson',
'deps/simdutf/simdutf.gyp:simdutf',
],
'includes': [

View File

@ -11,7 +11,9 @@
#include "v8-inspector.h"
#include <cinttypes>
#include <limits>
#include <sstream>
#include "simdutf.h"
namespace node {
namespace profiler {
@ -23,7 +25,6 @@ using v8::FunctionCallbackInfo;
using v8::HandleScope;
using v8::Isolate;
using v8::Local;
using v8::MaybeLocal;
using v8::NewStringType;
using v8::Object;
using v8::String;
@ -38,11 +39,15 @@ V8ProfilerConnection::V8ProfilerConnection(Environment* env)
false)),
env_(env) {}
uint32_t V8ProfilerConnection::DispatchMessage(const char* method,
uint64_t V8ProfilerConnection::DispatchMessage(const char* method,
const char* params,
bool is_profile_request) {
std::stringstream ss;
uint32_t id = next_id();
uint64_t id = next_id();
// V8's inspector protocol cannot take an integer beyond the int32_t limit.
// In practice the id we use is up to 3-5 for the profilers we have
// here.
CHECK_LT(id, static_cast<uint64_t>(std::numeric_limits<int32_t>::max()));
ss << R"({ "id": )" << id;
DCHECK(method != nullptr);
ss << R"(, "method": ")" << method << '"';
@ -67,8 +72,10 @@ uint32_t V8ProfilerConnection::DispatchMessage(const char* method,
static void WriteResult(Environment* env,
const char* path,
Local<String> result) {
int ret = WriteFileSync(env->isolate(), path, result);
std::string_view profile) {
uv_buf_t buf =
uv_buf_init(const_cast<char*>(profile.data()), profile.length());
int ret = WriteFileSync(path, buf);
if (ret != 0) {
char err_buf[128];
uv_err_name_r(ret, err_buf, sizeof(err_buf));
@ -78,6 +85,29 @@ static void WriteResult(Environment* env,
Debug(env, DebugCategory::INSPECTOR_PROFILER, "Written result to %s\n", path);
}
bool StringViewToUTF8(const v8_inspector::StringView& source,
std::vector<char>* utf8_out,
size_t* utf8_length,
size_t padding) {
size_t source_len = source.length();
if (source.is8Bit()) {
const char* latin1 = reinterpret_cast<const char*>(source.characters8());
*utf8_length = simdutf::utf8_length_from_latin1(latin1, source_len);
utf8_out->resize(*utf8_length + padding);
size_t result_len =
simdutf::convert_latin1_to_utf8(latin1, source_len, utf8_out->data());
return *utf8_length == result_len;
}
const char16_t* utf16 =
reinterpret_cast<const char16_t*>(source.characters16());
*utf8_length = simdutf::utf8_length_from_utf16(utf16, source_len);
utf8_out->resize(*utf8_length + padding);
size_t result_len =
simdutf::convert_utf16_to_utf8(utf16, source_len, utf8_out->data());
return *utf8_length == result_len;
}
void V8ProfilerConnection::V8ProfilerSessionDelegate::SendMessageToFrontend(
const v8_inspector::StringView& message) {
Environment* env = connection_->env();
@ -85,70 +115,75 @@ void V8ProfilerConnection::V8ProfilerSessionDelegate::SendMessageToFrontend(
HandleScope handle_scope(isolate);
Local<Context> context = env->context();
Context::Scope context_scope(context);
const char* type = connection_->type();
// 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 to V8 string\n", type);
return;
}
Debug(env,
DebugCategory::INSPECTOR_PROFILER,
"Receive %s profile message\n",
"Received %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);
std::vector<char> message_utf8;
size_t message_utf8_length;
if (!StringViewToUTF8(message,
&message_utf8,
&message_utf8_length,
simdjson::SIMDJSON_PADDING)) {
fprintf(
stderr, "Failed to convert %s profile message to UTF8 string\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);
simdjson::ondemand::document parsed;
simdjson::ondemand::object response;
if (connection_->json_parser_
.iterate(
message_utf8.data(), message_utf8_length, message_utf8.size())
.get(parsed) ||
parsed.get_object().get(response)) {
fprintf(
stderr, "Cannot retrieve id from the response message:\n%s\n", *str);
stderr, "Failed to parse %s profile result as JSON object:\n", type);
fprintf(stderr,
"%.*s\n",
static_cast<int>(message_utf8_length),
message_utf8.data());
return;
}
uint64_t id;
if (response["id"].get_uint64().get(id)) {
fprintf(stderr, "Cannot retrieve id from %s profile response:\n", type);
fprintf(stderr,
"%.*s\n",
static_cast<int>(message_utf8_length),
message_utf8.data());
return;
}
uint32_t id = id_v.As<v8::Uint32>()->Value();
if (!connection_->HasProfileId(id)) {
Utf8Value str(isolate, message_str);
Debug(env, DebugCategory::INSPECTOR_PROFILER, "%s\n", *str);
Debug(env,
DebugCategory::INSPECTOR_PROFILER,
"%s\n",
std::string_view(message_utf8.data(), message_utf8_length));
return;
} else {
Debug(env,
DebugCategory::INSPECTOR_PROFILER,
"Writing profile response (id = %" PRIu64 ")\n",
static_cast<uint64_t>(id));
id);
}
simdjson::ondemand::object result;
// 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);
if (response["result"].get_object().get(result)) {
fprintf(stderr, "Failed to get 'result' from %s profile response:\n", type);
fprintf(stderr,
"%.*s\n",
static_cast<int>(message_utf8_length),
message_utf8.data());
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_->WriteProfile(&result);
connection_->RemoveProfileId(id);
}
@ -178,20 +213,31 @@ std::string V8CoverageConnection::GetFilename() const {
env()->thread_id());
}
void V8ProfilerConnection::WriteProfile(Local<Object> result) {
Local<Context> context = env_->context();
std::optional<std::string_view> V8ProfilerConnection::GetProfile(
simdjson::ondemand::object* result) {
simdjson::ondemand::object profile_object;
if ((*result)["profile"].get_object().get(profile_object)) {
fprintf(
stderr, "'profile' from %s profile result is not an Object\n", type());
return std::nullopt;
}
std::string_view profile_raw;
if (profile_object.raw_json().get(profile_raw)) {
fprintf(stderr,
"Cannot get raw string of the 'profile' field from %s profile\n",
type());
return std::nullopt;
}
return profile_raw;
}
void V8ProfilerConnection::WriteProfile(simdjson::ondemand::object* result) {
// Generate the profile output from the subclass.
Local<Object> profile;
if (!GetProfile(result).ToLocal(&profile)) {
return;
}
Local<String> result_s;
if (!v8::JSON::Stringify(context, profile).ToLocal(&result_s)) {
fprintf(stderr, "Failed to stringify %s profile result\n", type());
auto profile_opt = GetProfile(result);
if (!profile_opt.has_value()) {
return;
}
std::string_view profile = profile_opt.value();
// Create the directory if necessary.
std::string directory = GetDirectory();
@ -204,14 +250,12 @@ void V8ProfilerConnection::WriteProfile(Local<Object> result) {
DCHECK(!filename.empty());
std::string path = directory + kPathSeparator + filename;
WriteResult(env_, path.c_str(), result_s);
WriteResult(env_, path.c_str(), profile);
}
void V8CoverageConnection::WriteProfile(Local<Object> result) {
void V8CoverageConnection::WriteProfile(simdjson::ondemand::object* result) {
Isolate* isolate = env_->isolate();
Local<Context> context = env_->context();
HandleScope handle_scope(isolate);
Context::Scope context_scope(context);
// This is only set up during pre-execution (when the environment variables
// becomes available in the JS land). If it's empty, we don't have coverage
@ -223,11 +267,15 @@ void V8CoverageConnection::WriteProfile(Local<Object> result) {
return;
}
Local<Context> context = env_->context();
Context::Scope context_scope(context);
// Generate the profile output from the subclass.
Local<Object> profile;
if (!GetProfile(result).ToLocal(&profile)) {
auto profile_opt = GetProfile(result);
if (!profile_opt.has_value()) {
return;
}
std::string_view profile = profile_opt.value();
// append source-map cache information to coverage object:
Local<Value> source_map_cache_v;
@ -246,17 +294,6 @@ void V8CoverageConnection::WriteProfile(Local<Object> result) {
PrintCaughtException(isolate, context, try_catch);
}
}
// Avoid writing to disk if no source-map data:
if (!source_map_cache_v->IsUndefined()) {
profile->Set(context, FIXED_ONE_BYTE_STRING(isolate, "source-map-cache"),
source_map_cache_v).ToChecked();
}
Local<String> result_s;
if (!v8::JSON::Stringify(context, profile).ToLocal(&result_s)) {
fprintf(stderr, "Failed to stringify %s profile result\n", type());
return;
}
// Create the directory if necessary.
std::string directory = GetDirectory();
@ -269,11 +306,58 @@ void V8CoverageConnection::WriteProfile(Local<Object> result) {
DCHECK(!filename.empty());
std::string path = directory + kPathSeparator + filename;
WriteResult(env_, path.c_str(), result_s);
// Only insert source map cache when there's source map data at all.
if (!source_map_cache_v->IsUndefined()) {
// It would be more performant to just find the last } and insert the source
// map cache in front of it, but source map cache is still experimental
// anyway so just re-parse it with V8 for now.
Local<String> profile_str;
if (!v8::String::NewFromUtf8(isolate,
profile.data(),
v8::NewStringType::kNormal,
profile.length())
.ToLocal(&profile_str)) {
fprintf(stderr, "Failed to re-parse %s profile as UTF8\n", type());
return;
}
Local<Value> profile_value;
if (!v8::JSON::Parse(context, profile_str).ToLocal(&profile_value) ||
!profile_value->IsObject()) {
fprintf(stderr, "Failed to re-parse %s profile from JSON\n", type());
return;
}
if (profile_value.As<Object>()
->Set(context,
FIXED_ONE_BYTE_STRING(isolate, "source-map-cache"),
source_map_cache_v)
.IsNothing()) {
fprintf(stderr,
"Failed to insert source map cache into %s profile\n",
type());
return;
}
Local<String> result_s;
if (!v8::JSON::Stringify(context, profile_value).ToLocal(&result_s)) {
fprintf(stderr, "Failed to stringify %s profile result\n", type());
return;
}
Utf8Value result_utf8(isolate, result_s);
WriteResult(env_, path.c_str(), result_utf8.ToStringView());
} else {
WriteResult(env_, path.c_str(), profile);
}
}
MaybeLocal<Object> V8CoverageConnection::GetProfile(Local<Object> result) {
return result;
std::optional<std::string_view> V8CoverageConnection::GetProfile(
simdjson::ondemand::object* result) {
std::string_view profile_raw;
if (result->raw_json().get(profile_raw)) {
fprintf(stderr,
"Cannot get raw string of the 'profile' field from %s profile\n",
type());
return std::nullopt;
}
return profile_raw;
}
std::string V8CoverageConnection::GetDirectory() const {
@ -313,22 +397,6 @@ std::string V8CpuProfilerConnection::GetFilename() const {
return env()->cpu_prof_name();
}
MaybeLocal<Object> V8CpuProfilerConnection::GetProfile(Local<Object> result) {
Local<Value> profile_v;
if (!result
->Get(env()->context(),
FIXED_ONE_BYTE_STRING(env()->isolate(), "profile"))
.ToLocal(&profile_v)) {
fprintf(stderr, "'profile' from CPU profile result is undefined\n");
return MaybeLocal<Object>();
}
if (!profile_v->IsObject()) {
fprintf(stderr, "'profile' from CPU profile result is not an Object\n");
return MaybeLocal<Object>();
}
return profile_v.As<Object>();
}
void V8CpuProfilerConnection::Start() {
DispatchMessage("Profiler.enable");
std::string params = R"({ "interval": )";
@ -357,22 +425,6 @@ std::string V8HeapProfilerConnection::GetFilename() const {
return env()->heap_prof_name();
}
MaybeLocal<Object> V8HeapProfilerConnection::GetProfile(Local<Object> result) {
Local<Value> profile_v;
if (!result
->Get(env()->context(),
FIXED_ONE_BYTE_STRING(env()->isolate(), "profile"))
.ToLocal(&profile_v)) {
fprintf(stderr, "'profile' from heap profile result is undefined\n");
return MaybeLocal<Object>();
}
if (!profile_v->IsObject()) {
fprintf(stderr, "'profile' from heap profile result is not an Object\n");
return MaybeLocal<Object>();
}
return profile_v.As<Object>();
}
void V8HeapProfilerConnection::Start() {
DispatchMessage("HeapProfiler.enable");
std::string params = R"({ "samplingInterval": )";

View File

@ -7,8 +7,10 @@
#error("This header can only be used when inspector is enabled")
#endif
#include <optional>
#include <unordered_set>
#include "inspector_agent.h"
#include "simdjson.h"
namespace node {
// Forward declaration to break recursive dependency chain with src/env.h.
@ -40,7 +42,7 @@ class V8ProfilerConnection {
// The optional `params` should be formatted in JSON.
// The strings should be in one byte characters - which is enough for
// the commands we use here.
uint32_t DispatchMessage(const char* method,
uint64_t DispatchMessage(const char* method,
const char* params = nullptr,
bool is_profile_request = false);
@ -59,23 +61,24 @@ class V8ProfilerConnection {
virtual std::string GetFilename() const = 0;
// Return the profile object parsed from `message.result`,
// which will be then written as a JSON.
virtual v8::MaybeLocal<v8::Object> GetProfile(
v8::Local<v8::Object> result) = 0;
virtual void WriteProfile(v8::Local<v8::Object> result);
virtual std::optional<std::string_view> GetProfile(
simdjson::ondemand::object* result);
virtual void WriteProfile(simdjson::ondemand::object* result);
bool HasProfileId(uint32_t id) const {
bool HasProfileId(uint64_t id) const {
return profile_ids_.find(id) != profile_ids_.end();
}
void RemoveProfileId(uint32_t id) { profile_ids_.erase(id); }
void RemoveProfileId(uint64_t id) { profile_ids_.erase(id); }
private:
uint32_t next_id() { return id_++; }
uint64_t next_id() { return id_++; }
std::unique_ptr<inspector::InspectorSession> session_;
uint32_t id_ = 1;
std::unordered_set<uint32_t> profile_ids_;
uint64_t id_ = 1;
std::unordered_set<uint64_t> profile_ids_;
protected:
simdjson::ondemand::parser json_parser_;
Environment* env_ = nullptr;
};
@ -91,8 +94,9 @@ class V8CoverageConnection : public V8ProfilerConnection {
std::string GetDirectory() const override;
std::string GetFilename() const override;
v8::MaybeLocal<v8::Object> GetProfile(v8::Local<v8::Object> result) override;
void WriteProfile(v8::Local<v8::Object> result) override;
std::optional<std::string_view> GetProfile(
simdjson::ondemand::object* result) override;
void WriteProfile(simdjson::ondemand::object* result) override;
void WriteSourceMapCache();
void TakeCoverage();
void StopCoverage();
@ -115,7 +119,6 @@ class V8CpuProfilerConnection : public V8ProfilerConnection {
std::string GetDirectory() const override;
std::string GetFilename() const override;
v8::MaybeLocal<v8::Object> GetProfile(v8::Local<v8::Object> result) override;
private:
std::unique_ptr<inspector::InspectorSession> session_;
@ -135,7 +138,6 @@ class V8HeapProfilerConnection : public V8ProfilerConnection {
std::string GetDirectory() const override;
std::string GetFilename() const override;
v8::MaybeLocal<v8::Object> GetProfile(v8::Local<v8::Object> result) override;
private:
std::unique_ptr<inspector::InspectorSession> session_;