Skip to content
Open
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
35 changes: 35 additions & 0 deletions benchmark/napi/create_object_with_properties/binding.cc
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,40 @@ static napi_value CreateObjectWithPropertiesOld(napi_env env,
return nullptr;
}

// Returns an array of objects created like the ones above, for reading.
static napi_value CreateObjects(napi_env env, napi_callback_info info) {
size_t argc = 2;
napi_value args[2];
napi_get_cb_info(env, info, &argc, args, nullptr, nullptr);
bool use_new;
uint32_t count;
napi_get_value_bool(env, args[0], &use_new);
napi_get_value_uint32(env, args[1], &count);

InitializeTestProperties(env);

napi_value null_prototype;
napi_get_null(env, &null_prototype);

napi_value result;
napi_create_array_with_length(env, count, &result);
for (uint32_t i = 0; i < count; i++) {
napi_value obj;
if (use_new) {
node_api_create_object_with_properties(
env, null_prototype, global_names, global_values, 20, &obj);
} else {
napi_create_object(env, &obj);
for (int j = 0; j < 20; j++) {
napi_set_property(env, obj, global_names[j], global_values[j]);
}
}
napi_set_element(env, result, i, obj);
}

return result;
}

NAPI_MODULE_INIT() {
napi_property_descriptor desc[] = {
{"createObjectWithPropertiesNew",
Expand All @@ -109,6 +143,7 @@ NAPI_MODULE_INIT() {
0,
napi_default,
0},
{"createObjects", 0, CreateObjects, 0, 0, 0, napi_default, 0},
};

napi_define_properties(env, exports, sizeof(desc) / sizeof(desc[0]), desc);
Expand Down
21 changes: 19 additions & 2 deletions benchmark/napi/create_object_with_properties/index.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
'use strict';

const assert = require('assert');
const common = require('../../common.js');

let binding;
Expand All @@ -13,12 +14,28 @@ try {
const bench = common.createBenchmark(main, {
n: [1e2, 1e3, 1e4, 1e5, 1e6],
method: ['new', 'old'],
operation: ['create', 'read'],
});

function main({ n, method }) {
if (method === 'new') {
function main({ n, method, operation }) {
if (operation === 'read') {
read(n, method);
} else if (method === 'new') {
binding.createObjectWithPropertiesNew(n, bench, bench.start, bench.end);
} else {
binding.createObjectWithPropertiesOld(n, bench, bench.start, bench.end);
}
}

// Reads three properties of one of 1000 objects, n times.
function read(n, method) {
const objects = binding.createObjects(method === 'new', 1000);
let length = 0;
bench.start();
for (let i = 0; i < n; i++) {
const object = objects[i % objects.length];
length += object.foo0.length + object.foo9.length + object.foo19.length;
}
bench.end(n);
assert.ok(length > 0);
}
127 changes: 120 additions & 7 deletions src/js_native_api_v8.cc
Original file line number Diff line number Diff line change
Expand Up @@ -597,8 +597,114 @@ inline bool CanBeHeldWeakly(v8::Local<v8::Value> value) {
return value->IsObject() || value->IsSymbol();
}

// "0" to "4294967294" in canonical form, which V8 stores as an element.
bool IsArrayIndex(std::string_view name) {
if (name.empty() || name.size() > 10 || (name.size() > 1 && name[0] == '0')) {
return false;
}
uint64_t index = 0;
for (char c : name) {
if (c < '0' || c > '9') return false;
index = index * 10 + (c - '0');
}
return index <= 4294967294u;
}

// v8::DictionaryTemplate::New() takes the names as one-byte strings and
// aborts on an array index, and a duplicate name is only handled by
// v8::Object::New(), so there is no template for those names.
v8::Local<v8::DictionaryTemplate> NewDictionaryTemplate(
v8::Isolate* isolate, v8::Local<v8::Name>* names, size_t count) {
std::vector<std::string> strings(count);
for (size_t i = 0; i < count; i++) {
if (!names[i]->IsString()) return {};
v8::Local<v8::String> name = names[i].As<v8::String>();
if (!name->ContainsOnlyOneByte()) return {};
strings[i].resize(name->Length());
name->WriteOneByteV2(isolate,
0,
name->Length(),
reinterpret_cast<uint8_t*>(strings[i].data()));
if (IsArrayIndex(strings[i])) return {};
for (size_t j = 0; j < i; j++) {
if (strings[j] == strings[i]) return {};
}
}
std::vector<std::string_view> views(strings.begin(), strings.end());
return v8::DictionaryTemplate::New(
isolate,
v8::MemorySpan<const std::string_view>(views.data(), views.size()));
}

} // end of anonymous namespace

v8::MaybeLocal<v8::Object> ObjectShapeCache::New(
v8::Isolate* isolate,
v8::Local<v8::Context> context,
v8::Local<v8::Value> prototype_or_null,
v8::Local<v8::Name>* names,
v8::Local<v8::Value>* values,
size_t count) {
auto object_new = [&]() -> v8::MaybeLocal<v8::Object> {
return v8::Object::New(isolate, prototype_or_null, names, values, count);
};
// v8::Object::New() reports a prototype that is neither null nor an object.
if (count == 0 || count > kMaxProperties ||
!(prototype_or_null->IsNull() || prototype_or_null->IsObject())) {
return object_new();
}

// The identity hash of a string is the hash of its contents, so equal names
// created separately hash the same.
uint32_t hash = static_cast<uint32_t>(count);
for (size_t i = 0; i < count; i++) {
hash = hash * 31 + static_cast<uint32_t>(names[i]->GetIdentityHash());
}
auto it = shapes_.find(hash);
if (it == shapes_.end()) {
uint32_t& seen = seen_[(hash ^ (hash >> 16)) % kSeenEntries];
if (seen != hash || shapes_.size() == kMaxShapes) {
seen = hash;
return object_new();
}
// The second time these names are passed.
it = shapes_.emplace(hash, Shape()).first;
v8::Local<v8::DictionaryTemplate> tmpl =
NewDictionaryTemplate(isolate, names, count);
if (!tmpl.IsEmpty()) {
it->second.tmpl.Reset(isolate, tmpl);
it->second.names.reserve(count);
for (size_t i = 0; i < count; i++) {
it->second.names.emplace_back(isolate, names[i]);
}
}
}

const Shape& shape = it->second;
if (shape.tmpl.IsEmpty() || shape.names.size() != count) return object_new();
for (size_t i = 0; i < count; i++) {
v8::Local<v8::Name> name = shape.names[i].Get(isolate);
if (name != names[i] && !name->StrictEquals(names[i])) return object_new();
}

std::array<v8::MaybeLocal<v8::Value>, kMaxProperties> property_values;
for (size_t i = 0; i < count; i++) {
// NewInstance() would leave the property out, so keep what
// v8::Object::New() does with an empty value.
if (values[i].IsEmpty()) return object_new();
property_values[i] = values[i];
}
v8::Local<v8::Object> obj = shape.tmpl.Get(isolate)->NewInstance(
context,
v8::MemorySpan<v8::MaybeLocal<v8::Value>>(property_values.data(), count));
// The template's map has the context's Object.prototype.
if (obj->GetPrototypeV2() != prototype_or_null &&
obj->SetPrototypeV2(context, prototype_or_null).IsNothing()) {
return {};
}
return obj;
}

void Finalizer::ResetEnv() {
env_ = nullptr;
}
Expand Down Expand Up @@ -1651,13 +1757,20 @@ node_api_create_object_with_properties(napi_env env,
v8_values[i] = v8impl::V8LocalValueFromJsValue(property_values[i]);
}

v8::Local<v8::Object> obj = v8::Object::New(env->isolate,
v8_prototype_or_null,
v8_names.data(),
v8_values.data(),
property_count);

RETURN_STATUS_IF_FALSE(env, !obj.IsEmpty(), napi_generic_failure);
if (env->object_shape_cache == nullptr) {
env->object_shape_cache = std::make_unique<v8impl::ObjectShapeCache>();
}
v8::Local<v8::Object> obj;
RETURN_STATUS_IF_FALSE(env,
env->object_shape_cache
->New(env->isolate,
env->context(),
v8_prototype_or_null,
v8_names.data(),
v8_values.data(),
property_count)
.ToLocal(&obj),
napi_generic_failure);
*result = v8impl::JsValueFromV8LocalValue(obj);
return napi_clear_last_error(env);
}
Expand Down
44 changes: 44 additions & 0 deletions src/js_native_api_v8.h
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
#ifndef SRC_JS_NATIVE_API_V8_H_
#define SRC_JS_NATIVE_API_V8_H_

#include <array>
#include <memory>
#include <unordered_map>
#include <vector>

#include "js_native_api_types.h"
#include "js_native_api_v8_internals.h"

Expand Down Expand Up @@ -48,6 +53,43 @@ class RefTracker {
RefList* prev_ = nullptr;
};

// Creates the objects of node_api_create_object_with_properties().
// v8::Object::New() puts the properties of every object it creates in a
// dictionary, so those objects are slow to read, and with a null prototype
// each of them also gets a map of its own. When the same property names are
// passed again, this cache creates a v8::DictionaryTemplate for them and
// instantiates it instead, so the objects get fast properties and share a
// map, like objects created from an object literal.
class ObjectShapeCache {
public:
v8::MaybeLocal<v8::Object> New(v8::Isolate* isolate,
v8::Local<v8::Context> context,
v8::Local<v8::Value> prototype_or_null,
v8::Local<v8::Name>* names,
v8::Local<v8::Value>* values,
size_t count);

private:
// The first kMaxShapes lists of names that are passed a second time get a
// template, and keep it for the life of the env. A list passed once is only
// recorded by its hash, in a direct-mapped table of kSeenEntries.
static constexpr size_t kMaxShapes = 256;
static constexpr size_t kSeenEntries = 1024;
// From 128 properties V8 creates the objects in dictionary mode anyway, as
// it does for object literals.
static constexpr size_t kMaxProperties = 127;

struct Shape {
std::vector<v8::Global<v8::Name>> names;
// Empty when V8 cannot create a template for the names.
v8::Global<v8::DictionaryTemplate> tmpl;
};

// Keyed by a hash of the names.
std::unordered_map<uint32_t, Shape> shapes_;
std::array<uint32_t, kSeenEntries> seen_{};
};

} // end of namespace v8impl

struct napi_env__ {
Expand Down Expand Up @@ -161,6 +203,8 @@ struct napi_env__ {
void* instance_data = nullptr;
int32_t module_api_version = NODE_API_DEFAULT_MODULE_API_VERSION;
bool in_gc_finalizer = false;
// Created by the first node_api_create_object_with_properties() call.
std::unique_ptr<v8impl::ObjectShapeCache> object_shape_cache;

protected:
// Should not be deleted directly. Delete with `napi_env__::DeleteMe()`
Expand Down
36 changes: 36 additions & 0 deletions test/js-native-api/test_object/test_object.c
Original file line number Diff line number Diff line change
Expand Up @@ -783,6 +783,40 @@ static napi_value TestCreateObjectWithCustomPrototype(napi_env env,
return result;
}

// CreateObjectWithProperties(names, values, prototype) passes the arrays to
// node_api_create_object_with_properties(), and an undefined prototype as
// NULL.
static napi_value CreateObjectWithProperties(napi_env env,
napi_callback_info info) {
size_t argc = 3;
napi_value args[3];
napi_value names[200];
napi_value values[200];
uint32_t count;
napi_valuetype prototype_type;
napi_value result;

NODE_API_CALL(env, napi_get_cb_info(env, info, &argc, args, NULL, NULL));
NODE_API_CALL(env, napi_get_array_length(env, args[0], &count));
NODE_API_ASSERT(env, count <= 200, "Too many properties");
for (uint32_t i = 0; i < count; i++) {
NODE_API_CALL(env, napi_get_element(env, args[0], i, &names[i]));
NODE_API_CALL(env, napi_get_element(env, args[1], i, &values[i]));
}
NODE_API_CALL(env, napi_typeof(env, args[2], &prototype_type));

NODE_API_CALL(env,
node_api_create_object_with_properties(
env,
prototype_type == napi_undefined ? NULL : args[2],
names,
values,
count,
&result));

return result;
}

EXTERN_C_START
napi_value Init(napi_env env, napi_value exports) {
napi_property_descriptor descriptors[] = {
Expand Down Expand Up @@ -822,6 +856,8 @@ napi_value Init(napi_env env, napi_value exports) {
TestCreateObjectWithPropertiesEmpty),
DECLARE_NODE_API_PROPERTY("TestCreateObjectWithCustomPrototype",
TestCreateObjectWithCustomPrototype),
DECLARE_NODE_API_PROPERTY("CreateObjectWithProperties",
CreateObjectWithProperties),
};

init_test_null(env, exports);
Expand Down
Loading
Loading