Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

module: use compileFunction over Module.wrap #21573

Closed
wants to merge 16 commits into from
Closed
Show file tree
Hide file tree
Changes from 12 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 0 additions & 11 deletions lib/assert.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,6 @@ let isDeepEqual;
let isDeepStrictEqual;
let parseExpressionAt;
let findNodeAround;
let columnOffset = 0;
let decoder;

function lazyLoadComparison() {
Expand Down Expand Up @@ -256,16 +255,6 @@ function getErrMessage(message, fn) {
const line = call.getLineNumber() - 1;
let column = call.getColumnNumber() - 1;

// Line number one reports the wrong column due to being wrapped in a
// function. Remove that offset to get the actual call.
if (line === 0) {
if (columnOffset === 0) {
const { wrapper } = require('internal/modules/cjs/loader');
columnOffset = wrapper[0].length;
}
column -= columnOffset;
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This part would still be important in case wrap or wrapper get monkey patched since the line would then not start at an offset of zero. It won't be a big issue though as the assertion will just fall back to the message false != true.


const identifier = `${filename}${line}${column}`;

if (errorCache.has(identifier)) {
Expand Down
11 changes: 11 additions & 0 deletions lib/internal/modules/cjs/helpers.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
'use strict';

const { validateString } = require('internal/validators');
const path = require('path');
const { pathToFileURL } = require('internal/url');
const { URL } = require('url');

const {
CHAR_LINE_FEED,
Expand Down Expand Up @@ -145,10 +148,18 @@ function addBuiltinLibsToObject(object) {
});
}

function normalizeReferrerURL(referrer) {
if (typeof referrer === 'string' && path.isAbsolute(referrer)) {
return pathToFileURL(referrer).href;
}
return new URL(referrer).href;
}

module.exports = exports = {
addBuiltinLibsToObject,
builtinLibs,
makeRequireFunction,
normalizeReferrerURL,
requireDepth: 0,
stripBOM,
stripShebang
Expand Down
106 changes: 83 additions & 23 deletions lib/internal/modules/cjs/loader.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,14 +29,14 @@ const assert = require('assert').ok;
const fs = require('fs');
const internalFS = require('internal/fs/utils');
const path = require('path');
const { URL } = require('url');
const {
internalModuleReadJSON,
internalModuleStat
} = internalBinding('fs');
const { safeGetenv } = internalBinding('credentials');
const {
makeRequireFunction,
normalizeReferrerURL,
requireDepth,
stripBOM,
stripShebang
Expand All @@ -48,6 +48,7 @@ const experimentalModules = getOptionValue('--experimental-modules');
const manifest = getOptionValue('--experimental-policy') ?
require('internal/process/policy').manifest :
null;
const { compileFunction } = internalBinding('contextify');

const {
ERR_INVALID_ARG_VALUE,
Expand Down Expand Up @@ -131,15 +132,52 @@ Module._extensions = Object.create(null);
var modulePaths = [];
Module.globalPaths = [];

Module.wrap = function(script) {
let patched = false;

// eslint-disable-next-line func-style
let wrap = function(script) {
return Module.wrapper[0] + script + Module.wrapper[1];
};

Module.wrapper = [
const wrapper = [
'(function (exports, require, module, __filename, __dirname) { ',
'\n});'
];

let wrapperProxy = new Proxy(wrapper, {
set(target, property, value, receiver) {
patched = true;
return Reflect.set(target, property, value, receiver);
},

defineProperty(target, property, descriptor) {
patched = true;
return Object.defineProperty(target, property, descriptor);
}
});

Object.defineProperty(Module, 'wrap', {
get() {
return wrap;
},

set(value) {
patched = true;
wrap = value;
}
});

Object.defineProperty(Module, 'wrapper', {
get() {
return wrapperProxy;
},

set(value) {
patched = true;
wrapperProxy = value;
}
});

const debug = util.debuglog('module');
ryzokuken marked this conversation as resolved.
Show resolved Hide resolved

Module._debug = util.deprecate(debug, 'Module._debug is deprecated.',
Expand Down Expand Up @@ -671,13 +709,6 @@ Module.prototype.require = function(id) {
// (needed for setting breakpoint when called with --inspect-brk)
var resolvedArgv;

function normalizeReferrerURL(referrer) {
if (typeof referrer === 'string' && path.isAbsolute(referrer)) {
return pathToFileURL(referrer).href;
}
return new URL(referrer).href;
}


// Run the file contents in the correct scope or sandbox. Expose
// the correct helper variables (require, module, exports) to
Expand All @@ -691,19 +722,48 @@ Module.prototype._compile = function(content, filename) {

content = stripShebang(content);

// create wrapper function
var wrapper = Module.wrap(content);

var compiledWrapper = vm.runInThisContext(wrapper, {
filename: filename,
lineOffset: 0,
displayErrors: true,
importModuleDynamically: experimentalModules ? async (specifier) => {
if (asyncESM === undefined) lazyLoadESM();
const loader = await asyncESM.loaderPromise;
return loader.import(specifier, normalizeReferrerURL(filename));
} : undefined,
});
let compiledWrapper;
if (patched) {
const wrapper = Module.wrap(content);
compiledWrapper = vm.runInThisContext(wrapper, {
filename,
lineOffset: 0,
displayErrors: true,
importModuleDynamically: experimentalModules ? async (specifier) => {
if (asyncESM === undefined) lazyLoadESM();
const loader = await asyncESM.loaderPromise;
return loader.import(specifier, normalizeReferrerURL(filename));
} : undefined,
});
} else {
compiledWrapper = compileFunction(
content,
filename,
0,
0,
undefined,
false,
undefined,
[],
[
'exports',
'require',
'module',
'__filename',
'__dirname',
]
);
if (experimentalModules) {
const { callbackMap } = internalBinding('module_wrap');
callbackMap.set(compiledWrapper, {
importModuleDynamically: async (specifier) => {
if (asyncESM === undefined) lazyLoadESM();
const loader = await asyncESM.loaderPromise;
return loader.import(specifier, normalizeReferrerURL(filename));
}
});
}
}

var inspectorWrapper = null;
if (process._breakFirstLine && process._eval == null) {
Expand Down
1 change: 0 additions & 1 deletion lib/internal/process/esm_loader.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
const {
callbackMap,
} = internalBinding('module_wrap');

guybedford marked this conversation as resolved.
Show resolved Hide resolved
const { pathToFileURL } = require('internal/url');
const Loader = require('internal/modules/esm/loader');
const {
Expand Down
3 changes: 3 additions & 0 deletions src/env-inl.h
Original file line number Diff line number Diff line change
Expand Up @@ -459,6 +459,9 @@ inline uint32_t Environment::get_next_module_id() {
inline uint32_t Environment::get_next_script_id() {
return script_id_counter_++;
}
inline uint32_t Environment::get_next_function_id() {
return function_id_counter_++;
}

Environment::ShouldNotAbortOnUncaughtScope::ShouldNotAbortOnUncaughtScope(
Environment* env)
Expand Down
3 changes: 3 additions & 0 deletions src/env.h
Original file line number Diff line number Diff line change
Expand Up @@ -703,9 +703,11 @@ class Environment {
std::unordered_map<uint32_t, loader::ModuleWrap*> id_to_module_map;
std::unordered_map<uint32_t, contextify::ContextifyScript*>
id_to_script_map;
std::unordered_map<uint32_t, v8::Persistent<v8::Function>> id_to_function_map;

inline uint32_t get_next_module_id();
inline uint32_t get_next_script_id();
inline uint32_t get_next_function_id();

std::unordered_map<std::string, const loader::PackageConfig>
package_json_cache;
Expand Down Expand Up @@ -972,6 +974,7 @@ class Environment {

uint32_t module_id_counter_ = 0;
uint32_t script_id_counter_ = 0;
uint32_t function_id_counter_ = 0;

AliasedBuffer<uint32_t, v8::Uint32Array> should_abort_on_uncaught_toggle_;
int should_not_abort_scope_counter_ = 0;
Expand Down
2 changes: 2 additions & 0 deletions src/module_wrap.cc
Original file line number Diff line number Diff line change
Expand Up @@ -752,6 +752,8 @@ static MaybeLocal<Promise> ImportModuleDynamically(
} else if (type == ScriptType::kModule) {
ModuleWrap* wrap = ModuleWrap::GetFromID(env, id);
object = wrap->object();
} else if (type == ScriptType::kFunction) {
object = env->id_to_function_map.find(id)->second.Get(iso);
} else {
UNREACHABLE();
}
Expand Down
1 change: 1 addition & 0 deletions src/module_wrap.h
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ enum PackageMainCheck : bool {
enum ScriptType : int {
kScript,
kModule,
kFunction,
};

enum HostDefinedOptions : int {
Expand Down
52 changes: 44 additions & 8 deletions src/node_contextify.cc
Original file line number Diff line number Diff line change
Expand Up @@ -286,6 +286,14 @@ void ContextifyContext::WeakCallback(
delete context;
}

void ContextifyContext::WeakCallbackCompileFn(
const WeakCallbackInfo<CompileFnEntry>& data) {
CompileFnEntry* entry = data.GetParameter();
entry->env->id_to_function_map[entry->id].Reset();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can't we just use node::Persistent here?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you clarify what you mean in the code?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@guybedford The suggestion is to replace v8::Persistent with node::Persistent (aka just Persistent in most of our code), because that one calls Reset() automatically when it is destroyed.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Btw, this does get interesting for CompileFnEntry objects that still exist when the Environment is torn down. We never delete these objects, and we don’t call the weak callbacks either. This definitely still leaks memory. :/

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ahh thanks so much for explaining, that makes sense. I've switched to the Node persistent now, please review. Also if you have a code suggestion for the Reset emplace issue that would be welcome too.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The point is pretty much just that it Reset()s automatically – which V8’s Persistent should have done from the beginning. (I assume they didn’t change it for backwards compatibility.)

We don’t keep track of all Persistent handles and which Environment – if any – they are associated with, so doing that would be tricky, too.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(Btw, I think I could come up with an example patch for the BaseObject suggestion, if that would help you.)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was just looking at implementing the second option of maintaining a set of the CompileFnEntry items on the env record to dispose.

If you think the BaseObject approach is preferable though that works too.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can feel free to do that, sure. :)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, I've implemented this in the latest commit. Review would be appreciated :)

Also to note again - this entire architecture should be changed soon, in case that isn't completely obvious.

entry->env->id_to_function_map.erase(entry->id);
delete entry;
}

// static
ContextifyContext* ContextifyContext::ContextFromContextifiedSandbox(
Environment* env,
Expand Down Expand Up @@ -1007,7 +1015,30 @@ void ContextifyContext::CompileFunction(
data + cached_data_buf->ByteOffset(), cached_data_buf->ByteLength());
}

ScriptOrigin origin(filename, line_offset, column_offset, True(isolate));
// Get the function id
uint32_t id = env->get_next_function_id();

// Set host_defined_options
Local<PrimitiveArray> host_defined_options =
PrimitiveArray::New(isolate, loader::HostDefinedOptions::kLength);
host_defined_options->Set(
isolate,
loader::HostDefinedOptions::kType,
Number::New(isolate, loader::ScriptType::kFunction));
host_defined_options->Set(
isolate, loader::HostDefinedOptions::kID, Number::New(isolate, id));

guybedford marked this conversation as resolved.
Show resolved Hide resolved
ScriptOrigin origin(filename,
line_offset, // line offset
column_offset, // column offset
True(isolate), // is cross origin
Local<Integer>(), // script id
Local<Value>(), // source map URL
False(isolate), // is opaque (?)
False(isolate), // is WASM
False(isolate), // is ES Module
host_defined_options);

ScriptCompiler::Source source(code, origin, cached_data);
ScriptCompiler::CompileOptions options;
if (source.GetCachedData() == nullptr) {
Expand Down Expand Up @@ -1041,38 +1072,43 @@ void ContextifyContext::CompileFunction(
}
}

MaybeLocal<Function> maybe_fun = ScriptCompiler::CompileFunctionInContext(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm so sorry I sucked the fun out of this... :P

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So mean of you...

MaybeLocal<Function> maybe_fn = ScriptCompiler::CompileFunctionInContext(
parsing_context, &source, params.size(), params.data(),
context_extensions.size(), context_extensions.data(), options);

Local<Function> fun;
if (maybe_fun.IsEmpty() || !maybe_fun.ToLocal(&fun)) {
if (maybe_fn.IsEmpty()) {
DecorateErrorStack(env, try_catch);
try_catch.ReThrow();
return;
}
Local<Function> fn = maybe_fn.ToLocalChecked();
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are you sure you want to do this? What if this assumption is false and this statement fails? Even if it doesn't, what's wrong about the way I originally intended to do it? I'm sorry but I am more cautious since such assumptions have kicked me in the balls in the past while working with the V8 API.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As far as I'm aware any Maybe that is definitely !IsEmpty() will always pass ToLocalChecked(), but the v8 docs are sparse enough for me not to be too confident on that one either :)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It should be fine to do this since there is an IsEmtpy guard above

env->id_to_function_map[id].Reset(isolate, fn);
Copy link
Member

@joyeecheung joyeecheung Feb 4, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be better to use .emplace() instead of relying on the implicit insertion of [] (CompileFnEntry could also be wrapped into a BaseObject like ContextifyScript so that it can be properly captured in the heap snapshot for debugging.)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I must admit this is where I have a C++/v8 knowledge gap - if I try env->id_to_function_map.emplace then, as far as I recall, the copy constructor error for Persistent kicks in and we can't use Persistent and instead need to use Persistent with copyable traits, which I know is just a change of the Persistent constructor but it's a truly ugly API and I wasn't sure how to do that in the above form so this seemed easier to avoid the copy constructor entirely.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(for what its worth this pattern comes from module_wrap.cc)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the idea would be to do something like this:

Suggested change
env->id_to_function_map[id].Reset(isolate, fn);
env->id_to_function_map.emplace(std::piecewise_construct,
std::make_tuple(id),
std::make_tuple(isolate, fn));

CompileFnEntry* gc_entry = new CompileFnEntry(env, id);
env->id_to_function_map[id].SetWeak(gc_entry,
WeakCallbackCompileFn,
v8::WeakCallbackType::kParameter);

if (produce_cached_data) {
const std::unique_ptr<ScriptCompiler::CachedData> cached_data(
ScriptCompiler::CreateCodeCacheForFunction(fun));
ScriptCompiler::CreateCodeCacheForFunction(fn));
bool cached_data_produced = cached_data != nullptr;
if (cached_data_produced) {
MaybeLocal<Object> buf = Buffer::Copy(
env,
reinterpret_cast<const char*>(cached_data->data),
cached_data->length);
if (fun->Set(
if (fn->Set(
parsing_context,
env->cached_data_string(),
buf.ToLocalChecked()).IsNothing()) return;
}
if (fun->Set(
if (fn->Set(
parsing_context,
env->cached_data_produced_string(),
Boolean::New(isolate, cached_data_produced)).IsNothing()) return;
}

args.GetReturnValue().Set(fun);
args.GetReturnValue().Set(fn);
}


Expand Down
7 changes: 7 additions & 0 deletions src/node_contextify.h
Original file line number Diff line number Diff line change
Expand Up @@ -55,12 +55,19 @@ class ContextifyContext {
static ContextifyContext* Get(const v8::PropertyCallbackInfo<T>& args);

private:
struct CompileFnEntry {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm sorry, but I don't understand what the need for this and WeakCallbackCompileFn is, since neither the scripts nor the modules are handled similarly. The way I originally implemented dynamic modules, it worked without the need for these.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So the only fix needed to your previous work is the change of making the Function reference Persistent<Function>, but the problem with that as discussed on IRC is that then we have a memory leak as these are never cleaned up!

So the WeakCallbackCompileFn is a GC callback that then cleans up the map entry, so that we do not have a Node.js core memory leak :) This was a compromise approach discussed exhaustively with @devsnek to get it to work but not memory leak.

Environment* env;
uint32_t id;
CompileFnEntry(Environment* env, uint32_t id): env(env), id(id) {}
};
static void MakeContext(const v8::FunctionCallbackInfo<v8::Value>& args);
static void IsContext(const v8::FunctionCallbackInfo<v8::Value>& args);
static void CompileFunction(
const v8::FunctionCallbackInfo<v8::Value>& args);
static void WeakCallback(
const v8::WeakCallbackInfo<ContextifyContext>& data);
static void WeakCallbackCompileFn(
guybedford marked this conversation as resolved.
Show resolved Hide resolved
const v8::WeakCallbackInfo<CompileFnEntry>& data);
static void PropertyGetterCallback(
v8::Local<v8::Name> property,
const v8::PropertyCallbackInfo<v8::Value>& args);
Expand Down
9 changes: 9 additions & 0 deletions test/fixtures/cjs-module-wrap.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
const assert = require('assert');
const m = require('module');

global.mwc = 0;
m.wrapper[0] += 'global.mwc = (global.mwc || 0 ) + 1;';

require('./not-main-module.js');
assert.strictEqual(mwc, 1);
delete global.mwc;
Loading