#include "inspector_profiler.h" #include "base_object-inl.h" #include "debug_utils.h" #include "node_file.h" #include "node_internals.h" #include "v8-inspector.h" namespace node { namespace profiler { using v8::Context; using v8::Function; using v8::FunctionCallbackInfo; using v8::HandleScope; using v8::Isolate; using v8::Local; using v8::MaybeLocal; using v8::NewStringType; using v8::Object; using v8::String; using v8::Value; using v8_inspector::StringBuffer; using v8_inspector::StringView; #ifdef _WIN32 const char* const kPathSeparator = "\\/"; /* MAX_PATH is in characters, not bytes. Make sure we have enough headroom. */ #define CWD_BUFSIZE (MAX_PATH * 4) #else #include // PATH_MAX on Solaris. const char* const kPathSeparator = "/"; #define CWD_BUFSIZE (PATH_MAX) #endif std::unique_ptr ToProtocolString(Isolate* isolate, Local value) { TwoByteValue buffer(isolate, value); return StringBuffer::create(StringView(*buffer, buffer.length())); } V8ProfilerConnection::V8ProfilerConnection(Environment* env) : session_(env->inspector_agent()->Connect( std::make_unique( this), false)), env_(env) {} void V8ProfilerConnection::DispatchMessage(Local message) { session_->Dispatch(ToProtocolString(env()->isolate(), message)->string()); } bool V8ProfilerConnection::WriteResult(const char* path, Local result) { int ret = WriteFileSync(env()->isolate(), path, result); if (ret != 0) { char err_buf[128]; uv_err_name_r(ret, err_buf, sizeof(err_buf)); fprintf(stderr, "%s: Failed to write file %s\n", err_buf, path); return false; } Debug( env(), DebugCategory::INSPECTOR_PROFILER, "Written result to %s\n", path); return true; } void V8CoverageConnection::OnMessage(const v8_inspector::StringView& message) { Debug(env(), DebugCategory::INSPECTOR_PROFILER, "Receive coverage message, ending = %s\n", ending_ ? "true" : "false"); if (!ending_) { return; } Isolate* isolate = env()->isolate(); Local context = env()->context(); HandleScope handle_scope(isolate); Context::Scope context_scope(context); Local result; if (!String::NewFromTwoByte(isolate, message.characters16(), NewStringType::kNormal, message.length()) .ToLocal(&result)) { fprintf(stderr, "Failed to covert coverage message\n"); } WriteCoverage(result); } bool V8CoverageConnection::WriteCoverage(Local message) { const std::string& directory = env()->coverage_directory(); CHECK(!directory.empty()); uv_fs_t req; int ret = fs::MKDirpSync(nullptr, &req, directory, 0777, nullptr); uv_fs_req_cleanup(&req); if (ret < 0 && ret != UV_EEXIST) { char err_buf[128]; uv_err_name_r(ret, err_buf, sizeof(err_buf)); fprintf(stderr, "%s: Failed to create coverage directory %s\n", err_buf, directory.c_str()); return false; } std::string thread_id = std::to_string(env()->thread_id()); std::string pid = std::to_string(uv_os_getpid()); std::string timestamp = std::to_string( static_cast(GetCurrentTimeInMicroseconds() / 1000)); char filename[1024]; snprintf(filename, sizeof(filename), "coverage-%s-%s-%s.json", pid.c_str(), timestamp.c_str(), thread_id.c_str()); std::string target = directory + kPathSeparator + filename; MaybeLocal result = GetResult(message); if (result.IsEmpty()) { return false; } return WriteResult(target.c_str(), result.ToLocalChecked()); } MaybeLocal V8CoverageConnection::GetResult(Local message) { Local context = env()->context(); Isolate* isolate = env()->isolate(); Local parsed; if (!v8::JSON::Parse(context, message).ToLocal(&parsed) || !parsed->IsObject()) { fprintf(stderr, "Failed to parse coverage result as JSON object\n"); return MaybeLocal(); } Local result_v; if (!parsed.As() ->Get(context, FIXED_ONE_BYTE_STRING(isolate, "result")) .ToLocal(&result_v)) { fprintf(stderr, "Failed to get result from coverage message\n"); return MaybeLocal(); } if (result_v->IsUndefined()) { fprintf(stderr, "'result' from coverage message is undefined\n"); return MaybeLocal(); } Local result_s; if (!v8::JSON::Stringify(context, result_v).ToLocal(&result_s)) { fprintf(stderr, "Failed to stringify coverage result\n"); return MaybeLocal(); } return result_s; } void V8CoverageConnection::Start() { Debug(env(), DebugCategory::INSPECTOR_PROFILER, "Sending Profiler.startPreciseCoverage\n"); Isolate* isolate = env()->isolate(); Local enable = FIXED_ONE_BYTE_STRING( isolate, R"({"id": 1, "method": "Profiler.enable"})"); Local start = FIXED_ONE_BYTE_STRING(isolate, R"({ "id": 2, "method": "Profiler.startPreciseCoverage", "params": { "callCount": true, "detailed": true } })"); DispatchMessage(enable); DispatchMessage(start); } void V8CoverageConnection::End() { CHECK_EQ(ending_, false); ending_ = true; Debug(env(), DebugCategory::INSPECTOR_PROFILER, "Sending Profiler.takePreciseCoverage\n"); Isolate* isolate = env()->isolate(); HandleScope scope(isolate); Local end = FIXED_ONE_BYTE_STRING(isolate, R"({ "id": 3, "method": "Profiler.takePreciseCoverage" })"); DispatchMessage(end); } void V8CpuProfilerConnection::OnMessage( const v8_inspector::StringView& message) { Debug(env(), DebugCategory::INSPECTOR_PROFILER, "Receive cpu profiling message, ending = %s\n", ending_ ? "true" : "false"); if (!ending_) { return; } Isolate* isolate = env()->isolate(); HandleScope handle_scope(isolate); Local context = env()->context(); Context::Scope context_scope(context); Local result; if (!String::NewFromTwoByte(isolate, message.characters16(), NewStringType::kNormal, message.length()) .ToLocal(&result)) { fprintf(stderr, "Failed to convert profiling message\n"); } WriteCpuProfile(result); } void V8CpuProfilerConnection::WriteCpuProfile(Local message) { const std::string& filename = env()->cpu_prof_name(); const std::string& directory = env()->cpu_prof_dir(); CHECK(!filename.empty()); CHECK(!directory.empty()); uv_fs_t req; int ret = fs::MKDirpSync(nullptr, &req, directory, 0777, nullptr); uv_fs_req_cleanup(&req); if (ret < 0 && ret != UV_EEXIST) { char err_buf[128]; uv_err_name_r(ret, err_buf, sizeof(err_buf)); fprintf(stderr, "%s: Failed to create cpu profile directory %s\n", err_buf, directory.c_str()); return; } MaybeLocal result = GetResult(message); std::string target = directory + kPathSeparator + filename; if (!result.IsEmpty()) { WriteResult(target.c_str(), result.ToLocalChecked()); } } MaybeLocal V8CpuProfilerConnection::GetResult(Local message) { Local context = env()->context(); Isolate* isolate = env()->isolate(); Local parsed; if (!v8::JSON::Parse(context, message).ToLocal(&parsed) || !parsed->IsObject()) { fprintf(stderr, "Failed to parse CPU profile result as JSON object\n"); return MaybeLocal(); } Local result_v; if (!parsed.As() ->Get(context, FIXED_ONE_BYTE_STRING(isolate, "result")) .ToLocal(&result_v)) { fprintf(stderr, "Failed to get result from CPU profile message\n"); return MaybeLocal(); } if (!result_v->IsObject()) { fprintf(stderr, "'result' from CPU profile message is not an object\n"); return MaybeLocal(); } Local profile_v; if (!result_v.As() ->Get(context, FIXED_ONE_BYTE_STRING(isolate, "profile")) .ToLocal(&profile_v)) { fprintf(stderr, "'profile' from CPU profile result is undefined\n"); return MaybeLocal(); } Local result_s; if (!v8::JSON::Stringify(context, profile_v).ToLocal(&result_s)) { fprintf(stderr, "Failed to stringify CPU profile result\n"); return MaybeLocal(); } return result_s; } void V8CpuProfilerConnection::Start() { Debug(env(), DebugCategory::INSPECTOR_PROFILER, "Sending Profiler.start\n"); Isolate* isolate = env()->isolate(); Local enable = FIXED_ONE_BYTE_STRING( isolate, R"({"id": 1, "method": "Profiler.enable"})"); Local start = FIXED_ONE_BYTE_STRING( isolate, R"({"id": 2, "method": "Profiler.start"})"); DispatchMessage(enable); DispatchMessage(start); } void V8CpuProfilerConnection::End() { CHECK_EQ(ending_, false); ending_ = true; Debug(env(), DebugCategory::INSPECTOR_PROFILER, "Sending Profiler.stop\n"); Isolate* isolate = env()->isolate(); HandleScope scope(isolate); Local end = FIXED_ONE_BYTE_STRING(isolate, R"({"id": 3, "method": "Profiler.stop"})"); DispatchMessage(end); } // For now, we only support coverage profiling, but we may add more // in the future. void EndStartedProfilers(Environment* env) { Debug(env, DebugCategory::INSPECTOR_PROFILER, "EndStartedProfilers\n"); V8ProfilerConnection* connection = env->coverage_connection(); if (connection != nullptr && !connection->ending()) { Debug( env, DebugCategory::INSPECTOR_PROFILER, "Ending coverage collection\n"); connection->End(); } connection = env->cpu_profiler_connection(); if (connection != nullptr && !connection->ending()) { Debug(env, DebugCategory::INSPECTOR_PROFILER, "Ending cpu profiling\n"); connection->End(); } } std::string GetCwd() { char cwd[CWD_BUFSIZE]; size_t size = CWD_BUFSIZE; int err = uv_cwd(cwd, &size); // This can fail if the cwd is deleted. // TODO(joyeecheung): store this in the Environment during Environment // creation and fallback to exec_path and argv0, then we no longer need // SetCoverageDirectory(). CHECK_EQ(err, 0); CHECK_GT(size, 0); return cwd; } void StartProfilers(Environment* env) { Isolate* isolate = env->isolate(); Local coverage_str = env->env_vars()->Get( isolate, FIXED_ONE_BYTE_STRING(isolate, "NODE_V8_COVERAGE")); if (!coverage_str.IsEmpty() && coverage_str->Length() > 0) { CHECK_NULL(env->coverage_connection()); env->set_coverage_connection(std::make_unique(env)); env->coverage_connection()->Start(); } if (env->options()->cpu_prof) { const std::string& dir = env->options()->cpu_prof_dir; env->set_cpu_prof_dir(dir.empty() ? GetCwd() : dir); if (env->options()->cpu_prof_name.empty()) { DiagnosticFilename filename(env, "CPU", "cpuprofile"); env->set_cpu_prof_name(*filename); } else { env->set_cpu_prof_name(env->options()->cpu_prof_name); } CHECK_NULL(env->cpu_profiler_connection()); env->set_cpu_profiler_connection( std::make_unique(env)); env->cpu_profiler_connection()->Start(); } } static void SetCoverageDirectory(const FunctionCallbackInfo& args) { CHECK(args[0]->IsString()); Environment* env = Environment::GetCurrent(args); node::Utf8Value directory(env->isolate(), args[0].As()); env->set_coverage_directory(*directory); } static void Initialize(Local target, Local unused, Local context, void* priv) { Environment* env = Environment::GetCurrent(context); env->SetMethod(target, "setCoverageDirectory", SetCoverageDirectory); } } // namespace profiler } // namespace node NODE_MODULE_CONTEXT_AWARE_INTERNAL(profiler, node::profiler::Initialize)