• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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