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