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