1 #include "inspector_profiler.h"
2 #include "base_object-inl.h"
3 #include "debug_utils-inl.h"
4 #include "diagnosticfilename-inl.h"
5 #include "memory_tracker-inl.h"
6 #include "node_file.h"
7 #include "node_errors.h"
8 #include "node_internals.h"
9 #include "util-inl.h"
10 #include "v8-inspector.h"
11
12 #include <cinttypes>
13 #include <sstream>
14
15 namespace node {
16 namespace profiler {
17
18 using errors::TryCatchScope;
19 using v8::Context;
20 using v8::Function;
21 using v8::FunctionCallbackInfo;
22 using v8::HandleScope;
23 using v8::Isolate;
24 using v8::Local;
25 using v8::MaybeLocal;
26 using v8::NewStringType;
27 using v8::Object;
28 using v8::String;
29 using v8::Value;
30
31 using v8_inspector::StringView;
32
V8ProfilerConnection(Environment * env)33 V8ProfilerConnection::V8ProfilerConnection(Environment* env)
34 : session_(env->inspector_agent()->Connect(
35 std::make_unique<V8ProfilerConnection::V8ProfilerSessionDelegate>(
36 this),
37 false)),
38 env_(env) {}
39
DispatchMessage(const char * method,const char * params,bool is_profile_request)40 uint32_t V8ProfilerConnection::DispatchMessage(const char* method,
41 const char* params,
42 bool is_profile_request) {
43 std::stringstream ss;
44 uint32_t id = next_id();
45 ss << R"({ "id": )" << id;
46 DCHECK(method != nullptr);
47 ss << R"(, "method": ")" << method << '"';
48 if (params != nullptr) {
49 ss << R"(, "params": )" << params;
50 }
51 ss << " }";
52 std::string message = ss.str();
53 const uint8_t* message_data =
54 reinterpret_cast<const uint8_t*>(message.c_str());
55 // Save the id of the profile request to identify its response.
56 if (is_profile_request) {
57 profile_ids_.insert(id);
58 }
59 Debug(env(),
60 DebugCategory::INSPECTOR_PROFILER,
61 "Dispatching message %s\n",
62 message.c_str());
63 session_->Dispatch(StringView(message_data, message.length()));
64 return id;
65 }
66
67 static void WriteResult(Environment* env,
68 const char* path,
69 Local<String> result) {
70 int ret = WriteFileSync(env->isolate(), path, result);
71 if (ret != 0) {
72 char err_buf[128];
73 uv_err_name_r(ret, err_buf, sizeof(err_buf));
74 fprintf(stderr, "%s: Failed to write file %s\n", err_buf, path);
75 return;
76 }
77 Debug(env, DebugCategory::INSPECTOR_PROFILER, "Written result to %s\n", path);
78 }
79
80 void V8ProfilerConnection::V8ProfilerSessionDelegate::SendMessageToFrontend(
81 const v8_inspector::StringView& message) {
82 Environment* env = connection_->env();
83 Isolate* isolate = env->isolate();
84 HandleScope handle_scope(isolate);
85 Local<Context> context = env->context();
86 Context::Scope context_scope(context);
87
88 const char* type = connection_->type();
89 // Convert StringView to a Local<String>.
90 Local<String> message_str;
91 if (!String::NewFromTwoByte(isolate,
92 message.characters16(),
93 NewStringType::kNormal,
94 message.length())
95 .ToLocal(&message_str)) {
96 fprintf(
97 stderr, "Failed to convert %s profile message to V8 string\n", type);
98 return;
99 }
100
101 Debug(env,
102 DebugCategory::INSPECTOR_PROFILER,
103 "Receive %s profile message\n",
104 type);
105
106 Local<Value> parsed;
107 if (!v8::JSON::Parse(context, message_str).ToLocal(&parsed) ||
108 !parsed->IsObject()) {
109 fprintf(stderr, "Failed to parse %s profile result as JSON object\n", type);
110 return;
111 }
112
113 Local<Object> response = parsed.As<Object>();
114 Local<Value> id_v;
115 if (!response->Get(context, FIXED_ONE_BYTE_STRING(isolate, "id"))
116 .ToLocal(&id_v) ||
117 !id_v->IsUint32()) {
118 Utf8Value str(isolate, message_str);
119 fprintf(
120 stderr, "Cannot retrieve id from the response message:\n%s\n", *str);
121 return;
122 }
123 uint32_t id = id_v.As<v8::Uint32>()->Value();
124
125 if (!connection_->HasProfileId(id)) {
126 Utf8Value str(isolate, message_str);
127 Debug(env, DebugCategory::INSPECTOR_PROFILER, "%s\n", *str);
128 return;
129 } else {
130 Debug(env,
131 DebugCategory::INSPECTOR_PROFILER,
132 "Writing profile response (id = %" PRIu64 ")\n",
133 static_cast<uint64_t>(id));
134 }
135
136 // Get message.result from the response.
137 Local<Value> result_v;
138 if (!response->Get(context, FIXED_ONE_BYTE_STRING(isolate, "result"))
139 .ToLocal(&result_v)) {
140 fprintf(stderr, "Failed to get 'result' from %s profile response\n", type);
141 return;
142 }
143
144 if (!result_v->IsObject()) {
145 fprintf(
146 stderr, "'result' from %s profile response is not an object\n", type);
147 return;
148 }
149
150 connection_->WriteProfile(result_v.As<Object>());
151 connection_->RemoveProfileId(id);
152 }
153
154 static bool EnsureDirectory(const std::string& directory, const char* type) {
155 fs::FSReqWrapSync req_wrap_sync;
156 int ret = fs::MKDirpSync(nullptr, &req_wrap_sync.req, directory, 0777,
157 nullptr);
158 if (ret < 0 && ret != UV_EEXIST) {
159 char err_buf[128];
160 uv_err_name_r(ret, err_buf, sizeof(err_buf));
161 fprintf(stderr,
162 "%s: Failed to create %s profile directory %s\n",
163 err_buf,
164 type,
165 directory.c_str());
166 return false;
167 }
168 return true;
169 }
170
171 std::string V8CoverageConnection::GetFilename() const {
172 std::string thread_id = std::to_string(env()->thread_id());
173 std::string pid = std::to_string(uv_os_getpid());
174 std::string timestamp = std::to_string(
175 static_cast<uint64_t>(GetCurrentTimeInMicroseconds() / 1000));
176 char filename[1024];
177 snprintf(filename,
178 sizeof(filename),
179 "coverage-%s-%s-%s.json",
180 pid.c_str(),
181 timestamp.c_str(),
182 thread_id.c_str());
183 return filename;
184 }
185
186 void V8ProfilerConnection::WriteProfile(Local<Object> result) {
187 Local<Context> context = env_->context();
188
189 // Generate the profile output from the subclass.
190 Local<Object> profile;
191 if (!GetProfile(result).ToLocal(&profile)) {
192 return;
193 }
194
195 Local<String> result_s;
196 if (!v8::JSON::Stringify(context, profile).ToLocal(&result_s)) {
197 fprintf(stderr, "Failed to stringify %s profile result\n", type());
198 return;
199 }
200
201 // Create the directory if necessary.
202 std::string directory = GetDirectory();
203 DCHECK(!directory.empty());
204 if (!EnsureDirectory(directory, type())) {
205 return;
206 }
207
208 std::string filename = GetFilename();
209 DCHECK(!filename.empty());
210 std::string path = directory + kPathSeparator + filename;
211
212 WriteResult(env_, path.c_str(), result_s);
213 }
214
215 void V8CoverageConnection::WriteProfile(Local<Object> result) {
216 Isolate* isolate = env_->isolate();
217 Local<Context> context = env_->context();
218 HandleScope handle_scope(isolate);
219 Context::Scope context_scope(context);
220
221 // This is only set up during pre-execution (when the environment variables
222 // becomes available in the JS land). If it's empty, we don't have coverage
223 // directory path (which is resolved in JS land at the moment) either, so
224 // the best we could to is to just discard the profile and do nothing.
225 // This should only happen in half-baked Environments created using the
226 // embedder API.
227 if (env_->source_map_cache_getter().IsEmpty()) {
228 return;
229 }
230
231 // Generate the profile output from the subclass.
232 Local<Object> profile;
233 if (!GetProfile(result).ToLocal(&profile)) {
234 return;
235 }
236
237 // append source-map cache information to coverage object:
238 Local<Value> source_map_cache_v;
239 {
240 TryCatchScope try_catch(env());
241 {
242 Isolate::AllowJavascriptExecutionScope allow_js_here(isolate);
243 Local<Function> source_map_cache_getter = env_->source_map_cache_getter();
244 if (!source_map_cache_getter->Call(
245 context, Undefined(isolate), 0, nullptr)
246 .ToLocal(&source_map_cache_v)) {
247 return;
248 }
249 }
250 if (try_catch.HasCaught() && !try_catch.HasTerminated()) {
251 PrintCaughtException(isolate, context, try_catch);
252 }
253 }
254 // Avoid writing to disk if no source-map data:
255 if (!source_map_cache_v->IsUndefined()) {
256 profile->Set(context, FIXED_ONE_BYTE_STRING(isolate, "source-map-cache"),
257 source_map_cache_v).ToChecked();
258 }
259
260 Local<String> result_s;
261 if (!v8::JSON::Stringify(context, profile).ToLocal(&result_s)) {
262 fprintf(stderr, "Failed to stringify %s profile result\n", type());
263 return;
264 }
265
266 // Create the directory if necessary.
267 std::string directory = GetDirectory();
268 DCHECK(!directory.empty());
269 if (!EnsureDirectory(directory, type())) {
270 return;
271 }
272
273 std::string filename = GetFilename();
274 DCHECK(!filename.empty());
275 std::string path = directory + kPathSeparator + filename;
276
277 WriteResult(env_, path.c_str(), result_s);
278 }
279
280 MaybeLocal<Object> V8CoverageConnection::GetProfile(Local<Object> result) {
281 return result;
282 }
283
284 std::string V8CoverageConnection::GetDirectory() const {
285 return env()->coverage_directory();
286 }
287
288 void V8CoverageConnection::Start() {
289 DispatchMessage("Profiler.enable");
290 DispatchMessage("Profiler.startPreciseCoverage",
291 R"({ "callCount": true, "detailed": true })");
292 }
293
294 void V8CoverageConnection::TakeCoverage() {
295 DispatchMessage("Profiler.takePreciseCoverage", nullptr, true);
296 }
297
298 void V8CoverageConnection::StopCoverage() {
299 DispatchMessage("Profiler.stopPreciseCoverage");
300 }
301
302 void V8CoverageConnection::End() {
303 Debug(env_,
304 DebugCategory::INSPECTOR_PROFILER,
305 "V8CoverageConnection::End(), ending = %d\n", ending_);
306 if (ending_) {
307 return;
308 }
309 ending_ = true;
310 TakeCoverage();
311 }
312
313 std::string V8CpuProfilerConnection::GetDirectory() const {
314 return env()->cpu_prof_dir();
315 }
316
317 std::string V8CpuProfilerConnection::GetFilename() const {
318 return env()->cpu_prof_name();
319 }
320
321 MaybeLocal<Object> V8CpuProfilerConnection::GetProfile(Local<Object> result) {
322 Local<Value> profile_v;
323 if (!result
324 ->Get(env()->context(),
325 FIXED_ONE_BYTE_STRING(env()->isolate(), "profile"))
326 .ToLocal(&profile_v)) {
327 fprintf(stderr, "'profile' from CPU profile result is undefined\n");
328 return MaybeLocal<Object>();
329 }
330 if (!profile_v->IsObject()) {
331 fprintf(stderr, "'profile' from CPU profile result is not an Object\n");
332 return MaybeLocal<Object>();
333 }
334 return profile_v.As<Object>();
335 }
336
337 void V8CpuProfilerConnection::Start() {
338 DispatchMessage("Profiler.enable");
339 DispatchMessage("Profiler.start");
340 std::string params = R"({ "interval": )";
341 params += std::to_string(env()->cpu_prof_interval());
342 params += " }";
343 DispatchMessage("Profiler.setSamplingInterval", params.c_str());
344 }
345
346 void V8CpuProfilerConnection::End() {
347 Debug(env_,
348 DebugCategory::INSPECTOR_PROFILER,
349 "V8CpuProfilerConnection::End(), ending = %d\n", ending_);
350 if (ending_) {
351 return;
352 }
353 ending_ = true;
354 DispatchMessage("Profiler.stop", nullptr, true);
355 }
356
357 std::string V8HeapProfilerConnection::GetDirectory() const {
358 return env()->heap_prof_dir();
359 }
360
361 std::string V8HeapProfilerConnection::GetFilename() const {
362 return env()->heap_prof_name();
363 }
364
365 MaybeLocal<Object> V8HeapProfilerConnection::GetProfile(Local<Object> result) {
366 Local<Value> profile_v;
367 if (!result
368 ->Get(env()->context(),
369 FIXED_ONE_BYTE_STRING(env()->isolate(), "profile"))
370 .ToLocal(&profile_v)) {
371 fprintf(stderr, "'profile' from heap profile result is undefined\n");
372 return MaybeLocal<Object>();
373 }
374 if (!profile_v->IsObject()) {
375 fprintf(stderr, "'profile' from heap profile result is not an Object\n");
376 return MaybeLocal<Object>();
377 }
378 return profile_v.As<Object>();
379 }
380
381 void V8HeapProfilerConnection::Start() {
382 DispatchMessage("HeapProfiler.enable");
383 std::string params = R"({ "samplingInterval": )";
384 params += std::to_string(env()->heap_prof_interval());
385 params += " }";
386 DispatchMessage("HeapProfiler.startSampling", params.c_str());
387 }
388
389 void V8HeapProfilerConnection::End() {
390 Debug(env_,
391 DebugCategory::INSPECTOR_PROFILER,
392 "V8HeapProfilerConnection::End(), ending = %d\n", ending_);
393 if (ending_) {
394 return;
395 }
396 ending_ = true;
397 DispatchMessage("HeapProfiler.stopSampling", nullptr, true);
398 }
399
400 // For now, we only support coverage profiling, but we may add more
401 // in the future.
402 static void EndStartedProfilers(Environment* env) {
403 // TODO(joyeechueng): merge these connections and use one session per env.
404 Debug(env, DebugCategory::INSPECTOR_PROFILER, "EndStartedProfilers\n");
405 V8ProfilerConnection* connection = env->cpu_profiler_connection();
406 if (connection != nullptr) {
407 connection->End();
408 }
409
410 connection = env->heap_profiler_connection();
411 if (connection != nullptr) {
412 connection->End();
413 }
414
415 connection = env->coverage_connection();
416 if (connection != nullptr) {
417 connection->End();
418 }
419 }
420
421 void StartProfilers(Environment* env) {
422 AtExit(env, [](void* env) {
423 EndStartedProfilers(static_cast<Environment*>(env));
424 }, env);
425
426 Isolate* isolate = env->isolate();
427 Local<String> coverage_str = env->env_vars()->Get(
428 isolate, FIXED_ONE_BYTE_STRING(isolate, "NODE_V8_COVERAGE"))
429 .FromMaybe(Local<String>());
430 if (!coverage_str.IsEmpty() && coverage_str->Length() > 0) {
431 CHECK_NULL(env->coverage_connection());
432 env->set_coverage_connection(std::make_unique<V8CoverageConnection>(env));
433 env->coverage_connection()->Start();
434 }
435 if (env->options()->cpu_prof) {
436 const std::string& dir = env->options()->cpu_prof_dir;
437 env->set_cpu_prof_interval(env->options()->cpu_prof_interval);
438 env->set_cpu_prof_dir(dir.empty() ? env->GetCwd() : dir);
439 if (env->options()->cpu_prof_name.empty()) {
440 DiagnosticFilename filename(env, "CPU", "cpuprofile");
441 env->set_cpu_prof_name(*filename);
442 } else {
443 env->set_cpu_prof_name(env->options()->cpu_prof_name);
444 }
445 CHECK_NULL(env->cpu_profiler_connection());
446 env->set_cpu_profiler_connection(
447 std::make_unique<V8CpuProfilerConnection>(env));
448 env->cpu_profiler_connection()->Start();
449 }
450 if (env->options()->heap_prof) {
451 const std::string& dir = env->options()->heap_prof_dir;
452 env->set_heap_prof_interval(env->options()->heap_prof_interval);
453 env->set_heap_prof_dir(dir.empty() ? env->GetCwd() : dir);
454 if (env->options()->heap_prof_name.empty()) {
455 DiagnosticFilename filename(env, "Heap", "heapprofile");
456 env->set_heap_prof_name(*filename);
457 } else {
458 env->set_heap_prof_name(env->options()->heap_prof_name);
459 }
460 env->set_heap_profiler_connection(
461 std::make_unique<profiler::V8HeapProfilerConnection>(env));
462 env->heap_profiler_connection()->Start();
463 }
464 }
465
466 static void SetCoverageDirectory(const FunctionCallbackInfo<Value>& args) {
467 CHECK(args[0]->IsString());
468 Environment* env = Environment::GetCurrent(args);
469 node::Utf8Value directory(env->isolate(), args[0].As<String>());
470 env->set_coverage_directory(*directory);
471 }
472
473
474 static void SetSourceMapCacheGetter(const FunctionCallbackInfo<Value>& args) {
475 CHECK(args[0]->IsFunction());
476 Environment* env = Environment::GetCurrent(args);
477 env->set_source_map_cache_getter(args[0].As<Function>());
478 }
479
480 static void TakeCoverage(const FunctionCallbackInfo<Value>& args) {
481 Environment* env = Environment::GetCurrent(args);
482 V8CoverageConnection* connection = env->coverage_connection();
483
484 Debug(
485 env,
486 DebugCategory::INSPECTOR_PROFILER,
487 "TakeCoverage, connection %s nullptr\n",
488 connection == nullptr ? "==" : "!=");
489
490 if (connection != nullptr) {
491 Debug(env, DebugCategory::INSPECTOR_PROFILER, "taking coverage\n");
492 connection->TakeCoverage();
493 }
494 }
495
496 static void StopCoverage(const FunctionCallbackInfo<Value>& args) {
497 Environment* env = Environment::GetCurrent(args);
498 V8CoverageConnection* connection = env->coverage_connection();
499
500 Debug(env,
501 DebugCategory::INSPECTOR_PROFILER,
502 "StopCoverage, connection %s nullptr\n",
503 connection == nullptr ? "==" : "!=");
504
505 if (connection != nullptr) {
506 Debug(env, DebugCategory::INSPECTOR_PROFILER, "Stopping coverage\n");
507 connection->StopCoverage();
508 }
509 }
510
511 static void Initialize(Local<Object> target,
512 Local<Value> unused,
513 Local<Context> context,
514 void* priv) {
515 Environment* env = Environment::GetCurrent(context);
516 env->SetMethod(target, "setCoverageDirectory", SetCoverageDirectory);
517 env->SetMethod(target, "setSourceMapCacheGetter", SetSourceMapCacheGetter);
518 env->SetMethod(target, "takeCoverage", TakeCoverage);
519 env->SetMethod(target, "stopCoverage", StopCoverage);
520 }
521
522 } // namespace profiler
523 } // namespace node
524
525 NODE_MODULE_CONTEXT_AWARE_INTERNAL(profiler, node::profiler::Initialize)
526