From a56faff4d0a56f8602e41b1b2de1f998479b7c47 Mon Sep 17 00:00:00 2001 From: Joyee Cheung Date: Fri, 16 Feb 2024 04:45:37 +0100 Subject: [PATCH] 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 Reviewed-By: Chengzhong Wu --- node.gyp | 2 + src/inspector_profiler.cc | 264 +++++++++++++++++++++++--------------- src/inspector_profiler.h | 28 ++-- 3 files changed, 175 insertions(+), 119 deletions(-) diff --git a/node.gyp b/node.gyp index 88ce9e7026554c..bddbdc5a24c9ff 100644 --- a/node.gyp +++ b/node.gyp @@ -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': [ diff --git a/src/inspector_profiler.cc b/src/inspector_profiler.cc index aeebc1f18a026d..07629a9285019a 100644 --- a/src/inspector_profiler.cc +++ b/src/inspector_profiler.cc @@ -11,7 +11,9 @@ #include "v8-inspector.h" #include +#include #include +#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(std::numeric_limits::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 result) { - int ret = WriteFileSync(env->isolate(), path, result); + std::string_view profile) { + uv_buf_t buf = + uv_buf_init(const_cast(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* utf8_out, + size_t* utf8_length, + size_t padding) { + size_t source_len = source.length(); + if (source.is8Bit()) { + const char* latin1 = reinterpret_cast(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(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 = env->context(); Context::Scope context_scope(context); - const char* type = connection_->type(); - // Convert StringView to a Local. - Local 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 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 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 response = parsed.As(); - Local 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(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(message_utf8_length), + message_utf8.data()); return; } - uint32_t id = id_v.As()->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(id)); + id); } + simdjson::ondemand::object result; // Get message.result from the response. - Local 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); + 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(message_utf8_length), + message_utf8.data()); return; } - connection_->WriteProfile(result_v.As()); + connection_->WriteProfile(&result); connection_->RemoveProfileId(id); } @@ -178,20 +213,31 @@ std::string V8CoverageConnection::GetFilename() const { env()->thread_id()); } -void V8ProfilerConnection::WriteProfile(Local result) { - Local context = env_->context(); - - // Generate the profile output from the subclass. - Local profile; - if (!GetProfile(result).ToLocal(&profile)) { - return; +std::optional 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; +} - Local result_s; - if (!v8::JSON::Stringify(context, profile).ToLocal(&result_s)) { - fprintf(stderr, "Failed to stringify %s profile result\n", type()); +void V8ProfilerConnection::WriteProfile(simdjson::ondemand::object* result) { + // Generate the profile output from the subclass. + 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 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 result) { +void V8CoverageConnection::WriteProfile(simdjson::ondemand::object* result) { Isolate* isolate = env_->isolate(); - Local 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 result) { return; } + Local context = env_->context(); + Context::Scope context_scope(context); + // Generate the profile output from the subclass. - Local 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 source_map_cache_v; @@ -246,17 +294,6 @@ void V8CoverageConnection::WriteProfile(Local 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 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 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 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 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() + ->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 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 V8CoverageConnection::GetProfile(Local result) { - return result; +std::optional 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 V8CpuProfilerConnection::GetProfile(Local result) { - Local 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(); - } - if (!profile_v->IsObject()) { - fprintf(stderr, "'profile' from CPU profile result is not an Object\n"); - return MaybeLocal(); - } - return profile_v.As(); -} - 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 V8HeapProfilerConnection::GetProfile(Local result) { - Local 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(); - } - if (!profile_v->IsObject()) { - fprintf(stderr, "'profile' from heap profile result is not an Object\n"); - return MaybeLocal(); - } - return profile_v.As(); -} - void V8HeapProfilerConnection::Start() { DispatchMessage("HeapProfiler.enable"); std::string params = R"({ "samplingInterval": )"; diff --git a/src/inspector_profiler.h b/src/inspector_profiler.h index be74153b0c37ac..fd741c1f1ff659 100644 --- a/src/inspector_profiler.h +++ b/src/inspector_profiler.h @@ -7,8 +7,10 @@ #error("This header can only be used when inspector is enabled") #endif +#include #include #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 GetProfile( - v8::Local result) = 0; - virtual void WriteProfile(v8::Local result); + virtual std::optional 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 session_; - uint32_t id_ = 1; - std::unordered_set profile_ids_; + uint64_t id_ = 1; + std::unordered_set 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 GetProfile(v8::Local result) override; - void WriteProfile(v8::Local result) override; + std::optional 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 GetProfile(v8::Local result) override; private: std::unique_ptr session_; @@ -135,7 +138,6 @@ class V8HeapProfilerConnection : public V8ProfilerConnection { std::string GetDirectory() const override; std::string GetFilename() const override; - v8::MaybeLocal GetProfile(v8::Local result) override; private: std::unique_ptr session_;