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

vm: add code cache support for SourceTextModule #31278

Merged
merged 1 commit into from Jan 12, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
10 changes: 10 additions & 0 deletions doc/api/errors.md
Expand Up @@ -1988,6 +1988,16 @@ the following reasons:
* It is being linked (`linkingStatus` is `'linking'`)
* Linking has failed for this module (`linkingStatus` is `'errored'`)

<a id="ERR_VM_MODULE_CACHED_DATA_REJECTED"></a>
### `ERR_VM_MODULE_CACHED_DATA_REJECTED`

The `cachedData` option passed to a module constructor is invalid.

<a id="ERR_VM_MODULE_CANNOT_CREATE_CACHED_DATA"></a>
### `ERR_VM_MODULE_CANNOT_CREATE_CACHED_DATA`

Cached data cannot be created for modules which have already been evaluated.

<a id="ERR_VM_MODULE_DIFFERENT_CONTEXT"></a>
### `ERR_VM_MODULE_DIFFERENT_CONTEXT`

Expand Down
26 changes: 26 additions & 0 deletions doc/api/vm.md
Expand Up @@ -566,6 +566,10 @@ defined in the ECMAScript specification.
* `identifier` {string} String used in stack traces.
**Default:** `'vm:module(i)'` where `i` is a context-specific ascending
index.
* `cachedData` {Buffer|TypedArray|DataView} Provides an optional `Buffer` or
`TypedArray`, or `DataView` with V8's code cache data for the supplied
source. The `code` must be the same as the module from which this
`cachedData` was created.
* `context` {Object} The [contextified][] object as returned by the
`vm.createContext()` method, to compile and evaluate this `Module` in.
* `lineOffset` {integer} Specifies the line number offset that is displayed
Expand Down Expand Up @@ -621,6 +625,28 @@ const contextifiedObject = vm.createContext({ secret: 42 });
})();
```

### `sourceTextModule.createCachedData()`
<!-- YAML
added: REPLACEME
-->

* Returns: {Buffer}

Creates a code cache that can be used with the SourceTextModule constructor's
`cachedData` option. Returns a Buffer. This method may be called any number
of times before the module has been evaluated.

```js
// Create an initial module
const module = new vm.SourceTextModule('const a = 1;');

// Create cached data from this module
const cachedData = module.createCachedData();

// Create a new module using the cached data. The code must be the same.
const module2 = new vm.SourceTextModule('const a = 1;', { cachedData });
```

## Class: `vm.SyntheticModule`
<!-- YAML
added: v13.0.0
Expand Down
2 changes: 2 additions & 0 deletions lib/internal/errors.js
Expand Up @@ -1351,6 +1351,8 @@ E('ERR_VALID_PERFORMANCE_ENTRY_TYPE',
E('ERR_VM_DYNAMIC_IMPORT_CALLBACK_MISSING',
'A dynamic import callback was not specified.', TypeError);
E('ERR_VM_MODULE_ALREADY_LINKED', 'Module has already been linked', Error);
E('ERR_VM_MODULE_CANNOT_CREATE_CACHED_DATA',
'Cached data cannot be created for a module which has been evaluated', Error);
E('ERR_VM_MODULE_DIFFERENT_CONTEXT',
'Linked modules must use the same context', Error);
E('ERR_VM_MODULE_LINKING_ERRORED',
Expand Down
29 changes: 27 additions & 2 deletions lib/internal/vm/module.js
Expand Up @@ -11,7 +11,10 @@ const {
} = primordials;

const { isContext } = internalBinding('contextify');
const { isModuleNamespaceObject } = require('internal/util/types');
const {
isModuleNamespaceObject,
isArrayBufferView,
} = require('internal/util/types');
const {
getConstructorOf,
customInspectSymbol,
Expand All @@ -21,6 +24,7 @@ const {
ERR_INVALID_ARG_TYPE,
ERR_VM_MODULE_ALREADY_LINKED,
ERR_VM_MODULE_DIFFERENT_CONTEXT,
ERR_VM_MODULE_CANNOT_CREATE_CACHED_DATA,
ERR_VM_MODULE_LINKING_ERRORED,
ERR_VM_MODULE_NOT_MODULE,
ERR_VM_MODULE_STATUS,
Expand Down Expand Up @@ -107,7 +111,8 @@ class Module {

if (sourceText !== undefined) {
this[kWrap] = new ModuleWrap(identifier, context, sourceText,
options.lineOffset, options.columnOffset);
options.lineOffset, options.columnOffset,
options.cachedData);

binding.callbackMap.set(this[kWrap], {
initializeImportMeta: options.initializeImportMeta,
Expand Down Expand Up @@ -253,6 +258,7 @@ class SourceTextModule extends Module {
importModuleDynamically,
context,
identifier,
cachedData,
} = options;

validateInt32(lineOffset, 'options.lineOffset');
Expand All @@ -271,12 +277,21 @@ class SourceTextModule extends Module {
importModuleDynamically);
}

if (cachedData !== undefined && !isArrayBufferView(cachedData)) {
throw new ERR_INVALID_ARG_TYPE(
'options.cachedData',
['Buffer', 'TypedArray', 'DataView'],
cachedData
);
}

super({
sourceText,
context,
identifier,
lineOffset,
columnOffset,
cachedData,
initializeImportMeta,
importModuleDynamically,
});
Expand Down Expand Up @@ -348,6 +363,16 @@ class SourceTextModule extends Module {
}
return super.error;
}

createCachedData() {
const { status } = this;
if (status === 'evaluating' ||
status === 'evaluated' ||
status === 'errored') {
BridgeAR marked this conversation as resolved.
Show resolved Hide resolved
throw new ERR_VM_MODULE_CANNOT_CREATE_CACHED_DATA();
}
return this[kWrap].createCachedData();
}
}

class SyntheticModule extends Module {
Expand Down
73 changes: 65 additions & 8 deletions src/module_wrap.cc
Expand Up @@ -2,12 +2,13 @@

#include "env.h"
#include "memory_tracker-inl.h"
#include "node_contextify.h"
#include "node_errors.h"
#include "node_internals.h"
#include "node_process.h"
#include "node_url.h"
#include "util-inl.h"
#include "node_contextify.h"
#include "node_watchdog.h"
#include "node_process.h"
#include "util-inl.h"

#include <sys/stat.h> // S_IFDIR

Expand All @@ -22,6 +23,7 @@ using node::contextify::ContextifyContext;
using node::url::URL;
using node::url::URL_FLAGS_FAILED;
using v8::Array;
using v8::ArrayBufferView;
using v8::Context;
using v8::Function;
using v8::FunctionCallbackInfo;
Expand All @@ -44,6 +46,7 @@ using v8::Promise;
using v8::ScriptCompiler;
using v8::ScriptOrigin;
using v8::String;
using v8::UnboundModuleScript;
using v8::Undefined;
using v8::Value;

Expand Down Expand Up @@ -131,7 +134,7 @@ void ModuleWrap::New(const FunctionCallbackInfo<Value>& args) {
// new ModuleWrap(url, context, exportNames, syntheticExecutionFunction)
CHECK(args[3]->IsFunction());
} else {
// new ModuleWrap(url, context, source, lineOffset, columOffset)
// new ModuleWrap(url, context, source, lineOffset, columOffset, cachedData)
CHECK(args[2]->IsString());
CHECK(args[3]->IsNumber());
line_offset = args[3].As<Integer>();
Expand Down Expand Up @@ -167,6 +170,17 @@ void ModuleWrap::New(const FunctionCallbackInfo<Value>& args) {
module = Module::CreateSyntheticModule(isolate, url, export_names,
SyntheticModuleEvaluationStepsCallback);
} else {
ScriptCompiler::CachedData* cached_data = nullptr;
if (!args[5]->IsUndefined()) {
CHECK(args[5]->IsArrayBufferView());
Local<ArrayBufferView> cached_data_buf = args[5].As<ArrayBufferView>();
uint8_t* data = static_cast<uint8_t*>(
cached_data_buf->Buffer()->GetBackingStore()->Data());
cached_data =
new ScriptCompiler::CachedData(data + cached_data_buf->ByteOffset(),
cached_data_buf->ByteLength());
}

Local<String> source_text = args[2].As<String>();
ScriptOrigin origin(url,
line_offset, // line offset
Expand All @@ -178,8 +192,15 @@ void ModuleWrap::New(const FunctionCallbackInfo<Value>& args) {
False(isolate), // is WASM
True(isolate), // is ES Module
host_defined_options);
ScriptCompiler::Source source(source_text, origin);
if (!ScriptCompiler::CompileModule(isolate, &source).ToLocal(&module)) {
ScriptCompiler::Source source(source_text, origin, cached_data);
ScriptCompiler::CompileOptions options;
if (source.GetCachedData() == nullptr) {
options = ScriptCompiler::kNoCompileOptions;
} else {
options = ScriptCompiler::kConsumeCodeCache;
}
if (!ScriptCompiler::CompileModule(isolate, &source, options)
.ToLocal(&module)) {
if (try_catch.HasCaught() && !try_catch.HasTerminated()) {
CHECK(!try_catch.Message().IsEmpty());
CHECK(!try_catch.Exception().IsEmpty());
Expand All @@ -189,6 +210,13 @@ void ModuleWrap::New(const FunctionCallbackInfo<Value>& args) {
}
return;
}
if (options == ScriptCompiler::kConsumeCodeCache &&
source.GetCachedData()->rejected) {
THROW_ERR_VM_MODULE_CACHED_DATA_REJECTED(
env, "cachedData buffer was rejected");
try_catch.ReThrow();
return;
}
}
}

Expand Down Expand Up @@ -1507,8 +1535,7 @@ MaybeLocal<Value> ModuleWrap::SyntheticModuleEvaluationStepsCallback(
return ret;
}

void ModuleWrap::SetSyntheticExport(
const v8::FunctionCallbackInfo<v8::Value>& args) {
void ModuleWrap::SetSyntheticExport(const FunctionCallbackInfo<Value>& args) {
Isolate* isolate = args.GetIsolate();
Local<Object> that = args.This();

Expand All @@ -1528,6 +1555,35 @@ void ModuleWrap::SetSyntheticExport(
USE(module->SetSyntheticModuleExport(isolate, export_name, export_value));
}

void ModuleWrap::CreateCachedData(const FunctionCallbackInfo<Value>& args) {
Isolate* isolate = args.GetIsolate();
Local<Object> that = args.This();

ModuleWrap* obj;
ASSIGN_OR_RETURN_UNWRAP(&obj, that);

CHECK(!obj->synthetic_);

Local<Module> module = obj->module_.Get(isolate);

CHECK_LT(module->GetStatus(), v8::Module::Status::kEvaluating);

Local<UnboundModuleScript> unbound_module_script =
module->GetUnboundModuleScript();
std::unique_ptr<ScriptCompiler::CachedData> cached_data(
ScriptCompiler::CreateCodeCache(unbound_module_script));
Environment* env = Environment::GetCurrent(args);
if (!cached_data) {
args.GetReturnValue().Set(Buffer::New(env, 0).ToLocalChecked());
} else {
MaybeLocal<Object> buf =
Buffer::Copy(env,
reinterpret_cast<const char*>(cached_data->data),
cached_data->length);
args.GetReturnValue().Set(buf.ToLocalChecked());
}
}

void ModuleWrap::Initialize(Local<Object> target,
Local<Value> unused,
Local<Context> context,
Expand All @@ -1543,6 +1599,7 @@ void ModuleWrap::Initialize(Local<Object> target,
env->SetProtoMethod(tpl, "instantiate", Instantiate);
env->SetProtoMethod(tpl, "evaluate", Evaluate);
env->SetProtoMethod(tpl, "setExport", SetSyntheticExport);
env->SetProtoMethodNoSideEffect(tpl, "createCachedData", CreateCachedData);
env->SetProtoMethodNoSideEffect(tpl, "getNamespace", GetNamespace);
env->SetProtoMethodNoSideEffect(tpl, "getStatus", GetStatus);
env->SetProtoMethodNoSideEffect(tpl, "getError", GetError);
Expand Down
1 change: 1 addition & 0 deletions src/module_wrap.h
Expand Up @@ -75,6 +75,7 @@ class ModuleWrap : public BaseObject {
v8::Local<v8::Context> context, v8::Local<v8::Module> module);
static void SetSyntheticExport(
const v8::FunctionCallbackInfo<v8::Value>& args);
static void CreateCachedData(const v8::FunctionCallbackInfo<v8::Value>& args);

static v8::MaybeLocal<v8::Module> ResolveCallback(
v8::Local<v8::Context> context,
Expand Down
1 change: 1 addition & 0 deletions src/node_errors.h
Expand Up @@ -59,6 +59,7 @@ void PrintErrorString(const char* format, ...);
V(ERR_TLS_INVALID_PROTOCOL_METHOD, TypeError) \
V(ERR_TRANSFERRING_EXTERNALIZED_SHAREDARRAYBUFFER, TypeError) \
V(ERR_TLS_PSK_SET_IDENTIY_HINT_FAILED, Error) \
V(ERR_VM_MODULE_CACHED_DATA_REJECTED, Error) \

#define V(code, type) \
inline v8::Local<v8::Value> code(v8::Isolate* isolate, \
Expand Down
30 changes: 30 additions & 0 deletions test/parallel/test-vm-module-cached-data.js
@@ -0,0 +1,30 @@
'use strict';

// Flags: --experimental-vm-modules

require('../common');

const assert = require('assert');
const { SourceTextModule } = require('vm');

{
const m = new SourceTextModule('const a = 1');
const cachedData = m.createCachedData();

new SourceTextModule('const a = 1', { cachedData });

assert.throws(() => {
new SourceTextModule('differentSource', { cachedData });
}, {
code: 'ERR_VM_MODULE_CACHED_DATA_REJECTED',
});
}

assert.rejects(async () => {
const m = new SourceTextModule('const a = 1');
await m.link(() => {});
m.evaluate();
m.createCachedData();
}, {
code: 'ERR_VM_MODULE_CANNOT_CREATE_CACHED_DATA',
});