Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit 9a3cd9e

Browse files
kenny-yGabriel Schulhof
authored and
Gabriel Schulhof
committedJul 11, 2018
n-api: improve runtime perf of n-api func call
Added a new struct CallbackBundle to eliminate all GetInternalField() calls. The principle is to store all required data inside a C++ struct, and then store the pointer in the JavaScript object. Before this change, the required data are stored in the JavaScript object in 3 or 4 seperate pointers. For every napi fun call, 3 of them have to be fetched out, which are 3 GetInternalField() calls; after this change, the C++ struct will be directly fetched out by using v8::External::Value(), which is faster. Profiling data show that GetInternalField() is slow. On an i7-4770K (3.50GHz) box, a C++ V8-binding fun call is 8 ns, before this change, napi fun call is 36 ns; after this change, napi fun call is 20 ns. The above data are measured using a modified benchmark in 'benchmark/misc/function_call'. The modification adds an indicator of the average time of a "chatty" napi fun call (max 50M runs). This change will speed up chatty case 1.8x (overall), and will cut down the delay of napi mechanism to approx. 0.5x. Background: a simple C++ binding function (e.g. receiving little from JS, doing little and returning little to JS) is called 'chatty' case for JS<-->C++ fun call routine. This improvement also applies to getter/setter fun calls. PR-URL: nodejs#21072 Reviewed-By: Anna Henningsen <[email protected]> Reviewed-By: Gabriel Schulhof <[email protected]>
1 parent 4be5252 commit 9a3cd9e

File tree

5 files changed

+121
-92
lines changed

5 files changed

+121
-92
lines changed
 

‎Makefile

+1
Original file line numberDiff line numberDiff line change
@@ -260,6 +260,7 @@ test-check-deopts: all
260260
$(PYTHON) tools/test.py --mode=release --check-deopts parallel sequential -J
261261

262262
benchmark/misc/function_call/build/Release/binding.node: all \
263+
benchmark/misc/function_call/napi_binding.c \
263264
benchmark/misc/function_call/binding.cc \
264265
benchmark/misc/function_call/binding.gyp
265266
$(NODE) deps/npm/node_modules/node-gyp/bin/node-gyp rebuild \

‎benchmark/misc/function_call/binding.gyp

+4
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
{
22
'targets': [
3+
{
4+
'target_name': 'napi_binding',
5+
'sources': [ 'napi_binding.c' ]
6+
},
37
{
48
'target_name': 'binding',
59
'sources': [ 'binding.cc' ]

‎benchmark/misc/function_call/index.js

+11-2
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,15 @@ try {
1919
}
2020
const cxx = binding.hello;
2121

22+
let napi_binding;
23+
try {
24+
napi_binding = require('./build/Release/napi_binding');
25+
} catch (er) {
26+
console.error('misc/function_call/index.js NAPI-Binding failed to load');
27+
process.exit(0);
28+
}
29+
const napi = napi_binding.hello;
30+
2231
var c = 0;
2332
function js() {
2433
return c++;
@@ -27,14 +36,14 @@ function js() {
2736
assert(js() === cxx());
2837

2938
const bench = common.createBenchmark(main, {
30-
type: ['js', 'cxx'],
39+
type: ['js', 'cxx', 'napi'],
3140
millions: [1, 10, 50]
3241
});
3342

3443
function main(conf) {
3544
const n = +conf.millions * 1e6;
3645

37-
const fn = conf.type === 'cxx' ? cxx : js;
46+
const fn = conf.type === 'cxx' ? cxx : conf.type === 'napi' ? napi : js;
3847
bench.start();
3948
for (var i = 0; i < n; i++) {
4049
fn();
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
#include <assert.h>
2+
#include <node_api.h>
3+
4+
static int32_t increment = 0;
5+
6+
static napi_value Hello(napi_env env, napi_callback_info info) {
7+
napi_value result;
8+
napi_status status = napi_create_int32(env, increment++, &result);
9+
assert(status == napi_ok);
10+
return result;
11+
}
12+
13+
static napi_value Init(napi_env env, napi_value exports) {
14+
napi_value hello;
15+
napi_status status =
16+
napi_create_function(env,
17+
"hello",
18+
NAPI_AUTO_LENGTH,
19+
Hello,
20+
NULL,
21+
&hello);
22+
assert(status == napi_ok);
23+
status = napi_set_named_property(env, exports, "hello", hello);
24+
assert(status == napi_ok);
25+
return exports;
26+
}
27+
28+
NAPI_MODULE(NODE_GYP_MODULE_NAME, Init)

‎src/node_api.cc

+77-90
Original file line numberDiff line numberDiff line change
@@ -23,15 +23,9 @@ struct napi_env__ {
2323
loop(_loop) {}
2424
~napi_env__() {
2525
last_exception.Reset();
26-
wrap_template.Reset();
27-
function_data_template.Reset();
28-
accessor_data_template.Reset();
2926
}
3027
v8::Isolate* isolate;
3128
v8::Persistent<v8::Value> last_exception;
32-
v8::Persistent<v8::ObjectTemplate> wrap_template;
33-
v8::Persistent<v8::ObjectTemplate> function_data_template;
34-
v8::Persistent<v8::ObjectTemplate> accessor_data_template;
3529
napi_extended_error_info last_error;
3630
int open_handle_scopes = 0;
3731
int open_callback_scopes = 0;
@@ -41,19 +35,6 @@ struct napi_env__ {
4135
#define NAPI_PRIVATE_KEY(context, suffix) \
4236
(node::Environment::GetCurrent((context))->napi_ ## suffix())
4337

44-
#define ENV_OBJECT_TEMPLATE(env, prefix, destination, field_count) \
45-
do { \
46-
if ((env)->prefix ## _template.IsEmpty()) { \
47-
(destination) = v8::ObjectTemplate::New(isolate); \
48-
(destination)->SetInternalFieldCount((field_count)); \
49-
(env)->prefix ## _template.Reset(isolate, (destination)); \
50-
} else { \
51-
(destination) = v8::Local<v8::ObjectTemplate>::New( \
52-
isolate, env->prefix ## _template); \
53-
} \
54-
} while (0)
55-
56-
5738
#define RETURN_STATUS_IF_FALSE(env, condition, status) \
5839
do { \
5940
if (!(condition)) { \
@@ -510,15 +491,51 @@ class TryCatch : public v8::TryCatch {
510491

511492
//=== Function napi_callback wrapper =================================
512493

513-
static const int kDataIndex = 0;
514-
static const int kEnvIndex = 1;
494+
// TODO(somebody): these constants can be removed with relevant changes
495+
// in CallbackWrapperBase<> and CallbackBundle.
496+
// Leave them for now just to keep the change set and cognitive load minimal.
497+
static const int kFunctionIndex = 0; // Used in CallbackBundle::cb[]
498+
static const int kGetterIndex = 0; // Used in CallbackBundle::cb[]
499+
static const int kSetterIndex = 1; // Used in CallbackBundle::cb[]
500+
static const int kCallbackCount = 2; // Used in CallbackBundle::cb[]
501+
// Max is "getter + setter" case
502+
503+
// Use this data structure to associate callback data with each N-API function
504+
// exposed to JavaScript. The structure is stored in a v8::External which gets
505+
// passed into our callback wrapper. This reduces the performance impact of
506+
// calling through N-API.
507+
// Ref: benchmark/misc/function_call
508+
// Discussion (incl. perf. data): https://github.com/nodejs/node/pull/21072
509+
class CallbackBundle {
510+
public:
511+
~CallbackBundle() {
512+
handle.ClearWeak();
513+
handle.Reset();
514+
}
515515

516-
static const int kFunctionIndex = 2;
517-
static const int kFunctionFieldCount = 3;
516+
// Bind the lifecycle of `this` C++ object to a JavaScript object.
517+
// We never delete a CallbackBundle C++ object directly.
518+
void BindLifecycleTo(v8::Isolate* isolate, v8::Local<v8::Value> target) {
519+
handle.Reset(isolate, target);
520+
handle.SetWeak(this, WeakCallback, v8::WeakCallbackType::kParameter);
521+
}
518522

519-
static const int kGetterIndex = 2;
520-
static const int kSetterIndex = 3;
521-
static const int kAccessorFieldCount = 4;
523+
napi_env env; // Necessary to invoke C++ NAPI callback
524+
void* cb_data; // The user provided callback data
525+
napi_callback cb[kCallbackCount]; // Max capacity is 2 (getter + setter)
526+
v8::Persistent<v8::Value> handle; // Die with this JavaScript object
527+
528+
private:
529+
static void WeakCallback(v8::WeakCallbackInfo<CallbackBundle> const& info) {
530+
// Use the "WeakCallback mechanism" to delete the C++ `bundle` object.
531+
// This will be called when the v8::External containing `this` pointer
532+
// is being GC-ed.
533+
CallbackBundle* bundle = info.GetParameter();
534+
if (bundle != nullptr) {
535+
delete bundle;
536+
}
537+
}
538+
};
522539

523540
// Base class extended by classes that wrap V8 function and property callback
524541
// info.
@@ -550,10 +567,10 @@ class CallbackWrapperBase : public CallbackWrapper {
550567
: CallbackWrapper(JsValueFromV8LocalValue(cbinfo.This()),
551568
args_length,
552569
nullptr),
553-
_cbinfo(cbinfo),
554-
_cbdata(v8::Local<v8::Object>::Cast(cbinfo.Data())) {
555-
_data = v8::Local<v8::External>::Cast(_cbdata->GetInternalField(kDataIndex))
556-
->Value();
570+
_cbinfo(cbinfo) {
571+
_bundle = reinterpret_cast<CallbackBundle*>(
572+
v8::Local<v8::External>::Cast(cbinfo.Data())->Value());
573+
_data = _bundle->cb_data;
557574
}
558575

559576
napi_value GetNewTarget() override { return nullptr; }
@@ -562,13 +579,10 @@ class CallbackWrapperBase : public CallbackWrapper {
562579
void InvokeCallback() {
563580
napi_callback_info cbinfo_wrapper = reinterpret_cast<napi_callback_info>(
564581
static_cast<CallbackWrapper*>(this));
565-
napi_callback cb = reinterpret_cast<napi_callback>(
566-
v8::Local<v8::External>::Cast(
567-
_cbdata->GetInternalField(kInternalFieldIndex))->Value());
568582

569-
napi_env env = static_cast<napi_env>(
570-
v8::Local<v8::External>::Cast(
571-
_cbdata->GetInternalField(kEnvIndex))->Value());
583+
// All other pointers we need are stored in `_bundle`
584+
napi_env env = _bundle->env;
585+
napi_callback cb = _bundle->cb[kInternalFieldIndex];
572586

573587
napi_value result;
574588
NAPI_CALL_INTO_MODULE_THROW(env, result = cb(env, cbinfo_wrapper));
@@ -579,7 +593,7 @@ class CallbackWrapperBase : public CallbackWrapper {
579593
}
580594

581595
const Info& _cbinfo;
582-
const v8::Local<v8::Object> _cbdata;
596+
CallbackBundle* _bundle;
583597
};
584598

585599
class FunctionCallbackWrapper
@@ -701,62 +715,35 @@ class SetterCallbackWrapper
701715
// Creates an object to be made available to the static function callback
702716
// wrapper, used to retrieve the native callback function and data pointer.
703717
static
704-
v8::Local<v8::Object> CreateFunctionCallbackData(napi_env env,
705-
napi_callback cb,
706-
void* data) {
707-
v8::Isolate* isolate = env->isolate;
708-
v8::Local<v8::Context> context = isolate->GetCurrentContext();
718+
v8::Local<v8::Value> CreateFunctionCallbackData(napi_env env,
719+
napi_callback cb,
720+
void* data) {
721+
CallbackBundle* bundle = new CallbackBundle();
722+
bundle->cb[kFunctionIndex] = cb;
723+
bundle->cb_data = data;
724+
bundle->env = env;
725+
v8::Local<v8::Value> cbdata = v8::External::New(env->isolate, bundle);
726+
bundle->BindLifecycleTo(env->isolate, cbdata);
709727

710-
v8::Local<v8::ObjectTemplate> otpl;
711-
ENV_OBJECT_TEMPLATE(env, function_data, otpl, v8impl::kFunctionFieldCount);
712-
v8::Local<v8::Object> cbdata = otpl->NewInstance(context).ToLocalChecked();
713-
714-
cbdata->SetInternalField(
715-
v8impl::kEnvIndex,
716-
v8::External::New(isolate, static_cast<void*>(env)));
717-
cbdata->SetInternalField(
718-
v8impl::kFunctionIndex,
719-
v8::External::New(isolate, reinterpret_cast<void*>(cb)));
720-
cbdata->SetInternalField(
721-
v8impl::kDataIndex,
722-
v8::External::New(isolate, data));
723728
return cbdata;
724729
}
725730

726731
// Creates an object to be made available to the static getter/setter
727732
// callback wrapper, used to retrieve the native getter/setter callback
728733
// function and data pointer.
729734
static
730-
v8::Local<v8::Object> CreateAccessorCallbackData(napi_env env,
731-
napi_callback getter,
732-
napi_callback setter,
733-
void* data) {
734-
v8::Isolate* isolate = env->isolate;
735-
v8::Local<v8::Context> context = isolate->GetCurrentContext();
736-
737-
v8::Local<v8::ObjectTemplate> otpl;
738-
ENV_OBJECT_TEMPLATE(env, accessor_data, otpl, v8impl::kAccessorFieldCount);
739-
v8::Local<v8::Object> cbdata = otpl->NewInstance(context).ToLocalChecked();
740-
741-
cbdata->SetInternalField(
742-
v8impl::kEnvIndex,
743-
v8::External::New(isolate, static_cast<void*>(env)));
744-
745-
if (getter != nullptr) {
746-
cbdata->SetInternalField(
747-
v8impl::kGetterIndex,
748-
v8::External::New(isolate, reinterpret_cast<void*>(getter)));
749-
}
750-
751-
if (setter != nullptr) {
752-
cbdata->SetInternalField(
753-
v8impl::kSetterIndex,
754-
v8::External::New(isolate, reinterpret_cast<void*>(setter)));
755-
}
735+
v8::Local<v8::Value> CreateAccessorCallbackData(napi_env env,
736+
napi_callback getter,
737+
napi_callback setter,
738+
void* data) {
739+
CallbackBundle* bundle = new CallbackBundle();
740+
bundle->cb[kGetterIndex] = getter;
741+
bundle->cb[kSetterIndex] = setter;
742+
bundle->cb_data = data;
743+
bundle->env = env;
744+
v8::Local<v8::Value> cbdata = v8::External::New(env->isolate, bundle);
745+
bundle->BindLifecycleTo(env->isolate, cbdata);
756746

757-
cbdata->SetInternalField(
758-
v8impl::kDataIndex,
759-
v8::External::New(isolate, data));
760747
return cbdata;
761748
}
762749

@@ -1025,7 +1012,7 @@ napi_status napi_create_function(napi_env env,
10251012
v8::Isolate* isolate = env->isolate;
10261013
v8::Local<v8::Function> return_value;
10271014
v8::EscapableHandleScope scope(isolate);
1028-
v8::Local<v8::Object> cbdata =
1015+
v8::Local<v8::Value> cbdata =
10291016
v8impl::CreateFunctionCallbackData(env, cb, callback_data);
10301017

10311018
RETURN_STATUS_IF_FALSE(env, !cbdata.IsEmpty(), napi_generic_failure);
@@ -1065,7 +1052,7 @@ napi_status napi_define_class(napi_env env,
10651052
v8::Isolate* isolate = env->isolate;
10661053

10671054
v8::EscapableHandleScope scope(isolate);
1068-
v8::Local<v8::Object> cbdata =
1055+
v8::Local<v8::Value> cbdata =
10691056
v8impl::CreateFunctionCallbackData(env, constructor, callback_data);
10701057

10711058
RETURN_STATUS_IF_FALSE(env, !cbdata.IsEmpty(), napi_generic_failure);
@@ -1101,7 +1088,7 @@ napi_status napi_define_class(napi_env env,
11011088
// This code is similar to that in napi_define_properties(); the
11021089
// difference is it applies to a template instead of an object.
11031090
if (p->getter != nullptr || p->setter != nullptr) {
1104-
v8::Local<v8::Object> cbdata = v8impl::CreateAccessorCallbackData(
1091+
v8::Local<v8::Value> cbdata = v8impl::CreateAccessorCallbackData(
11051092
env, p->getter, p->setter, p->data);
11061093

11071094
tpl->PrototypeTemplate()->SetAccessor(
@@ -1112,7 +1099,7 @@ napi_status napi_define_class(napi_env env,
11121099
v8::AccessControl::DEFAULT,
11131100
attributes);
11141101
} else if (p->method != nullptr) {
1115-
v8::Local<v8::Object> cbdata =
1102+
v8::Local<v8::Value> cbdata =
11161103
v8impl::CreateFunctionCallbackData(env, p->method, p->data);
11171104

11181105
RETURN_STATUS_IF_FALSE(env, !cbdata.IsEmpty(), napi_generic_failure);
@@ -1474,7 +1461,7 @@ napi_status napi_define_properties(napi_env env,
14741461
v8impl::V8PropertyAttributesFromDescriptor(p);
14751462

14761463
if (p->getter != nullptr || p->setter != nullptr) {
1477-
v8::Local<v8::Object> cbdata = v8impl::CreateAccessorCallbackData(
1464+
v8::Local<v8::Value> cbdata = v8impl::CreateAccessorCallbackData(
14781465
env,
14791466
p->getter,
14801467
p->setter,
@@ -1493,7 +1480,7 @@ napi_status napi_define_properties(napi_env env,
14931480
return napi_set_last_error(env, napi_invalid_arg);
14941481
}
14951482
} else if (p->method != nullptr) {
1496-
v8::Local<v8::Object> cbdata =
1483+
v8::Local<v8::Value> cbdata =
14971484
v8impl::CreateFunctionCallbackData(env, p->method, p->data);
14981485

14991486
RETURN_STATUS_IF_FALSE(env, !cbdata.IsEmpty(), napi_generic_failure);

0 commit comments

Comments
 (0)
Please sign in to comment.