process: initial SourceMap support via NODE_V8_COVERAGE
PR-URL: https://github.com/nodejs/node/pull/28960 Reviewed-By: Anna Henningsen <anna@addaleax.net> Reviewed-By: Benjamin Gruenbaum <benjamingr@gmail.com> Reviewed-By: Matteo Collina <matteo.collina@gmail.com> Reviewed-By: David Carlier <devnexen@gmail.com>
This commit is contained in:
parent
e74f30894c
commit
8f06773a8c
@ -1110,9 +1110,19 @@ variable is strongly discouraged.
|
|||||||
|
|
||||||
### `NODE_V8_COVERAGE=dir`
|
### `NODE_V8_COVERAGE=dir`
|
||||||
|
|
||||||
When set, Node.js will begin outputting [V8 JavaScript code coverage][] to the
|
When set, Node.js will begin outputting [V8 JavaScript code coverage][] and
|
||||||
directory provided as an argument. Coverage is output as an array of
|
[Source Map][] data to the directory provided as an argument (coverage
|
||||||
[ScriptCoverage][] objects:
|
information is written as JSON to files with a `coverage` prefix).
|
||||||
|
|
||||||
|
`NODE_V8_COVERAGE` will automatically propagate to subprocesses, making it
|
||||||
|
easier to instrument applications that call the `child_process.spawn()` family
|
||||||
|
of functions. `NODE_V8_COVERAGE` can be set to an empty string, to prevent
|
||||||
|
propagation.
|
||||||
|
|
||||||
|
#### Coverage Output
|
||||||
|
|
||||||
|
Coverage is output as an array of [ScriptCoverage][] objects on the top-level
|
||||||
|
key `result`:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
@ -1126,13 +1136,46 @@ directory provided as an argument. Coverage is output as an array of
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
`NODE_V8_COVERAGE` will automatically propagate to subprocesses, making it
|
#### Source Map Cache
|
||||||
easier to instrument applications that call the `child_process.spawn()` family
|
|
||||||
of functions. `NODE_V8_COVERAGE` can be set to an empty string, to prevent
|
|
||||||
propagation.
|
|
||||||
|
|
||||||
At this time coverage is only collected in the main thread and will not be
|
> Stability: 1 - Experimental
|
||||||
output for code executed by worker threads.
|
|
||||||
|
If found, Source Map data is appended to the top-level key `source-map-cache`
|
||||||
|
on the JSON coverage object.
|
||||||
|
|
||||||
|
`source-map-cache` is an object with keys representing the files source maps
|
||||||
|
were extracted from, and the values include the raw source-map URL
|
||||||
|
(in the key `url`) and the parsed Source Map V3 information (in the key `data`).
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"result": [
|
||||||
|
{
|
||||||
|
"scriptId": "68",
|
||||||
|
"url": "file:///absolute/path/to/source.js",
|
||||||
|
"functions": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"source-map-cache": {
|
||||||
|
"file:///absolute/path/to/source.js": {
|
||||||
|
"url": "./path-to-map.json",
|
||||||
|
"data": {
|
||||||
|
"version": 3,
|
||||||
|
"sources": [
|
||||||
|
"file:///absolute/path/to/original.js"
|
||||||
|
],
|
||||||
|
"names": [
|
||||||
|
"Foo",
|
||||||
|
"console",
|
||||||
|
"info"
|
||||||
|
],
|
||||||
|
"mappings": "MAAMA,IACJC,YAAaC",
|
||||||
|
"sourceRoot": "./"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
### `OPENSSL_CONF=file`
|
### `OPENSSL_CONF=file`
|
||||||
<!-- YAML
|
<!-- YAML
|
||||||
@ -1203,6 +1246,7 @@ greater than `4` (its current default value). For more information, see the
|
|||||||
[Chrome DevTools Protocol]: https://chromedevtools.github.io/devtools-protocol/
|
[Chrome DevTools Protocol]: https://chromedevtools.github.io/devtools-protocol/
|
||||||
[REPL]: repl.html
|
[REPL]: repl.html
|
||||||
[ScriptCoverage]: https://chromedevtools.github.io/devtools-protocol/tot/Profiler#type-ScriptCoverage
|
[ScriptCoverage]: https://chromedevtools.github.io/devtools-protocol/tot/Profiler#type-ScriptCoverage
|
||||||
|
[Source Map]: https://sourcemaps.info/spec.html
|
||||||
[Subresource Integrity]: https://developer.mozilla.org/en-US/docs/Web/Security/Subresource_Integrity
|
[Subresource Integrity]: https://developer.mozilla.org/en-US/docs/Web/Security/Subresource_Integrity
|
||||||
[V8 JavaScript code coverage]: https://v8project.blogspot.com/2017/12/javascript-code-coverage.html
|
[V8 JavaScript code coverage]: https://v8project.blogspot.com/2017/12/javascript-code-coverage.html
|
||||||
[customizing esm specifier resolution]: esm.html#esm_customizing_esm_specifier_resolution_algorithm
|
[customizing esm specifier resolution]: esm.html#esm_customizing_esm_specifier_resolution_algorithm
|
||||||
|
@ -119,7 +119,9 @@ function setupCoverageHooks(dir) {
|
|||||||
const cwd = require('internal/process/execution').tryGetCwd();
|
const cwd = require('internal/process/execution').tryGetCwd();
|
||||||
const { resolve } = require('path');
|
const { resolve } = require('path');
|
||||||
const coverageDirectory = resolve(cwd, dir);
|
const coverageDirectory = resolve(cwd, dir);
|
||||||
|
const { sourceMapCacheToObject } = require('internal/source_map');
|
||||||
internalBinding('profiler').setCoverageDirectory(coverageDirectory);
|
internalBinding('profiler').setCoverageDirectory(coverageDirectory);
|
||||||
|
internalBinding('profiler').setSourceMapCacheGetter(sourceMapCacheToObject);
|
||||||
return coverageDirectory;
|
return coverageDirectory;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -31,6 +31,7 @@ const {
|
|||||||
} = primordials;
|
} = primordials;
|
||||||
|
|
||||||
const { NativeModule } = require('internal/bootstrap/loaders');
|
const { NativeModule } = require('internal/bootstrap/loaders');
|
||||||
|
const { maybeCacheSourceMap } = require('internal/source_map');
|
||||||
const { pathToFileURL, fileURLToPath, URL } = require('internal/url');
|
const { pathToFileURL, fileURLToPath, URL } = require('internal/url');
|
||||||
const { deprecate } = require('internal/util');
|
const { deprecate } = require('internal/util');
|
||||||
const vm = require('vm');
|
const vm = require('vm');
|
||||||
@ -845,7 +846,9 @@ Module.prototype.require = function(id) {
|
|||||||
var resolvedArgv;
|
var resolvedArgv;
|
||||||
let hasPausedEntry = false;
|
let hasPausedEntry = false;
|
||||||
|
|
||||||
function wrapSafe(filename, content) {
|
function wrapSafe(filename, content, cjsModuleInstance) {
|
||||||
|
maybeCacheSourceMap(filename, content, cjsModuleInstance);
|
||||||
|
|
||||||
if (patched) {
|
if (patched) {
|
||||||
const wrapper = Module.wrap(content);
|
const wrapper = Module.wrap(content);
|
||||||
return vm.runInThisContext(wrapper, {
|
return vm.runInThisContext(wrapper, {
|
||||||
@ -910,7 +913,7 @@ Module.prototype._compile = function(content, filename) {
|
|||||||
manifest.assertIntegrity(moduleURL, content);
|
manifest.assertIntegrity(moduleURL, content);
|
||||||
}
|
}
|
||||||
|
|
||||||
const compiledWrapper = wrapSafe(filename, content);
|
const compiledWrapper = wrapSafe(filename, content, this);
|
||||||
|
|
||||||
var inspectorWrapper = null;
|
var inspectorWrapper = null;
|
||||||
if (getOptionValue('--inspect-brk') && process._eval == null) {
|
if (getOptionValue('--inspect-brk') && process._eval == null) {
|
||||||
|
@ -31,6 +31,7 @@ const {
|
|||||||
} = require('internal/errors').codes;
|
} = require('internal/errors').codes;
|
||||||
const readFileAsync = promisify(fs.readFile);
|
const readFileAsync = promisify(fs.readFile);
|
||||||
const JsonParse = JSON.parse;
|
const JsonParse = JSON.parse;
|
||||||
|
const { maybeCacheSourceMap } = require('internal/source_map');
|
||||||
|
|
||||||
const debug = debuglog('esm');
|
const debug = debuglog('esm');
|
||||||
|
|
||||||
@ -74,6 +75,7 @@ async function importModuleDynamically(specifier, { url }) {
|
|||||||
// Strategy for loading a standard JavaScript module
|
// Strategy for loading a standard JavaScript module
|
||||||
translators.set('module', async function moduleStrategy(url) {
|
translators.set('module', async function moduleStrategy(url) {
|
||||||
const source = `${await getSource(url)}`;
|
const source = `${await getSource(url)}`;
|
||||||
|
maybeCacheSourceMap(url, source);
|
||||||
debug(`Translating StandardModule ${url}`);
|
debug(`Translating StandardModule ${url}`);
|
||||||
const { ModuleWrap, callbackMap } = internalBinding('module_wrap');
|
const { ModuleWrap, callbackMap } = internalBinding('module_wrap');
|
||||||
const module = new ModuleWrap(source, url);
|
const module = new ModuleWrap(source, url);
|
||||||
|
152
lib/internal/source_map.js
Normal file
152
lib/internal/source_map.js
Normal file
@ -0,0 +1,152 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
// See https://sourcemaps.info/spec.html for SourceMap V3 specification.
|
||||||
|
const { Buffer } = require('buffer');
|
||||||
|
const debug = require('internal/util/debuglog').debuglog('source_map');
|
||||||
|
const { dirname, resolve } = require('path');
|
||||||
|
const fs = require('fs');
|
||||||
|
const {
|
||||||
|
normalizeReferrerURL,
|
||||||
|
} = require('internal/modules/cjs/helpers');
|
||||||
|
const { JSON, Object } = primordials;
|
||||||
|
// For cjs, since Module._cache is exposed to users, we use a WeakMap
|
||||||
|
// keyed on module, facilitating garbage collection.
|
||||||
|
const cjsSourceMapCache = new WeakMap();
|
||||||
|
// The esm cache is not exposed to users, so we can use a Map keyed
|
||||||
|
// on filenames.
|
||||||
|
const esmSourceMapCache = new Map();
|
||||||
|
const { fileURLToPath, URL } = require('url');
|
||||||
|
|
||||||
|
function maybeCacheSourceMap(filename, content, cjsModuleInstance) {
|
||||||
|
if (!process.env.NODE_V8_COVERAGE) return;
|
||||||
|
|
||||||
|
let basePath;
|
||||||
|
try {
|
||||||
|
filename = normalizeReferrerURL(filename);
|
||||||
|
basePath = dirname(fileURLToPath(filename));
|
||||||
|
} catch (err) {
|
||||||
|
// This is most likely an [eval]-wrapper, which is currently not
|
||||||
|
// supported.
|
||||||
|
debug(err.stack);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const match = content.match(/\/[*/]#\s+sourceMappingURL=(?<sourceMappingURL>[^\s]+)/);
|
||||||
|
if (match) {
|
||||||
|
if (cjsModuleInstance) {
|
||||||
|
cjsSourceMapCache.set(cjsModuleInstance, {
|
||||||
|
url: match.groups.sourceMappingURL,
|
||||||
|
data: dataFromUrl(basePath, match.groups.sourceMappingURL)
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// If there is no cjsModuleInstance assume we are in a
|
||||||
|
// "modules/esm" context.
|
||||||
|
esmSourceMapCache.set(filename, {
|
||||||
|
url: match.groups.sourceMappingURL,
|
||||||
|
data: dataFromUrl(basePath, match.groups.sourceMappingURL)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function dataFromUrl(basePath, sourceMappingURL) {
|
||||||
|
try {
|
||||||
|
const url = new URL(sourceMappingURL);
|
||||||
|
switch (url.protocol) {
|
||||||
|
case 'data:':
|
||||||
|
return sourceMapFromDataUrl(basePath, url.pathname);
|
||||||
|
default:
|
||||||
|
debug(`unknown protocol ${url.protocol}`);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
debug(err.stack);
|
||||||
|
// If no scheme is present, we assume we are dealing with a file path.
|
||||||
|
const sourceMapFile = resolve(basePath, sourceMappingURL);
|
||||||
|
return sourceMapFromFile(sourceMapFile);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function sourceMapFromFile(sourceMapFile) {
|
||||||
|
try {
|
||||||
|
const content = fs.readFileSync(sourceMapFile, 'utf8');
|
||||||
|
const data = JSON.parse(content);
|
||||||
|
return sourcesToAbsolute(dirname(sourceMapFile), data);
|
||||||
|
} catch (err) {
|
||||||
|
debug(err.stack);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// data:[<mediatype>][;base64],<data> see:
|
||||||
|
// https://tools.ietf.org/html/rfc2397#section-2
|
||||||
|
function sourceMapFromDataUrl(basePath, url) {
|
||||||
|
const [format, data] = url.split(',');
|
||||||
|
const splitFormat = format.split(';');
|
||||||
|
const contentType = splitFormat[0];
|
||||||
|
const base64 = splitFormat[splitFormat.length - 1] === 'base64';
|
||||||
|
if (contentType === 'application/json') {
|
||||||
|
const decodedData = base64 ?
|
||||||
|
Buffer.from(data, 'base64').toString('utf8') : data;
|
||||||
|
try {
|
||||||
|
const parsedData = JSON.parse(decodedData);
|
||||||
|
return sourcesToAbsolute(basePath, parsedData);
|
||||||
|
} catch (err) {
|
||||||
|
debug(err.stack);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
debug(`unknown content-type ${contentType}`);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the sources are not absolute URLs after prepending of the "sourceRoot",
|
||||||
|
// the sources are resolved relative to the SourceMap (like resolving script
|
||||||
|
// src in a html document).
|
||||||
|
function sourcesToAbsolute(base, data) {
|
||||||
|
data.sources = data.sources.map((source) => {
|
||||||
|
source = (data.sourceRoot || '') + source;
|
||||||
|
if (!/^[\\/]/.test(source[0])) {
|
||||||
|
source = resolve(base, source);
|
||||||
|
}
|
||||||
|
if (!source.startsWith('file://')) source = `file://${source}`;
|
||||||
|
return source;
|
||||||
|
});
|
||||||
|
// The sources array is now resolved to absolute URLs, sourceRoot should
|
||||||
|
// be updated to noop.
|
||||||
|
data.sourceRoot = '';
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
function sourceMapCacheToObject() {
|
||||||
|
const obj = Object.create(null);
|
||||||
|
|
||||||
|
for (const [k, v] of esmSourceMapCache) {
|
||||||
|
obj[k] = v;
|
||||||
|
}
|
||||||
|
appendCJSCache(obj);
|
||||||
|
|
||||||
|
if (Object.keys(obj).length === 0) {
|
||||||
|
return undefined;
|
||||||
|
} else {
|
||||||
|
return obj;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Since WeakMap can't be iterated over, we use Module._cache's
|
||||||
|
// keys to facilitate Source Map serialization.
|
||||||
|
function appendCJSCache(obj) {
|
||||||
|
const { Module } = require('internal/modules/cjs/loader');
|
||||||
|
Object.keys(Module._cache).forEach((key) => {
|
||||||
|
const value = cjsSourceMapCache.get(Module._cache[key]);
|
||||||
|
if (value) {
|
||||||
|
obj[`file://${key}`] = value;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
sourceMapCacheToObject,
|
||||||
|
maybeCacheSourceMap
|
||||||
|
};
|
1
node.gyp
1
node.gyp
@ -175,6 +175,7 @@
|
|||||||
'lib/internal/repl/history.js',
|
'lib/internal/repl/history.js',
|
||||||
'lib/internal/repl/utils.js',
|
'lib/internal/repl/utils.js',
|
||||||
'lib/internal/socket_list.js',
|
'lib/internal/socket_list.js',
|
||||||
|
'lib/internal/source_map.js',
|
||||||
'lib/internal/test/binding.js',
|
'lib/internal/test/binding.js',
|
||||||
'lib/internal/timers.js',
|
'lib/internal/timers.js',
|
||||||
'lib/internal/tls.js',
|
'lib/internal/tls.js',
|
||||||
|
@ -444,6 +444,7 @@ constexpr size_t kFsStatsBufferLength =
|
|||||||
V(primordials, v8::Object) \
|
V(primordials, v8::Object) \
|
||||||
V(promise_reject_callback, v8::Function) \
|
V(promise_reject_callback, v8::Function) \
|
||||||
V(script_data_constructor_function, v8::Function) \
|
V(script_data_constructor_function, v8::Function) \
|
||||||
|
V(source_map_cache_getter, v8::Function) \
|
||||||
V(tick_callback_function, v8::Function) \
|
V(tick_callback_function, v8::Function) \
|
||||||
V(timers_callback_function, v8::Function) \
|
V(timers_callback_function, v8::Function) \
|
||||||
V(tls_wrap_constructor_function, v8::Function) \
|
V(tls_wrap_constructor_function, v8::Function) \
|
||||||
|
@ -180,6 +180,58 @@ void V8ProfilerConnection::WriteProfile(Local<String> message) {
|
|||||||
if (!GetProfile(result).ToLocal(&profile)) {
|
if (!GetProfile(result).ToLocal(&profile)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Local<String> result_s;
|
||||||
|
if (!v8::JSON::Stringify(context, profile).ToLocal(&result_s)) {
|
||||||
|
fprintf(stderr, "Failed to stringify %s profile result\n", type());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create the directory if necessary.
|
||||||
|
std::string directory = GetDirectory();
|
||||||
|
DCHECK(!directory.empty());
|
||||||
|
if (!EnsureDirectory(directory, type())) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string filename = GetFilename();
|
||||||
|
DCHECK(!filename.empty());
|
||||||
|
std::string path = directory + kPathSeparator + filename;
|
||||||
|
|
||||||
|
WriteResult(env_, path.c_str(), result_s);
|
||||||
|
}
|
||||||
|
|
||||||
|
void V8CoverageConnection::WriteProfile(Local<String> message) {
|
||||||
|
Isolate* isolate = env_->isolate();
|
||||||
|
Local<Context> context = env_->context();
|
||||||
|
HandleScope handle_scope(isolate);
|
||||||
|
Context::Scope context_scope(context);
|
||||||
|
|
||||||
|
// Get message.result from the response.
|
||||||
|
Local<Object> result;
|
||||||
|
if (!ParseProfile(env_, message, type()).ToLocal(&result)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Generate the profile output from the subclass.
|
||||||
|
Local<Object> profile;
|
||||||
|
if (!GetProfile(result).ToLocal(&profile)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// append source-map cache information to coverage object:
|
||||||
|
Local<Function> source_map_cache_getter = env_->source_map_cache_getter();
|
||||||
|
Local<Value> source_map_cache_v;
|
||||||
|
if (!source_map_cache_getter->Call(env()->context(),
|
||||||
|
Undefined(isolate), 0, nullptr)
|
||||||
|
.ToLocal(&source_map_cache_v)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Avoid writing to disk if no source-map data:
|
||||||
|
if (!source_map_cache_v->IsUndefined()) {
|
||||||
|
profile->Set(context, FIXED_ONE_BYTE_STRING(isolate, "source-map-cache"),
|
||||||
|
source_map_cache_v);
|
||||||
|
}
|
||||||
|
|
||||||
Local<String> result_s;
|
Local<String> result_s;
|
||||||
if (!v8::JSON::Stringify(context, profile).ToLocal(&result_s)) {
|
if (!v8::JSON::Stringify(context, profile).ToLocal(&result_s)) {
|
||||||
fprintf(stderr, "Failed to stringify %s profile result\n", type());
|
fprintf(stderr, "Failed to stringify %s profile result\n", type());
|
||||||
@ -385,12 +437,20 @@ static void SetCoverageDirectory(const FunctionCallbackInfo<Value>& args) {
|
|||||||
env->set_coverage_directory(*directory);
|
env->set_coverage_directory(*directory);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static void SetSourceMapCacheGetter(const FunctionCallbackInfo<Value>& args) {
|
||||||
|
CHECK(args[0]->IsFunction());
|
||||||
|
Environment* env = Environment::GetCurrent(args);
|
||||||
|
env->set_source_map_cache_getter(args[0].As<Function>());
|
||||||
|
}
|
||||||
|
|
||||||
static void Initialize(Local<Object> target,
|
static void Initialize(Local<Object> target,
|
||||||
Local<Value> unused,
|
Local<Value> unused,
|
||||||
Local<Context> context,
|
Local<Context> context,
|
||||||
void* priv) {
|
void* priv) {
|
||||||
Environment* env = Environment::GetCurrent(context);
|
Environment* env = Environment::GetCurrent(context);
|
||||||
env->SetMethod(target, "setCoverageDirectory", SetCoverageDirectory);
|
env->SetMethod(target, "setCoverageDirectory", SetCoverageDirectory);
|
||||||
|
env->SetMethod(target, "setSourceMapCacheGetter", SetSourceMapCacheGetter);
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace profiler
|
} // namespace profiler
|
||||||
|
@ -59,13 +59,15 @@ class V8ProfilerConnection {
|
|||||||
// which will be then written as a JSON.
|
// which will be then written as a JSON.
|
||||||
virtual v8::MaybeLocal<v8::Object> GetProfile(
|
virtual v8::MaybeLocal<v8::Object> GetProfile(
|
||||||
v8::Local<v8::Object> result) = 0;
|
v8::Local<v8::Object> result) = 0;
|
||||||
|
virtual void WriteProfile(v8::Local<v8::String> message);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
size_t next_id() { return id_++; }
|
size_t next_id() { return id_++; }
|
||||||
void WriteProfile(v8::Local<v8::String> message);
|
|
||||||
std::unique_ptr<inspector::InspectorSession> session_;
|
std::unique_ptr<inspector::InspectorSession> session_;
|
||||||
Environment* env_ = nullptr;
|
|
||||||
size_t id_ = 1;
|
size_t id_ = 1;
|
||||||
|
|
||||||
|
protected:
|
||||||
|
Environment* env_ = nullptr;
|
||||||
};
|
};
|
||||||
|
|
||||||
class V8CoverageConnection : public V8ProfilerConnection {
|
class V8CoverageConnection : public V8ProfilerConnection {
|
||||||
@ -81,6 +83,8 @@ class V8CoverageConnection : public V8ProfilerConnection {
|
|||||||
std::string GetDirectory() const override;
|
std::string GetDirectory() const override;
|
||||||
std::string GetFilename() const override;
|
std::string GetFilename() const override;
|
||||||
v8::MaybeLocal<v8::Object> GetProfile(v8::Local<v8::Object> result) override;
|
v8::MaybeLocal<v8::Object> GetProfile(v8::Local<v8::Object> result) override;
|
||||||
|
void WriteProfile(v8::Local<v8::String> message) override;
|
||||||
|
void WriteSourceMapCache();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
std::unique_ptr<inspector::InspectorSession> session_;
|
std::unique_ptr<inspector::InspectorSession> session_;
|
||||||
|
7
test/fixtures/source-map/basic.js
vendored
Normal file
7
test/fixtures/source-map/basic.js
vendored
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
const a = 99;
|
||||||
|
if (true) {
|
||||||
|
const b = 101;
|
||||||
|
} else {
|
||||||
|
const c = 102;
|
||||||
|
}
|
||||||
|
//# sourceMappingURL=https://http.cat/418
|
2
test/fixtures/source-map/disk-relative-path.js
vendored
Normal file
2
test/fixtures/source-map/disk-relative-path.js
vendored
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
class Foo{constructor(x=33){this.x=x?x:99;if(this.x){console.info("covered")}else{console.info("uncovered")}this.methodC()}methodA(){console.info("covered")}methodB(){console.info("uncovered")}methodC(){console.info("covered")}methodD(){console.info("uncovered")}}const a=new Foo(0);const b=new Foo(33);a.methodA();
|
||||||
|
//# sourceMappingURL=./disk.map
|
27
test/fixtures/source-map/disk.js
vendored
Normal file
27
test/fixtures/source-map/disk.js
vendored
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
class Foo {
|
||||||
|
constructor (x=33) {
|
||||||
|
this.x = x ? x : 99
|
||||||
|
if (this.x) {
|
||||||
|
console.info('covered')
|
||||||
|
} else {
|
||||||
|
console.info('uncovered')
|
||||||
|
}
|
||||||
|
this.methodC()
|
||||||
|
}
|
||||||
|
methodA () {
|
||||||
|
console.info('covered')
|
||||||
|
}
|
||||||
|
methodB () {
|
||||||
|
console.info('uncovered')
|
||||||
|
}
|
||||||
|
methodC () {
|
||||||
|
console.info('covered')
|
||||||
|
}
|
||||||
|
methodD () {
|
||||||
|
console.info('uncovered')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const a = new Foo(0)
|
||||||
|
const b = new Foo(33)
|
||||||
|
a.methodA()
|
20
test/fixtures/source-map/disk.map
vendored
Normal file
20
test/fixtures/source-map/disk.map
vendored
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"version": 3,
|
||||||
|
"sources": [
|
||||||
|
"disk.js"
|
||||||
|
],
|
||||||
|
"names": [
|
||||||
|
"Foo",
|
||||||
|
"[object Object]",
|
||||||
|
"x",
|
||||||
|
"this",
|
||||||
|
"console",
|
||||||
|
"info",
|
||||||
|
"methodC",
|
||||||
|
"a",
|
||||||
|
"b",
|
||||||
|
"methodA"
|
||||||
|
],
|
||||||
|
"mappings": "MAAMA,IACJC,YAAaC,EAAE,IACbC,KAAKD,EAAIA,EAAIA,EAAI,GACjB,GAAIC,KAAKD,EAAG,CACVE,QAAQC,KAAK,eACR,CACLD,QAAQC,KAAK,aAEfF,KAAKG,UAEPL,UACEG,QAAQC,KAAK,WAEfJ,UACEG,QAAQC,KAAK,aAEfJ,UACEG,QAAQC,KAAK,WAEfJ,UACEG,QAAQC,KAAK,cAIjB,MAAME,EAAI,IAAIP,IAAI,GAClB,MAAMQ,EAAI,IAAIR,IAAI,IAClBO,EAAEE",
|
||||||
|
"sourceRoot": "./"
|
||||||
|
}
|
4
test/fixtures/source-map/esm-basic.mjs
vendored
Normal file
4
test/fixtures/source-map/esm-basic.mjs
vendored
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
import {foo} from './esm-dep.mjs';
|
||||||
|
import {strictEqual} from 'assert';
|
||||||
|
strictEqual(foo(), 'foo');
|
||||||
|
//# sourceMappingURL=https://http.cat/405
|
4
test/fixtures/source-map/esm-dep.mjs
vendored
Normal file
4
test/fixtures/source-map/esm-dep.mjs
vendored
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
export function foo () {
|
||||||
|
return 'foo';
|
||||||
|
};
|
||||||
|
//# sourceMappingURL=https://http.cat/422
|
8
test/fixtures/source-map/exit-1.js
vendored
Normal file
8
test/fixtures/source-map/exit-1.js
vendored
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
const a = 99;
|
||||||
|
if (true) {
|
||||||
|
const b = 101;
|
||||||
|
} else {
|
||||||
|
const c = 102;
|
||||||
|
}
|
||||||
|
process.exit(1);
|
||||||
|
//# sourceMappingURL=https://http.cat/404
|
2
test/fixtures/source-map/inline-base64.js
vendored
Normal file
2
test/fixtures/source-map/inline-base64.js
vendored
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
var cov_263bu3eqm8=function(){var path= "./branches.js";var hash="424788076537d051b5bf0e2564aef393124eabc7";var global=new Function("return this")();var gcv="__coverage__";var coverageData={path: "./branches.js",statementMap:{"0":{start:{line:1,column:0},end:{line:7,column:1}},"1":{start:{line:2,column:2},end:{line:2,column:29}},"2":{start:{line:3,column:7},end:{line:7,column:1}},"3":{start:{line:4,column:2},end:{line:4,column:27}},"4":{start:{line:6,column:2},end:{line:6,column:29}},"5":{start:{line:10,column:2},end:{line:16,column:3}},"6":{start:{line:11,column:4},end:{line:11,column:28}},"7":{start:{line:12,column:9},end:{line:16,column:3}},"8":{start:{line:13,column:4},end:{line:13,column:31}},"9":{start:{line:15,column:4},end:{line:15,column:29}},"10":{start:{line:19,column:0},end:{line:19,column:12}},"11":{start:{line:20,column:0},end:{line:20,column:13}}},fnMap:{"0":{name:"branch",decl:{start:{line:9,column:9},end:{line:9,column:15}},loc:{start:{line:9,column:20},end:{line:17,column:1}},line:9}},branchMap:{"0":{loc:{start:{line:1,column:0},end:{line:7,column:1}},type:"if",locations:[{start:{line:1,column:0},end:{line:7,column:1}},{start:{line:1,column:0},end:{line:7,column:1}}],line:1},"1":{loc:{start:{line:3,column:7},end:{line:7,column:1}},type:"if",locations:[{start:{line:3,column:7},end:{line:7,column:1}},{start:{line:3,column:7},end:{line:7,column:1}}],line:3},"2":{loc:{start:{line:10,column:2},end:{line:16,column:3}},type:"if",locations:[{start:{line:10,column:2},end:{line:16,column:3}},{start:{line:10,column:2},end:{line:16,column:3}}],line:10},"3":{loc:{start:{line:12,column:9},end:{line:16,column:3}},type:"if",locations:[{start:{line:12,column:9},end:{line:16,column:3}},{start:{line:12,column:9},end:{line:16,column:3}}],line:12}},s:{"0":0,"1":0,"2":0,"3":0,"4":0,"5":0,"6":0,"7":0,"8":0,"9":0,"10":0,"11":0},f:{"0":0},b:{"0":[0,0],"1":[0,0],"2":[0,0],"3":[0,0]},_coverageSchema:"43e27e138ebf9cfc5966b082cf9a028302ed4184",hash:"424788076537d051b5bf0e2564aef393124eabc7"};var coverage=global[gcv]||(global[gcv]={});if(coverage[path]&&coverage[path].hash===hash){return coverage[path];}return coverage[path]=coverageData;}();cov_263bu3eqm8.s[0]++;if(false){cov_263bu3eqm8.b[0][0]++;cov_263bu3eqm8.s[1]++;console.info('unreachable');}else{cov_263bu3eqm8.b[0][1]++;cov_263bu3eqm8.s[2]++;if(true){cov_263bu3eqm8.b[1][0]++;cov_263bu3eqm8.s[3]++;console.info('reachable');}else{cov_263bu3eqm8.b[1][1]++;cov_263bu3eqm8.s[4]++;console.info('unreachable');}}function branch(a){cov_263bu3eqm8.f[0]++;cov_263bu3eqm8.s[5]++;if(a){cov_263bu3eqm8.b[2][0]++;cov_263bu3eqm8.s[6]++;console.info('a = true');}else{cov_263bu3eqm8.b[2][1]++;cov_263bu3eqm8.s[7]++;if(undefined){cov_263bu3eqm8.b[3][0]++;cov_263bu3eqm8.s[8]++;console.info('unreachable');}else{cov_263bu3eqm8.b[3][1]++;cov_263bu3eqm8.s[9]++;console.info('a = false');}}}cov_263bu3eqm8.s[10]++;branch(true);cov_263bu3eqm8.s[11]++;branch(false);
|
||||||
|
//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbIi4vYnJhbmNoZXMuanMiXSwibmFtZXMiOlsiY29uc29sZSIsImluZm8iLCJicmFuY2giLCJhIiwidW5kZWZpbmVkIl0sIm1hcHBpbmdzIjoic3VFQUFBLEdBQUksS0FBSixDQUFXLGdEQUNUQSxPQUFPLENBQUNDLElBQVIsQ0FBYSxhQUFiLEVBQ0QsQ0FGRCxJQUVPLG1EQUFJLElBQUosQ0FBVSxnREFDZkQsT0FBTyxDQUFDQyxJQUFSLENBQWEsV0FBYixFQUNELENBRk0sSUFFQSxnREFDTEQsT0FBTyxDQUFDQyxJQUFSLENBQWEsYUFBYixFQUNELEVBRUQsUUFBU0MsQ0FBQUEsTUFBVCxDQUFpQkMsQ0FBakIsQ0FBb0IsNkNBQ2xCLEdBQUlBLENBQUosQ0FBTyxnREFDTEgsT0FBTyxDQUFDQyxJQUFSLENBQWEsVUFBYixFQUNELENBRkQsSUFFTyxtREFBSUcsU0FBSixDQUFlLGdEQUNwQkosT0FBTyxDQUFDQyxJQUFSLENBQWEsYUFBYixFQUNELENBRk0sSUFFQSxnREFDTEQsT0FBTyxDQUFDQyxJQUFSLENBQWEsV0FBYixFQUNELEVBQ0YsQyx1QkFFREMsTUFBTSxDQUFDLElBQUQsQ0FBTixDLHVCQUNBQSxNQUFNLENBQUMsS0FBRCxDQUFOIiwic291cmNlc0NvbnRlbnQiOlsiaWYgKGZhbHNlKSB7XG4gIGNvbnNvbGUuaW5mbygndW5yZWFjaGFibGUnKVxufSBlbHNlIGlmICh0cnVlKSB7XG4gIGNvbnNvbGUuaW5mbygncmVhY2hhYmxlJylcbn0gZWxzZSB7XG4gIGNvbnNvbGUuaW5mbygndW5yZWFjaGFibGUnKVxufVxuXG5mdW5jdGlvbiBicmFuY2ggKGEpIHtcbiAgaWYgKGEpIHtcbiAgICBjb25zb2xlLmluZm8oJ2EgPSB0cnVlJylcbiAgfSBlbHNlIGlmICh1bmRlZmluZWQpIHtcbiAgICBjb25zb2xlLmluZm8oJ3VucmVhY2hhYmxlJylcbiAgfSBlbHNlIHtcbiAgICBjb25zb2xlLmluZm8oJ2EgPSBmYWxzZScpXG4gIH1cbn1cblxuYnJhbmNoKHRydWUpXG5icmFuY2goZmFsc2UpXG4iXX0=
|
8
test/fixtures/source-map/sigint.js
vendored
Normal file
8
test/fixtures/source-map/sigint.js
vendored
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
const a = 99;
|
||||||
|
if (true) {
|
||||||
|
const b = 101;
|
||||||
|
} else {
|
||||||
|
const c = 102;
|
||||||
|
}
|
||||||
|
process.kill(process.pid, "SIGINT");
|
||||||
|
//# sourceMappingURL=https://http.cat/402
|
@ -55,6 +55,7 @@ const expectedModules = new Set([
|
|||||||
'NativeModule internal/process/task_queues',
|
'NativeModule internal/process/task_queues',
|
||||||
'NativeModule internal/process/warning',
|
'NativeModule internal/process/warning',
|
||||||
'NativeModule internal/querystring',
|
'NativeModule internal/querystring',
|
||||||
|
'NativeModule internal/source_map',
|
||||||
'NativeModule internal/timers',
|
'NativeModule internal/timers',
|
||||||
'NativeModule internal/url',
|
'NativeModule internal/url',
|
||||||
'NativeModule internal/util',
|
'NativeModule internal/util',
|
||||||
|
132
test/parallel/test-source-map.js
Normal file
132
test/parallel/test-source-map.js
Normal file
@ -0,0 +1,132 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
if (!process.features.inspector) return;
|
||||||
|
|
||||||
|
const common = require('../common');
|
||||||
|
const assert = require('assert');
|
||||||
|
const { dirname } = require('path');
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
const { spawnSync } = require('child_process');
|
||||||
|
|
||||||
|
const tmpdir = require('../common/tmpdir');
|
||||||
|
tmpdir.refresh();
|
||||||
|
|
||||||
|
let dirc = 0;
|
||||||
|
function nextdir() {
|
||||||
|
return process.env.NODE_V8_COVERAGE ||
|
||||||
|
path.join(tmpdir.path, `source_map_${++dirc}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Outputs source maps when event loop is drained, with no async logic.
|
||||||
|
{
|
||||||
|
const coverageDirectory = nextdir();
|
||||||
|
const output = spawnSync(process.execPath, [
|
||||||
|
require.resolve('../fixtures/source-map/basic')
|
||||||
|
], { env: { ...process.env, NODE_V8_COVERAGE: coverageDirectory } });
|
||||||
|
if (output.status !== 0) {
|
||||||
|
console.log(output.stderr.toString());
|
||||||
|
}
|
||||||
|
assert.strictEqual(output.status, 0);
|
||||||
|
assert.strictEqual(output.stderr.toString(), '');
|
||||||
|
const sourceMap = getSourceMapFromCache('basic.js', coverageDirectory);
|
||||||
|
assert.strictEqual(sourceMap.url, 'https://http.cat/418');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Outputs source maps when process.kill(process.pid, "SIGINT"); exits process.
|
||||||
|
{
|
||||||
|
const coverageDirectory = nextdir();
|
||||||
|
const output = spawnSync(process.execPath, [
|
||||||
|
require.resolve('../fixtures/source-map/sigint')
|
||||||
|
], { env: { ...process.env, NODE_V8_COVERAGE: coverageDirectory } });
|
||||||
|
if (!common.isWindows) {
|
||||||
|
if (output.signal !== 'SIGINT') {
|
||||||
|
console.log(output.stderr.toString());
|
||||||
|
}
|
||||||
|
assert.strictEqual(output.signal, 'SIGINT');
|
||||||
|
}
|
||||||
|
assert.strictEqual(output.stderr.toString(), '');
|
||||||
|
const sourceMap = getSourceMapFromCache('sigint.js', coverageDirectory);
|
||||||
|
assert.strictEqual(sourceMap.url, 'https://http.cat/402');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Outputs source maps when source-file calls process.exit(1).
|
||||||
|
{
|
||||||
|
const coverageDirectory = nextdir();
|
||||||
|
const output = spawnSync(process.execPath, [
|
||||||
|
require.resolve('../fixtures/source-map/exit-1')
|
||||||
|
], { env: { ...process.env, NODE_V8_COVERAGE: coverageDirectory } });
|
||||||
|
assert.strictEqual(output.stderr.toString(), '');
|
||||||
|
const sourceMap = getSourceMapFromCache('exit-1.js', coverageDirectory);
|
||||||
|
assert.strictEqual(sourceMap.url, 'https://http.cat/404');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Outputs source-maps for esm module.
|
||||||
|
{
|
||||||
|
const coverageDirectory = nextdir();
|
||||||
|
const output = spawnSync(process.execPath, [
|
||||||
|
'--no-warnings',
|
||||||
|
'--experimental-modules',
|
||||||
|
require.resolve('../fixtures/source-map/esm-basic.mjs')
|
||||||
|
], { env: { ...process.env, NODE_V8_COVERAGE: coverageDirectory } });
|
||||||
|
assert.strictEqual(output.stderr.toString(), '');
|
||||||
|
const sourceMap = getSourceMapFromCache('esm-basic.mjs', coverageDirectory);
|
||||||
|
assert.strictEqual(sourceMap.url, 'https://http.cat/405');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Loads source-maps with relative path from .map file on disk.
|
||||||
|
{
|
||||||
|
const coverageDirectory = nextdir();
|
||||||
|
const output = spawnSync(process.execPath, [
|
||||||
|
require.resolve('../fixtures/source-map/disk-relative-path')
|
||||||
|
], { env: { ...process.env, NODE_V8_COVERAGE: coverageDirectory } });
|
||||||
|
assert.strictEqual(output.status, 0);
|
||||||
|
assert.strictEqual(output.stderr.toString(), '');
|
||||||
|
const sourceMap = getSourceMapFromCache(
|
||||||
|
'disk-relative-path.js',
|
||||||
|
coverageDirectory
|
||||||
|
);
|
||||||
|
// Source-map should have been loaded from disk and sources should have been
|
||||||
|
// rewritten, such that they're absolute paths.
|
||||||
|
assert.strictEqual(
|
||||||
|
dirname(
|
||||||
|
`file://${require.resolve('../fixtures/source-map/disk-relative-path')}`),
|
||||||
|
dirname(sourceMap.data.sources[0])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Loads source-maps from inline data URL.
|
||||||
|
{
|
||||||
|
const coverageDirectory = nextdir();
|
||||||
|
const output = spawnSync(process.execPath, [
|
||||||
|
require.resolve('../fixtures/source-map/inline-base64.js')
|
||||||
|
], { env: { ...process.env, NODE_V8_COVERAGE: coverageDirectory } });
|
||||||
|
assert.strictEqual(output.status, 0);
|
||||||
|
assert.strictEqual(output.stderr.toString(), '');
|
||||||
|
const sourceMap = getSourceMapFromCache(
|
||||||
|
'inline-base64.js',
|
||||||
|
coverageDirectory
|
||||||
|
);
|
||||||
|
// base64 JSON should have been decoded, and paths to sources should have
|
||||||
|
// been rewritten such that they're absolute:
|
||||||
|
assert.strictEqual(
|
||||||
|
dirname(
|
||||||
|
`file://${require.resolve('../fixtures/source-map/inline-base64')}`),
|
||||||
|
dirname(sourceMap.data.sources[0])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getSourceMapFromCache(fixtureFile, coverageDirectory) {
|
||||||
|
const jsonFiles = fs.readdirSync(coverageDirectory);
|
||||||
|
for (const jsonFile of jsonFiles) {
|
||||||
|
const maybeSourceMapCache = require(
|
||||||
|
path.join(coverageDirectory, jsonFile)
|
||||||
|
)['source-map-cache'] || {};
|
||||||
|
const keys = Object.keys(maybeSourceMapCache);
|
||||||
|
for (const key of keys) {
|
||||||
|
if (key.includes(fixtureFile)) {
|
||||||
|
return maybeSourceMapCache[key];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user