Skip to content

Commit 6ba39d4

Browse files
bcoeBridgeAR
authored andcommitted
process: initial SourceMap support via NODE_V8_COVERAGE
PR-URL: #28960 Reviewed-By: Anna Henningsen <[email protected]> Reviewed-By: Benjamin Gruenbaum <[email protected]> Reviewed-By: Matteo Collina <[email protected]> Reviewed-By: David Carlier <[email protected]>
1 parent f84f1db commit 6ba39d4

20 files changed

+494
-11
lines changed

doc/api/cli.md

+53-9
Original file line numberDiff line numberDiff line change
@@ -1138,9 +1138,19 @@ variable is strongly discouraged.
11381138

11391139
### `NODE_V8_COVERAGE=dir`
11401140

1141-
When set, Node.js will begin outputting [V8 JavaScript code coverage][] to the
1142-
directory provided as an argument. Coverage is output as an array of
1143-
[ScriptCoverage][] objects:
1141+
When set, Node.js will begin outputting [V8 JavaScript code coverage][] and
1142+
[Source Map][] data to the directory provided as an argument (coverage
1143+
information is written as JSON to files with a `coverage` prefix).
1144+
1145+
`NODE_V8_COVERAGE` will automatically propagate to subprocesses, making it
1146+
easier to instrument applications that call the `child_process.spawn()` family
1147+
of functions. `NODE_V8_COVERAGE` can be set to an empty string, to prevent
1148+
propagation.
1149+
1150+
#### Coverage Output
1151+
1152+
Coverage is output as an array of [ScriptCoverage][] objects on the top-level
1153+
key `result`:
11441154

11451155
```json
11461156
{
@@ -1154,13 +1164,46 @@ directory provided as an argument. Coverage is output as an array of
11541164
}
11551165
```
11561166

1157-
`NODE_V8_COVERAGE` will automatically propagate to subprocesses, making it
1158-
easier to instrument applications that call the `child_process.spawn()` family
1159-
of functions. `NODE_V8_COVERAGE` can be set to an empty string, to prevent
1160-
propagation.
1167+
#### Source Map Cache
1168+
1169+
> Stability: 1 - Experimental
1170+
1171+
If found, Source Map data is appended to the top-level key `source-map-cache`
1172+
on the JSON coverage object.
1173+
1174+
`source-map-cache` is an object with keys representing the files source maps
1175+
were extracted from, and the values include the raw source-map URL
1176+
(in the key `url`) and the parsed Source Map V3 information (in the key `data`).
11611177

1162-
At this time coverage is only collected in the main thread and will not be
1163-
output for code executed by worker threads.
1178+
```json
1179+
{
1180+
"result": [
1181+
{
1182+
"scriptId": "68",
1183+
"url": "file:///absolute/path/to/source.js",
1184+
"functions": []
1185+
}
1186+
],
1187+
"source-map-cache": {
1188+
"file:///absolute/path/to/source.js": {
1189+
"url": "./path-to-map.json",
1190+
"data": {
1191+
"version": 3,
1192+
"sources": [
1193+
"file:///absolute/path/to/original.js"
1194+
],
1195+
"names": [
1196+
"Foo",
1197+
"console",
1198+
"info"
1199+
],
1200+
"mappings": "MAAMA,IACJC,YAAaC",
1201+
"sourceRoot": "./"
1202+
}
1203+
}
1204+
}
1205+
}
1206+
```
11641207

11651208
### `OPENSSL_CONF=file`
11661209
<!-- YAML
@@ -1231,6 +1274,7 @@ greater than `4` (its current default value). For more information, see the
12311274
[Chrome DevTools Protocol]: https://chromedevtools.github.io/devtools-protocol/
12321275
[REPL]: repl.html
12331276
[ScriptCoverage]: https://chromedevtools.github.io/devtools-protocol/tot/Profiler#type-ScriptCoverage
1277+
[Source Map]: https://sourcemaps.info/spec.html
12341278
[Subresource Integrity]: https://developer.mozilla.org/en-US/docs/Web/Security/Subresource_Integrity
12351279
[V8 JavaScript code coverage]: https://v8project.blogspot.com/2017/12/javascript-code-coverage.html
12361280
[customizing esm specifier resolution]: esm.html#esm_customizing_esm_specifier_resolution_algorithm

lib/internal/bootstrap/pre_execution.js

+2
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,9 @@ function setupCoverageHooks(dir) {
119119
const cwd = require('internal/process/execution').tryGetCwd();
120120
const { resolve } = require('path');
121121
const coverageDirectory = resolve(cwd, dir);
122+
const { sourceMapCacheToObject } = require('internal/source_map');
122123
internalBinding('profiler').setCoverageDirectory(coverageDirectory);
124+
internalBinding('profiler').setSourceMapCacheGetter(sourceMapCacheToObject);
123125
return coverageDirectory;
124126
}
125127

lib/internal/modules/cjs/loader.js

+2
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ const {
3131
} = primordials;
3232

3333
const { NativeModule } = require('internal/bootstrap/loaders');
34+
const { maybeCacheSourceMap } = require('internal/source_map');
3435
const { pathToFileURL, fileURLToPath, URL } = require('internal/url');
3536
const { deprecate } = require('internal/util');
3637
const vm = require('vm');
@@ -860,6 +861,7 @@ Module.prototype._compile = function(content, filename) {
860861
}
861862

862863
content = stripShebang(content);
864+
maybeCacheSourceMap(filename, content, this);
863865

864866
let compiledWrapper;
865867
if (patched) {

lib/internal/modules/esm/translators.js

+2
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ const {
3232
} = require('internal/errors').codes;
3333
const readFileAsync = promisify(fs.readFile);
3434
const JsonParse = JSON.parse;
35+
const { maybeCacheSourceMap } = require('internal/source_map');
3536

3637
const debug = debuglog('esm');
3738

@@ -75,6 +76,7 @@ async function importModuleDynamically(specifier, { url }) {
7576
// Strategy for loading a standard JavaScript module
7677
translators.set('module', async function moduleStrategy(url) {
7778
const source = `${await getSource(url)}`;
79+
maybeCacheSourceMap(url, source);
7880
debug(`Translating StandardModule ${url}`);
7981
const { ModuleWrap, callbackMap } = internalBinding('module_wrap');
8082
const module = new ModuleWrap(stripShebang(source), url);

lib/internal/source_map.js

+152
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
'use strict';
2+
3+
// See https://sourcemaps.info/spec.html for SourceMap V3 specification.
4+
const { Buffer } = require('buffer');
5+
const debug = require('internal/util/debuglog').debuglog('source_map');
6+
const { dirname, resolve } = require('path');
7+
const fs = require('fs');
8+
const {
9+
normalizeReferrerURL,
10+
} = require('internal/modules/cjs/helpers');
11+
const { JSON, Object } = primordials;
12+
// For cjs, since Module._cache is exposed to users, we use a WeakMap
13+
// keyed on module, facilitating garbage collection.
14+
const cjsSourceMapCache = new WeakMap();
15+
// The esm cache is not exposed to users, so we can use a Map keyed
16+
// on filenames.
17+
const esmSourceMapCache = new Map();
18+
const { fileURLToPath, URL } = require('url');
19+
20+
function maybeCacheSourceMap(filename, content, cjsModuleInstance) {
21+
if (!process.env.NODE_V8_COVERAGE) return;
22+
23+
let basePath;
24+
try {
25+
filename = normalizeReferrerURL(filename);
26+
basePath = dirname(fileURLToPath(filename));
27+
} catch (err) {
28+
// This is most likely an [eval]-wrapper, which is currently not
29+
// supported.
30+
debug(err.stack);
31+
return;
32+
}
33+
34+
const match = content.match(/\/[*/]#\s+sourceMappingURL=(?<sourceMappingURL>[^\s]+)/);
35+
if (match) {
36+
if (cjsModuleInstance) {
37+
cjsSourceMapCache.set(cjsModuleInstance, {
38+
url: match.groups.sourceMappingURL,
39+
data: dataFromUrl(basePath, match.groups.sourceMappingURL)
40+
});
41+
} else {
42+
// If there is no cjsModuleInstance assume we are in a
43+
// "modules/esm" context.
44+
esmSourceMapCache.set(filename, {
45+
url: match.groups.sourceMappingURL,
46+
data: dataFromUrl(basePath, match.groups.sourceMappingURL)
47+
});
48+
}
49+
}
50+
}
51+
52+
function dataFromUrl(basePath, sourceMappingURL) {
53+
try {
54+
const url = new URL(sourceMappingURL);
55+
switch (url.protocol) {
56+
case 'data:':
57+
return sourceMapFromDataUrl(basePath, url.pathname);
58+
default:
59+
debug(`unknown protocol ${url.protocol}`);
60+
return null;
61+
}
62+
} catch (err) {
63+
debug(err.stack);
64+
// If no scheme is present, we assume we are dealing with a file path.
65+
const sourceMapFile = resolve(basePath, sourceMappingURL);
66+
return sourceMapFromFile(sourceMapFile);
67+
}
68+
}
69+
70+
function sourceMapFromFile(sourceMapFile) {
71+
try {
72+
const content = fs.readFileSync(sourceMapFile, 'utf8');
73+
const data = JSON.parse(content);
74+
return sourcesToAbsolute(dirname(sourceMapFile), data);
75+
} catch (err) {
76+
debug(err.stack);
77+
return null;
78+
}
79+
}
80+
81+
// data:[<mediatype>][;base64],<data> see:
82+
// https://tools.ietf.org/html/rfc2397#section-2
83+
function sourceMapFromDataUrl(basePath, url) {
84+
const [format, data] = url.split(',');
85+
const splitFormat = format.split(';');
86+
const contentType = splitFormat[0];
87+
const base64 = splitFormat[splitFormat.length - 1] === 'base64';
88+
if (contentType === 'application/json') {
89+
const decodedData = base64 ?
90+
Buffer.from(data, 'base64').toString('utf8') : data;
91+
try {
92+
const parsedData = JSON.parse(decodedData);
93+
return sourcesToAbsolute(basePath, parsedData);
94+
} catch (err) {
95+
debug(err.stack);
96+
return null;
97+
}
98+
} else {
99+
debug(`unknown content-type ${contentType}`);
100+
return null;
101+
}
102+
}
103+
104+
// If the sources are not absolute URLs after prepending of the "sourceRoot",
105+
// the sources are resolved relative to the SourceMap (like resolving script
106+
// src in a html document).
107+
function sourcesToAbsolute(base, data) {
108+
data.sources = data.sources.map((source) => {
109+
source = (data.sourceRoot || '') + source;
110+
if (!/^[\\/]/.test(source[0])) {
111+
source = resolve(base, source);
112+
}
113+
if (!source.startsWith('file://')) source = `file://${source}`;
114+
return source;
115+
});
116+
// The sources array is now resolved to absolute URLs, sourceRoot should
117+
// be updated to noop.
118+
data.sourceRoot = '';
119+
return data;
120+
}
121+
122+
function sourceMapCacheToObject() {
123+
const obj = Object.create(null);
124+
125+
for (const [k, v] of esmSourceMapCache) {
126+
obj[k] = v;
127+
}
128+
appendCJSCache(obj);
129+
130+
if (Object.keys(obj).length === 0) {
131+
return undefined;
132+
} else {
133+
return obj;
134+
}
135+
}
136+
137+
// Since WeakMap can't be iterated over, we use Module._cache's
138+
// keys to facilitate Source Map serialization.
139+
function appendCJSCache(obj) {
140+
const { Module } = require('internal/modules/cjs/loader');
141+
Object.keys(Module._cache).forEach((key) => {
142+
const value = cjsSourceMapCache.get(Module._cache[key]);
143+
if (value) {
144+
obj[`file://${key}`] = value;
145+
}
146+
});
147+
}
148+
149+
module.exports = {
150+
sourceMapCacheToObject,
151+
maybeCacheSourceMap
152+
};

node.gyp

+1
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,7 @@
175175
'lib/internal/repl/history.js',
176176
'lib/internal/repl/utils.js',
177177
'lib/internal/socket_list.js',
178+
'lib/internal/source_map.js',
178179
'lib/internal/test/binding.js',
179180
'lib/internal/timers.js',
180181
'lib/internal/tls.js',

src/env.h

+1
Original file line numberDiff line numberDiff line change
@@ -445,6 +445,7 @@ constexpr size_t kFsStatsBufferLength =
445445
V(primordials, v8::Object) \
446446
V(promise_reject_callback, v8::Function) \
447447
V(script_data_constructor_function, v8::Function) \
448+
V(source_map_cache_getter, v8::Function) \
448449
V(tick_callback_function, v8::Function) \
449450
V(timers_callback_function, v8::Function) \
450451
V(tls_wrap_constructor_function, v8::Function) \

src/inspector_profiler.cc

+60
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,58 @@ void V8ProfilerConnection::WriteProfile(Local<String> message) {
180180
if (!GetProfile(result).ToLocal(&profile)) {
181181
return;
182182
}
183+
184+
Local<String> result_s;
185+
if (!v8::JSON::Stringify(context, profile).ToLocal(&result_s)) {
186+
fprintf(stderr, "Failed to stringify %s profile result\n", type());
187+
return;
188+
}
189+
190+
// Create the directory if necessary.
191+
std::string directory = GetDirectory();
192+
DCHECK(!directory.empty());
193+
if (!EnsureDirectory(directory, type())) {
194+
return;
195+
}
196+
197+
std::string filename = GetFilename();
198+
DCHECK(!filename.empty());
199+
std::string path = directory + kPathSeparator + filename;
200+
201+
WriteResult(env_, path.c_str(), result_s);
202+
}
203+
204+
void V8CoverageConnection::WriteProfile(Local<String> message) {
205+
Isolate* isolate = env_->isolate();
206+
Local<Context> context = env_->context();
207+
HandleScope handle_scope(isolate);
208+
Context::Scope context_scope(context);
209+
210+
// Get message.result from the response.
211+
Local<Object> result;
212+
if (!ParseProfile(env_, message, type()).ToLocal(&result)) {
213+
return;
214+
}
215+
// Generate the profile output from the subclass.
216+
Local<Object> profile;
217+
if (!GetProfile(result).ToLocal(&profile)) {
218+
return;
219+
}
220+
221+
// append source-map cache information to coverage object:
222+
Local<Function> source_map_cache_getter = env_->source_map_cache_getter();
223+
Local<Value> source_map_cache_v;
224+
if (!source_map_cache_getter->Call(env()->context(),
225+
Undefined(isolate), 0, nullptr)
226+
.ToLocal(&source_map_cache_v)) {
227+
return;
228+
}
229+
// Avoid writing to disk if no source-map data:
230+
if (!source_map_cache_v->IsUndefined()) {
231+
profile->Set(context, FIXED_ONE_BYTE_STRING(isolate, "source-map-cache"),
232+
source_map_cache_v);
233+
}
234+
183235
Local<String> result_s;
184236
if (!v8::JSON::Stringify(context, profile).ToLocal(&result_s)) {
185237
fprintf(stderr, "Failed to stringify %s profile result\n", type());
@@ -385,12 +437,20 @@ static void SetCoverageDirectory(const FunctionCallbackInfo<Value>& args) {
385437
env->set_coverage_directory(*directory);
386438
}
387439

440+
441+
static void SetSourceMapCacheGetter(const FunctionCallbackInfo<Value>& args) {
442+
CHECK(args[0]->IsFunction());
443+
Environment* env = Environment::GetCurrent(args);
444+
env->set_source_map_cache_getter(args[0].As<Function>());
445+
}
446+
388447
static void Initialize(Local<Object> target,
389448
Local<Value> unused,
390449
Local<Context> context,
391450
void* priv) {
392451
Environment* env = Environment::GetCurrent(context);
393452
env->SetMethod(target, "setCoverageDirectory", SetCoverageDirectory);
453+
env->SetMethod(target, "setSourceMapCacheGetter", SetSourceMapCacheGetter);
394454
}
395455

396456
} // namespace profiler

0 commit comments

Comments
 (0)