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

tools: js2c-cache #4777

Closed
wants to merge 4 commits into from
Closed
Show file tree
Hide file tree
Changes from all 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
2 changes: 2 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -532,6 +532,8 @@ CPPLINT_FILES = $(filter-out $(CPPLINT_EXCLUDE), $(wildcard \
test/addons/*/*.h \
tools/icu/*.cc \
tools/icu/*.h \
tools/*.cc \
tools/*.h \
))

cpplint:
Expand Down
6 changes: 6 additions & 0 deletions doc/api/vm.markdown
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,12 @@ The options when creating a script are:
code are controlled by the options to the script's methods.
- `timeout`: a number of milliseconds to execute `code` before terminating
execution. If execution is terminated, an [`Error`][] will be thrown.
- `cachedData`: an optional `Buffer` with V8's code cache data for the supplied
source. When supplied `cachedDataRejected` value will be set to either
`true` or `false` depending on acceptance of the data by V8.
Copy link

Choose a reason for hiding this comment

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

What are the factors that determine whether or not V8 will accept the cached data or not?

I'm trying to better understand it from reading the test cases below, but will V8 only accept the cached data if the script to be compiled is the same?

Copy link
Member Author

Choose a reason for hiding this comment

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

There are several of them:

  • script source should be the same
  • V8 version should be the same
  • V8 command line arguments should be the same
  • CPU features should be the same

Copy link
Member

Choose a reason for hiding this comment

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

CPU features should be the same

Doesn't that make it useless for release builds? The CPU features of the system the release binary is built on will be different from the systems it runs on.

Copy link
Member Author

Choose a reason for hiding this comment

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

I thought about this too, but I believe that there is much less CPU feature variety around us now. Especially on OS X.

We should definitely check this.

Copy link
Member

Choose a reason for hiding this comment

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

Not sure I agree. Take AVX: only newer Macbooks support that.

Copy link
Member Author

Choose a reason for hiding this comment

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

@bnoordhuis mmm... and what do you suggest?

Copy link
Contributor

Choose a reason for hiding this comment

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

CPU feature matching is a perennial problem in JVMs that support AOT compilation as well. Usually the solution for such AOT compilation is to produce the AOT code on the first invocation on a given machine and then persist it so that subsequent invocations can use the results of the first compilation. I am not sure the complexity is worth it in this case for Node, specially because the JVM class libraries tend to be much larger than the node core API, so AOT compilation has a bigger payoff on the JVMs.

This also doesn't help in scenarios where people run node inside containers where you don't expect repeated invocations of node.

Copy link
Member Author

Choose a reason for hiding this comment

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

@ofrobots This API could be used to produce such caching data. I guess we can just strip down the second commit and land it without it. Perhaps, V8 could be improved a bit, though, and rely only on the features that were actually used in the code, or allow compiling with reduced set of CPU features and using it on CPU that actually have wider feature support.

Copy link
Contributor

Choose a reason for hiding this comment

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

allow compiling with reduced set of CPU features and using it on CPU that actually have wider feature support.

This would leave performance on the table. You would want to use all the CPU features available on the machine that the code is actually running on. Over and above the CPU feature set (the architecture), modern JIT compilers tune code to the exact micro-architecture the code is going to be running on. For example, the IBM Power 5 and IBM Power 6 were not very different on the surface from CPU feature set point of view, but are radically different micro-architectures. Code compiled with the Power 5 feature set works fine on Power 6 but you can get significant performance improvements by tweaking the instruction selection and scheduling if you knew the code is going to run on a Power 6.

Shipping code built in the lab will leave these performance opportunities on the table.

- `produceCachedData`: if `true` and no `cachedData` is present - a `Buffer`
with V8's code cache data will be produced and stored in `cachedData` property
of the returned `vm.Script` instance.

### script.runInContext(contextifiedSandbox[, options])

Expand Down
29 changes: 27 additions & 2 deletions node.gyp
Original file line number Diff line number Diff line change
Expand Up @@ -502,6 +502,13 @@
{
'target_name': 'node_js2c',
'type': 'none',
'conditions': [
[ 'target_arch == host_arch', {
'dependencies': [
'js2c-cache#target',
],
}],
],
'toolsets': ['host'],
'actions': [
{
Expand All @@ -522,7 +529,10 @@
}],
[ 'node_use_perfctr=="false"', {
'inputs': [ 'src/perfctr_macros.py' ]
}]
}],
[ 'target_arch == host_arch', {
'inputs': [ '<(PRODUCT_DIR)/js2c-cache' ],
}],
],
'action': [
'<(python)',
Expand All @@ -533,6 +543,21 @@
},
],
}, # end node_js2c
{
'target_name': 'js2c-cache',
'type': 'executable',
'dependencies': [
'deps/v8/tools/gyp/v8.gyp:v8',
'deps/v8/tools/gyp/v8.gyp:v8_libplatform'
],
'include_dirs': [
'tools',
'deps/v8/include',
],
'sources': [
'tools/js2c-cache.cc',
],
}, # end js2c-cache
{
'target_name': 'node_dtrace_header',
'type': 'none',
Expand Down Expand Up @@ -694,7 +719,7 @@
'sources': [
'test/cctest/util.cc',
],
}
},
], # end targets

'conditions': [
Expand Down
3 changes: 3 additions & 0 deletions src/env.h
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,8 @@ namespace node {
V(buffer_string, "buffer") \
V(bytes_string, "bytes") \
V(bytes_parsed_string, "bytesParsed") \
V(cached_data_string, "cachedData") \
V(cached_data_rejected_string, "cachedDataRejected") \
V(callback_string, "callback") \
V(change_string, "change") \
V(oncertcb_string, "oncertcb") \
Expand Down Expand Up @@ -175,6 +177,7 @@ namespace node {
V(preference_string, "preference") \
V(priority_string, "priority") \
V(processed_string, "processed") \
V(produce_cached_data_string, "produceCachedData") \
V(prototype_string, "prototype") \
V(raw_string, "raw") \
V(rdev_string, "rdev") \
Expand Down
20 changes: 13 additions & 7 deletions src/node.cc
Original file line number Diff line number Diff line change
Expand Up @@ -114,13 +114,15 @@ using v8::Integer;
using v8::Isolate;
using v8::Local;
using v8::Locker;
using v8::MaybeLocal;
using v8::Message;
using v8::Number;
using v8::Object;
using v8::ObjectTemplate;
using v8::Promise;
using v8::PromiseRejectMessage;
using v8::PropertyCallbackInfo;
using v8::ScriptCompiler;
using v8::SealHandleScope;
using v8::StackFrame;
using v8::StackTrace;
Expand Down Expand Up @@ -1586,23 +1588,27 @@ static void ReportException(Environment* env, const TryCatch& try_catch) {


// Executes a str within the current v8 context.
static Local<Value> ExecuteString(Environment* env,
Local<String> source,
Local<String> filename) {
static Local<Value> ExecuteSource(Environment* env,
ScriptCompiler::Source* source) {
EscapableHandleScope scope(env->isolate());
TryCatch try_catch;

// try_catch must be nonverbose to disable FatalException() handler,
// we will handle exceptions ourself.
try_catch.SetVerbose(false);

Local<v8::Script> script = v8::Script::Compile(source, filename);
if (script.IsEmpty()) {
MaybeLocal<v8::Script> maybe_script = v8::ScriptCompiler::Compile(
env->context(),
source,
source->GetCachedData() == nullptr ? ScriptCompiler::kNoCompileOptions :
ScriptCompiler::kConsumeCodeCache);
delete source;
if (maybe_script.IsEmpty()) {
ReportException(env, try_catch);
exit(3);
}

Local<Value> result = script->Run();
Local<Value> result = maybe_script.ToLocalChecked()->Run();
if (result.IsEmpty()) {
ReportException(env, try_catch);
exit(4);
Expand Down Expand Up @@ -3157,7 +3163,7 @@ void LoadEnvironment(Environment* env) {
try_catch.SetVerbose(false);

Local<String> script_name = FIXED_ONE_BYTE_STRING(env->isolate(), "node.js");
Local<Value> f_value = ExecuteString(env, MainSource(env), script_name);
Local<Value> f_value = ExecuteSource(env, MainSource(env, script_name));
if (try_catch.HasCaught()) {
ReportException(env, try_catch);
exit(10);
Expand Down
6 changes: 6 additions & 0 deletions src/node.js
Original file line number Diff line number Diff line change
Expand Up @@ -972,6 +972,10 @@
return NativeModule._source[id];
};

NativeModule.getSourceCachedData = function(id) {
return NativeModule._source[id + '__cached_data'];
};

NativeModule.wrap = function(script) {
return NativeModule.wrapper[0] + script + NativeModule.wrapper[1];
};
Expand All @@ -983,10 +987,12 @@

NativeModule.prototype.compile = function() {
var source = NativeModule.getSource(this.id);
var cachedData = NativeModule.getSourceCachedData(this.id);
source = NativeModule.wrap(source);

var fn = runInThisContext(source, {
filename: this.filename,
cachedData: cachedData,
lineOffset: 0
});
fn(this.exports, NativeModule.require, this, this.filename);
Expand Down
78 changes: 75 additions & 3 deletions src/node_contextify.cc
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ namespace node {

using v8::AccessType;
using v8::Array;
using v8::ArrayBuffer;
using v8::Boolean;
using v8::Context;
using v8::Debug;
Expand Down Expand Up @@ -40,6 +41,7 @@ using v8::ScriptCompiler;
using v8::ScriptOrigin;
using v8::String;
using v8::TryCatch;
using v8::Uint8Array;
using v8::UnboundScript;
using v8::V8;
using v8::Value;
Expand Down Expand Up @@ -507,15 +509,35 @@ class ContextifyScript : public BaseObject {
Local<Integer> lineOffset = GetLineOffsetArg(args, 1);
Local<Integer> columnOffset = GetColumnOffsetArg(args, 1);
bool display_errors = GetDisplayErrorsArg(args, 1);
MaybeLocal<Uint8Array> cached_data_buf = GetCachedData(env, args, 1);
bool produce_cached_data = GetProduceCachedData(env, args, 1);
if (try_catch.HasCaught()) {
try_catch.ReThrow();
return;
}

ScriptCompiler::CachedData* cached_data = nullptr;
if (!cached_data_buf.IsEmpty()) {
ArrayBuffer::Contents contents =
cached_data_buf.ToLocalChecked()->Buffer()->GetContents();
cached_data = new ScriptCompiler::CachedData(
reinterpret_cast<uint8_t*>(contents.Data()), contents.ByteLength());
}

ScriptOrigin origin(filename, lineOffset, columnOffset);
ScriptCompiler::Source source(code, origin);
Local<UnboundScript> v8_script =
ScriptCompiler::CompileUnbound(env->isolate(), &source);
ScriptCompiler::Source source(code, origin, cached_data);
ScriptCompiler::CompileOptions compile_options =
ScriptCompiler::kNoCompileOptions;

if (source.GetCachedData() != nullptr)
compile_options = ScriptCompiler::kConsumeCodeCache;
else if (produce_cached_data)
compile_options = ScriptCompiler::kProduceCodeCache;

Local<UnboundScript> v8_script = ScriptCompiler::CompileUnbound(
env->isolate(),
&source,
compile_options);

if (v8_script.IsEmpty()) {
if (display_errors) {
Expand All @@ -525,6 +547,19 @@ class ContextifyScript : public BaseObject {
return;
}
contextify_script->script_.Reset(env->isolate(), v8_script);

if (compile_options == ScriptCompiler::kConsumeCodeCache) {
args.This()->Set(
env->cached_data_rejected_string(),
Boolean::New(env->isolate(), source.GetCachedData()->rejected));
} else if (compile_options == ScriptCompiler::kProduceCodeCache) {
const ScriptCompiler::CachedData* cached_data = source.GetCachedData();
MaybeLocal<Object> buf = Buffer::Copy(
env,
reinterpret_cast<const char*>(cached_data->data),
cached_data->length);
args.This()->Set(env->cached_data_string(), buf.ToLocalChecked());
Copy link
Contributor

Choose a reason for hiding this comment

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

args.Holder()?

EDIT: forget this for now. investigating something.

Copy link
Member Author

Choose a reason for hiding this comment

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

Want to share it with me? We could investigate together ;)

}
}


Expand Down Expand Up @@ -677,6 +712,43 @@ class ContextifyScript : public BaseObject {
}


static MaybeLocal<Uint8Array> GetCachedData(
Environment* env,
const FunctionCallbackInfo<Value>& args,
const int i) {
if (!args[i]->IsObject()) {
return MaybeLocal<Uint8Array>();
}
Local<Value> value = args[i].As<Object>()->Get(env->cached_data_string());
if (value->IsUndefined()) {
return MaybeLocal<Uint8Array>();
}

if (!value->IsUint8Array()) {
Environment::ThrowTypeError(
args.GetIsolate(),
"options.cachedData must be a Buffer instance");
return MaybeLocal<Uint8Array>();
}

return value.As<Uint8Array>();
}


static bool GetProduceCachedData(
Environment* env,
const FunctionCallbackInfo<Value>& args,
const int i) {
if (!args[i]->IsObject()) {
return false;
}
Local<Value> value =
args[i].As<Object>()->Get(env->produce_cached_data_string());

return value->IsTrue();
}


static Local<Integer> GetLineOffsetArg(
const FunctionCallbackInfo<Value>& args,
const int i) {
Expand Down
41 changes: 36 additions & 5 deletions src/node_javascript.cc
Original file line number Diff line number Diff line change
Expand Up @@ -11,26 +11,57 @@

namespace node {

using v8::ArrayBuffer;
using v8::HandleScope;
using v8::Local;
using v8::Object;
using v8::ScriptCompiler;
using v8::ScriptOrigin;
using v8::String;
using v8::Uint8Array;

Local<String> MainSource(Environment* env) {
return OneByteString(env->isolate(), node_native, sizeof(node_native) - 1);
ScriptCompiler::Source* MainSource(Environment* env, Local<String> filename) {
Local<String> source_str =
OneByteString(env->isolate(), node_native, sizeof(node_native) - 1);

ScriptOrigin origin(filename);
ScriptCompiler::CachedData* cached_data = nullptr;

// NOTE: It is illegal to have empty arrays in C, so we use { 0 } for this
// purposes.
if (sizeof(node_native_cache) <= 1) {
cached_data = new ScriptCompiler::CachedData(node_native_cache,
sizeof(node_native_cache));
}

return new ScriptCompiler::Source(source_str, origin, cached_data);
}

void DefineJavaScript(Environment* env, Local<Object> target) {
HandleScope scope(env->isolate());

Local<String> cache_postfix =
String::NewFromUtf8(env->isolate(), "__cached_data");

for (int i = 0; natives[i].name; i++) {
if (natives[i].source != node_native) {
Local<String> name = String::NewFromUtf8(env->isolate(), natives[i].name);
Local<String> source = String::NewFromUtf8(env->isolate(),
natives[i].source,
String::kNormalString,
natives[i].source_len);
natives[i].source,
String::kNormalString,
natives[i].source_len);
target->Set(name, source);

if (natives[i].cache_len <= 1)
continue;

Local<ArrayBuffer> cache_ab = ArrayBuffer::New(env->isolate(),
const_cast<unsigned char*>(natives[i].cache),
natives[i].cache_len);
Local<Uint8Array> cache =
Uint8Array::New(cache_ab, 0, natives[i].cache_len);

target->Set(String::Concat(name, cache_postfix), cache);
}
}
}
Expand Down
3 changes: 2 additions & 1 deletion src/node_javascript.h
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@
namespace node {

void DefineJavaScript(Environment* env, v8::Local<v8::Object> target);
v8::Local<v8::String> MainSource(Environment* env);
v8::ScriptCompiler::Source* MainSource(Environment* env,
v8::Local<v8::String> filename);

} // namespace node

Expand Down
Loading