Skip to content

Commit b238b6b

Browse files
joyeecheungMylesBorins
authored andcommittedAug 31, 2021
v8: implement v8.takeCoverage()
Add an v8.takeCoverage() API that allows the user to write the coverage started by NODE_V8_COVERAGE to disk on demand. The coverage can be written multiple times during the lifetime of the process, each time the execution counter will be reset. When the process is about to exit, one last coverage will still be written to disk. Also refactors the internal profiler connection code so that we use the inspector response id to identify the profile response instead of using an ad-hoc flag in C++. PR-URL: #33807 Reviewed-By: Anna Henningsen <anna@addaleax.net> Reviewed-By: Ben Noordhuis <info@bnoordhuis.nl> Reviewed-By: Jiawen Geng <technicalcute@gmail.com> Reviewed-By: Ben Coe <bencoe@gmail.com>
1 parent 0a759df commit b238b6b

File tree

8 files changed

+292
-80
lines changed

8 files changed

+292
-80
lines changed
 

‎doc/api/v8.md

+16
Original file line numberDiff line numberDiff line change
@@ -223,6 +223,21 @@ v8.setFlagsFromString('--trace_gc');
223223
setTimeout(() => { v8.setFlagsFromString('--notrace_gc'); }, 60e3);
224224
```
225225

226+
## `v8.takeCoverage()`
227+
228+
<!-- YAML
229+
added: REPLACEME
230+
-->
231+
232+
The `v8.takeCoverage()` method allows the user to write the coverage started by
233+
[`NODE_V8_COVERAGE`][] to disk on demand. This method can be invoked multiple
234+
times during the lifetime of the process, each time the execution counter will
235+
be reset and a new coverage report will be written to the directory specified
236+
by [`NODE_V8_COVERAGE`][].
237+
238+
When the process is about to exit, one last coverage will still be written to
239+
disk.
240+
226241
## `v8.writeHeapSnapshot([filename])`
227242
<!-- YAML
228243
added: v11.13.0
@@ -520,6 +535,7 @@ A subclass of [`Deserializer`][] corresponding to the format written by
520535
[`Deserializer`]: #v8_class_v8_deserializer
521536
[`Error`]: errors.md#errors_class_error
522537
[`GetHeapSpaceStatistics`]: https://v8docs.nodesource.com/node-13.2/d5/dda/classv8_1_1_isolate.html#ac673576f24fdc7a33378f8f57e1d13a4
538+
[`NODE_V8_COVERAGE`]: cli.html#cli_node_v8_coverage_dir
523539
[`Serializer`]: #v8_class_v8_serializer
524540
[`deserializer._readHostObject()`]: #v8_deserializer_readhostobject
525541
[`deserializer.transferArrayBuffer()`]: #v8_deserializer_transferarraybuffer_id_arraybuffer

‎lib/v8.js

+7
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,12 @@ const {
4040
Serializer,
4141
Deserializer
4242
} = internalBinding('serdes');
43+
44+
let profiler = {};
45+
if (internalBinding('config').hasInspector) {
46+
profiler = internalBinding('profiler');
47+
}
48+
4349
const assert = require('internal/assert');
4450
const { copy } = internalBinding('buffer');
4551
const { inspect } = require('internal/util/inspect');
@@ -272,6 +278,7 @@ module.exports = {
272278
DefaultSerializer,
273279
DefaultDeserializer,
274280
deserialize,
281+
takeCoverage: profiler.takeCoverage,
275282
serialize,
276283
writeHeapSnapshot,
277284
};

‎src/inspector_profiler.cc

+112-75
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
#include "util-inl.h"
1010
#include "v8-inspector.h"
1111

12+
#include <cinttypes>
1213
#include <sstream>
1314

1415
namespace node {
@@ -36,10 +37,11 @@ V8ProfilerConnection::V8ProfilerConnection(Environment* env)
3637
false)),
3738
env_(env) {}
3839

39-
size_t V8ProfilerConnection::DispatchMessage(const char* method,
40-
const char* params) {
40+
uint32_t V8ProfilerConnection::DispatchMessage(const char* method,
41+
const char* params,
42+
bool is_profile_request) {
4143
std::stringstream ss;
42-
size_t id = next_id();
44+
uint32_t id = next_id();
4345
ss << R"({ "id": )" << id;
4446
DCHECK(method != nullptr);
4547
ss << R"(, "method": ")" << method << '"';
@@ -50,12 +52,15 @@ size_t V8ProfilerConnection::DispatchMessage(const char* method,
5052
std::string message = ss.str();
5153
const uint8_t* message_data =
5254
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+
}
5359
Debug(env(),
5460
DebugCategory::INSPECTOR_PROFILER,
5561
"Dispatching message %s\n",
5662
message.c_str());
5763
session_->Dispatch(StringView(message_data, message.length()));
58-
// TODO(joyeecheung): use this to identify the ending message.
5964
return id;
6065
}
6166

@@ -77,33 +82,73 @@ void V8ProfilerConnection::V8ProfilerSessionDelegate::SendMessageToFrontend(
7782
Environment* env = connection_->env();
7883
Isolate* isolate = env->isolate();
7984
HandleScope handle_scope(isolate);
80-
Context::Scope context_scope(env->context());
85+
Local<Context> context = env->context();
86+
Context::Scope context_scope(context);
8187

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.
8588
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-
9589
// Convert StringView to a Local<String>.
9690
Local<String> message_str;
9791
if (!String::NewFromTwoByte(isolate,
9892
message.characters16(),
9993
NewStringType::kNormal,
10094
message.length())
10195
.ToLocal(&message_str)) {
102-
fprintf(stderr, "Failed to convert %s profile message\n", type);
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);
103110
return;
104111
}
105112

106-
connection_->WriteProfile(message_str);
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);
107152
}
108153

109154
static bool EnsureDirectory(const std::string& directory, const char* type) {
@@ -138,45 +183,9 @@ std::string V8CoverageConnection::GetFilename() const {
138183
return filename;
139184
}
140185

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) {
186+
void V8ProfilerConnection::WriteProfile(Local<Object> result) {
173187
Local<Context> context = env_->context();
174188

175-
// Get message.result from the response.
176-
Local<Object> result;
177-
if (!ParseProfile(env_, message, type()).ToLocal(&result)) {
178-
return;
179-
}
180189
// Generate the profile output from the subclass.
181190
Local<Object> profile;
182191
if (!GetProfile(result).ToLocal(&profile)) {
@@ -203,7 +212,7 @@ void V8ProfilerConnection::WriteProfile(Local<String> message) {
203212
WriteResult(env_, path.c_str(), result_s);
204213
}
205214

206-
void V8CoverageConnection::WriteProfile(Local<String> message) {
215+
void V8CoverageConnection::WriteProfile(Local<Object> result) {
207216
Isolate* isolate = env_->isolate();
208217
Local<Context> context = env_->context();
209218
HandleScope handle_scope(isolate);
@@ -219,11 +228,6 @@ void V8CoverageConnection::WriteProfile(Local<String> message) {
219228
return;
220229
}
221230

222-
// Get message.result from the response.
223-
Local<Object> result;
224-
if (!ParseProfile(env_, message, type()).ToLocal(&result)) {
225-
return;
226-
}
227231
// Generate the profile output from the subclass.
228232
Local<Object> profile;
229233
if (!GetProfile(result).ToLocal(&profile)) {
@@ -287,10 +291,19 @@ void V8CoverageConnection::Start() {
287291
R"({ "callCount": true, "detailed": true })");
288292
}
289293

294+
void V8CoverageConnection::TakeCoverage() {
295+
DispatchMessage("Profiler.takePreciseCoverage", nullptr, true);
296+
}
297+
290298
void V8CoverageConnection::End() {
291-
CHECK_EQ(ending_, false);
299+
Debug(env_,
300+
DebugCategory::INSPECTOR_PROFILER,
301+
"V8CoverageConnection::End(), ending = %d\n", ending_);
302+
if (ending_) {
303+
return;
304+
}
292305
ending_ = true;
293-
DispatchMessage("Profiler.takePreciseCoverage");
306+
TakeCoverage();
294307
}
295308

296309
std::string V8CpuProfilerConnection::GetDirectory() const {
@@ -327,9 +340,14 @@ void V8CpuProfilerConnection::Start() {
327340
}
328341

329342
void V8CpuProfilerConnection::End() {
330-
CHECK_EQ(ending_, false);
343+
Debug(env_,
344+
DebugCategory::INSPECTOR_PROFILER,
345+
"V8CpuProfilerConnection::End(), ending = %d\n", ending_);
346+
if (ending_) {
347+
return;
348+
}
331349
ending_ = true;
332-
DispatchMessage("Profiler.stop");
350+
DispatchMessage("Profiler.stop", nullptr, true);
333351
}
334352

335353
std::string V8HeapProfilerConnection::GetDirectory() const {
@@ -365,31 +383,33 @@ void V8HeapProfilerConnection::Start() {
365383
}
366384

367385
void V8HeapProfilerConnection::End() {
368-
CHECK_EQ(ending_, false);
386+
Debug(env_,
387+
DebugCategory::INSPECTOR_PROFILER,
388+
"V8HeapProfilerConnection::End(), ending = %d\n", ending_);
389+
if (ending_) {
390+
return;
391+
}
369392
ending_ = true;
370-
DispatchMessage("HeapProfiler.stopSampling");
393+
DispatchMessage("HeapProfiler.stopSampling", nullptr, true);
371394
}
372395

373396
// For now, we only support coverage profiling, but we may add more
374397
// in the future.
375398
static void EndStartedProfilers(Environment* env) {
399+
// TODO(joyeechueng): merge these connections and use one session per env.
376400
Debug(env, DebugCategory::INSPECTOR_PROFILER, "EndStartedProfilers\n");
377401
V8ProfilerConnection* connection = env->cpu_profiler_connection();
378-
if (connection != nullptr && !connection->ending()) {
379-
Debug(env, DebugCategory::INSPECTOR_PROFILER, "Ending cpu profiling\n");
402+
if (connection != nullptr) {
380403
connection->End();
381404
}
382405

383406
connection = env->heap_profiler_connection();
384-
if (connection != nullptr && !connection->ending()) {
385-
Debug(env, DebugCategory::INSPECTOR_PROFILER, "Ending heap profiling\n");
407+
if (connection != nullptr) {
386408
connection->End();
387409
}
388410

389411
connection = env->coverage_connection();
390-
if (connection != nullptr && !connection->ending()) {
391-
Debug(
392-
env, DebugCategory::INSPECTOR_PROFILER, "Ending coverage collection\n");
412+
if (connection != nullptr) {
393413
connection->End();
394414
}
395415
}
@@ -469,13 +489,30 @@ static void SetSourceMapCacheGetter(const FunctionCallbackInfo<Value>& args) {
469489
env->set_source_map_cache_getter(args[0].As<Function>());
470490
}
471491

492+
static void TakeCoverage(const FunctionCallbackInfo<Value>& args) {
493+
Environment* env = Environment::GetCurrent(args);
494+
V8CoverageConnection* connection = env->coverage_connection();
495+
496+
Debug(
497+
env,
498+
DebugCategory::INSPECTOR_PROFILER,
499+
"TakeCoverage, connection %s nullptr\n",
500+
connection == nullptr ? "==" : "!=");
501+
502+
if (connection != nullptr) {
503+
Debug(env, DebugCategory::INSPECTOR_PROFILER, "taking coverage\n");
504+
connection->TakeCoverage();
505+
}
506+
}
507+
472508
static void Initialize(Local<Object> target,
473509
Local<Value> unused,
474510
Local<Context> context,
475511
void* priv) {
476512
Environment* env = Environment::GetCurrent(context);
477513
env->SetMethod(target, "setCoverageDirectory", SetCoverageDirectory);
478514
env->SetMethod(target, "setSourceMapCacheGetter", SetSourceMapCacheGetter);
515+
env->SetMethod(target, "takeCoverage", TakeCoverage);
479516
}
480517

481518
} // namespace profiler

‎src/inspector_profiler.h

+16-5
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
#error("This header can only be used when inspector is enabled")
88
#endif
99

10+
#include <unordered_set>
1011
#include "inspector_agent.h"
1112

1213
namespace node {
@@ -39,7 +40,9 @@ class V8ProfilerConnection {
3940
// The optional `params` should be formatted in JSON.
4041
// The strings should be in one byte characters - which is enough for
4142
// the commands we use here.
42-
size_t DispatchMessage(const char* method, const char* params = nullptr);
43+
uint32_t DispatchMessage(const char* method,
44+
const char* params = nullptr,
45+
bool is_profile_request = false);
4346

4447
// Use DispatchMessage() to dispatch necessary inspector messages
4548
// to start and end the profiling.
@@ -58,12 +61,19 @@ class V8ProfilerConnection {
5861
// which will be then written as a JSON.
5962
virtual v8::MaybeLocal<v8::Object> GetProfile(
6063
v8::Local<v8::Object> result) = 0;
61-
virtual void WriteProfile(v8::Local<v8::String> message);
64+
virtual void WriteProfile(v8::Local<v8::Object> result);
65+
66+
bool HasProfileId(uint32_t id) const {
67+
return profile_ids_.find(id) != profile_ids_.end();
68+
}
69+
70+
void RemoveProfileId(uint32_t id) { profile_ids_.erase(id); }
6271

6372
private:
64-
size_t next_id() { return id_++; }
73+
uint32_t next_id() { return id_++; }
6574
std::unique_ptr<inspector::InspectorSession> session_;
66-
size_t id_ = 1;
75+
uint32_t id_ = 1;
76+
std::unordered_set<uint32_t> profile_ids_;
6777

6878
protected:
6979
Environment* env_ = nullptr;
@@ -82,8 +92,9 @@ class V8CoverageConnection : public V8ProfilerConnection {
8292
std::string GetDirectory() const override;
8393
std::string GetFilename() const override;
8494
v8::MaybeLocal<v8::Object> GetProfile(v8::Local<v8::Object> result) override;
85-
void WriteProfile(v8::Local<v8::String> message) override;
95+
void WriteProfile(v8::Local<v8::Object> result) override;
8696
void WriteSourceMapCache();
97+
void TakeCoverage();
8798

8899
private:
89100
std::unique_ptr<inspector::InspectorSession> session_;

‎test/fixtures/v8-coverage/interval.js

+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
'use strict';
2+
let counter = 0;
3+
let result;
4+
const TEST_INTERVALS = parseInt(process.env.TEST_INTERVALS) || 1;
5+
6+
const i = setInterval(function interval() {
7+
counter++;
8+
if (counter < TEST_INTERVALS) {
9+
result = 1;
10+
} else {
11+
result = process.hrtime();
12+
clearInterval(i);
13+
}
14+
}, 100);
+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
'use strict';
2+
const v8 = require('v8');
3+
4+
setTimeout(() => {
5+
v8.takeCoverage();
6+
}, 1000);
7+
8+
setTimeout(() => {
9+
v8.takeCoverage();
10+
}, 2000);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
'use strict';
2+
3+
if (!process.features.inspector) return;
4+
5+
require('../common');
6+
const fixtures = require('../common/fixtures');
7+
const tmpdir = require('../common/tmpdir');
8+
const assert = require('assert');
9+
const fs = require('fs');
10+
const { spawnSync } = require('child_process');
11+
12+
tmpdir.refresh();
13+
14+
// v8.takeCoverage() should be a noop if NODE_V8_COVERAGE is not set.
15+
const intervals = 40;
16+
{
17+
const output = spawnSync(process.execPath, [
18+
'-r',
19+
fixtures.path('v8-coverage', 'take-coverage'),
20+
fixtures.path('v8-coverage', 'interval'),
21+
], {
22+
env: {
23+
...process.env,
24+
NODE_DEBUG_NATIVE: 'INSPECTOR_PROFILER',
25+
TEST_INTERVALS: intervals
26+
},
27+
});
28+
console.log(output.stderr.toString());
29+
assert.strictEqual(output.status, 0);
30+
const coverageFiles = fs.readdirSync(tmpdir.path);
31+
assert.strictEqual(coverageFiles.length, 0);
32+
}
+85
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
'use strict';
2+
3+
if (!process.features.inspector) return;
4+
5+
require('../common');
6+
const fixtures = require('../common/fixtures');
7+
const tmpdir = require('../common/tmpdir');
8+
const assert = require('assert');
9+
const fs = require('fs');
10+
const path = require('path');
11+
const { spawnSync } = require('child_process');
12+
13+
tmpdir.refresh();
14+
const intervals = 40;
15+
// Outputs coverage when v8.takeCoverage() is invoked.
16+
{
17+
const output = spawnSync(process.execPath, [
18+
'-r',
19+
fixtures.path('v8-coverage', 'take-coverage'),
20+
fixtures.path('v8-coverage', 'interval'),
21+
], {
22+
env: {
23+
...process.env,
24+
NODE_V8_COVERAGE: tmpdir.path,
25+
NODE_DEBUG_NATIVE: 'INSPECTOR_PROFILER',
26+
TEST_INTERVALS: intervals
27+
},
28+
});
29+
console.log(output.stderr.toString());
30+
assert.strictEqual(output.status, 0);
31+
const coverageFiles = fs.readdirSync(tmpdir.path);
32+
33+
let coverages = [];
34+
for (const coverageFile of coverageFiles) {
35+
const coverage = require(path.join(tmpdir.path, coverageFile));
36+
for (const result of coverage.result) {
37+
if (result.url.includes('/interval')) {
38+
coverages.push({
39+
file: coverageFile,
40+
func: result.functions.find((f) => f.functionName === 'interval'),
41+
timestamp: coverage.timestamp
42+
});
43+
}
44+
}
45+
}
46+
47+
coverages = coverages.sort((a, b) => { return a.timestamp - b.timestamp; });
48+
// There should be two coverages taken, one triggered by v8.takeCoverage(),
49+
// the other by process exit.
50+
console.log('Coverages:', coverages);
51+
assert.strictEqual(coverages.length, 3);
52+
53+
let blockHitsTotal = 0;
54+
for (let i = 0; i < coverages.length; ++i) {
55+
const { ranges } = coverages[i].func;
56+
console.log('coverage', i, ranges);
57+
58+
if (i !== coverages.length - 1) {
59+
// When the first two coverages are taken:
60+
assert.strictEqual(ranges.length, 2);
61+
const blockHits = ranges[0].count;
62+
// The block inside interval() should be hit at least once.
63+
assert.notStrictEqual(blockHits, 0);
64+
blockHitsTotal += blockHits;
65+
// The else branch should not be hit.
66+
const elseBranchHits = ranges[1].count;
67+
assert.strictEqual(elseBranchHits, 0);
68+
} else {
69+
// At process exit:
70+
assert.strictEqual(ranges.length, 3);
71+
const blockHits = ranges[0].count;
72+
// The block inside interval() should be hit at least once more.
73+
assert.notStrictEqual(blockHits, 0);
74+
blockHitsTotal += blockHits;
75+
// The else branch should be hit exactly once.
76+
const elseBranchHits = ranges[2].count;
77+
assert.strictEqual(elseBranchHits, 1);
78+
const ifBranchHits = ranges[1].count;
79+
assert.strictEqual(ifBranchHits, blockHits - elseBranchHits);
80+
}
81+
}
82+
83+
// The block should be hit `intervals` times in total.
84+
assert.strictEqual(blockHitsTotal, intervals);
85+
}

0 commit comments

Comments
 (0)
Please sign in to comment.