Coverage Report

Created: 2025-08-28 09:57

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