Skip to content

Commit 0f07587

Browse files
committed
perf(ffi/jsc,quickjs): inline Value storage to eliminate heap allocation per primitive
Same optimization as V8: Value stores kind/bool/number/borrowed inline on the stack and only allocates shared_ptr for owned engine handles. Tests: jsc 713/0, quickjs 713/0
1 parent 4c131f4 commit 0f07587

4 files changed

Lines changed: 250 additions & 199 deletions

File tree

NativeScript/ffi/jsc/NativeApiJSCRuntime.h

Lines changed: 84 additions & 90 deletions
Original file line numberDiff line numberDiff line change
@@ -311,160 +311,144 @@ class String {
311311

312312
class Value {
313313
public:
314-
Value()
315-
: storage_(
316-
std::make_shared<jscengine::ValueStorage>(jscengine::ValueStorage::Kind::Undefined)) {}
314+
Value() : kind_(jscengine::ValueStorage::Kind::Undefined) {}
317315

318-
Value(bool value)
319-
: storage_(std::make_shared<jscengine::ValueStorage>(jscengine::ValueStorage::Kind::Bool)) {
320-
storage_->boolValue = value;
321-
}
316+
Value(bool value) : kind_(jscengine::ValueStorage::Kind::Bool), boolValue_(value) {}
322317

323-
Value(double value)
324-
: storage_(std::make_shared<jscengine::ValueStorage>(jscengine::ValueStorage::Kind::Number)) {
325-
storage_->numberValue = value;
326-
}
318+
Value(double value) : kind_(jscengine::ValueStorage::Kind::Number), numberValue_(value) {}
327319

328320
Value(int value) : Value(static_cast<double>(value)) {}
329321
Value(uint32_t value) : Value(static_cast<double>(value)) {}
330322

331323
Value(Runtime& runtime, const Value& value) {
332-
if (value.storage_->kind == jscengine::ValueStorage::Kind::JSCBorrowed) {
333-
storage_ =
334-
std::make_shared<jscengine::ValueStorage>(jscengine::ValueStorage::Kind::JSC);
324+
if (value.kind_ == jscengine::ValueStorage::Kind::JSCBorrowed) {
325+
// Promote borrowed to owned
326+
storage_ = std::make_shared<jscengine::ValueStorage>(jscengine::ValueStorage::Kind::JSC);
335327
storage_->context = runtime.context();
336-
storage_->value = value.storage_->value != nullptr
337-
? value.storage_->value
338-
: JSValueMakeUndefined(runtime.context());
328+
storage_->value = value.borrowedValue_ != nullptr ? value.borrowedValue_
329+
: JSValueMakeUndefined(runtime.context());
339330
JSValueProtect(runtime.context(), storage_->value);
331+
kind_ = jscengine::ValueStorage::Kind::JSC;
340332
return;
341333
}
334+
kind_ = value.kind_;
335+
boolValue_ = value.boolValue_;
336+
numberValue_ = value.numberValue_;
337+
borrowedContext_ = value.borrowedContext_;
338+
borrowedValue_ = value.borrowedValue_;
342339
storage_ = value.storage_;
343340
}
344-
Value(Runtime& runtime, Value&& value) : storage_(std::move(value.storage_)) {}
345-
Value(Runtime& runtime, const String& value) : storage_(value.storage_) {}
341+
Value(Runtime& runtime, Value&& value)
342+
: kind_(value.kind_),
343+
boolValue_(value.boolValue_),
344+
numberValue_(value.numberValue_),
345+
borrowedContext_(value.borrowedContext_),
346+
borrowedValue_(value.borrowedValue_),
347+
storage_(std::move(value.storage_)) {}
348+
Value(Runtime& runtime, const String& value);
346349
Value(Runtime& runtime, const Object& object);
347350
Value(Runtime& runtime, const Function& function);
348351
Value(Runtime& runtime, const Array& array);
349352
Value(Runtime& runtime, const ArrayBuffer& arrayBuffer);
350353
Value(Runtime& runtime, const BigInt& bigint);
351354
Value(Runtime& runtime, JSValueRef value)
352-
: storage_(std::make_shared<jscengine::ValueStorage>(jscengine::ValueStorage::Kind::JSC)) {
355+
: kind_(jscengine::ValueStorage::Kind::JSC),
356+
storage_(std::make_shared<jscengine::ValueStorage>(jscengine::ValueStorage::Kind::JSC)) {
353357
storage_->context = runtime.context();
354358
storage_->value = value != nullptr ? value : JSValueMakeUndefined(runtime.context());
355359
JSValueProtect(runtime.context(), storage_->value);
356360
}
357361

358362
static Value borrowed(Runtime& runtime, JSValueRef value) {
359363
Value result;
360-
result.storage_->kind = jscengine::ValueStorage::Kind::JSCBorrowed;
361-
result.storage_->context = runtime.context();
362-
result.storage_->value = value != nullptr ? value : JSValueMakeUndefined(runtime.context());
364+
result.kind_ = jscengine::ValueStorage::Kind::JSCBorrowed;
365+
result.borrowedContext_ = runtime.context();
366+
result.borrowedValue_ = value != nullptr ? value : JSValueMakeUndefined(runtime.context());
363367
return result;
364368
}
365369

366370
static Value undefined() { return Value(); }
367371
static Value null() {
368372
Value value;
369-
value.storage_ = std::make_shared<jscengine::ValueStorage>(jscengine::ValueStorage::Kind::Null);
373+
value.kind_ = jscengine::ValueStorage::Kind::Null;
370374
return value;
371375
}
372376

373377
bool isUndefined() const {
374-
return storage_->kind == jscengine::ValueStorage::Kind::Undefined ||
375-
(storage_->kind == jscengine::ValueStorage::Kind::JSC &&
376-
JSValueIsUndefined(storage_->context, storage_->value)) ||
377-
(storage_->kind == jscengine::ValueStorage::Kind::JSCBorrowed &&
378-
JSValueIsUndefined(storage_->context, storage_->value));
378+
return kind_ == jscengine::ValueStorage::Kind::Undefined ||
379+
(isJSC() && JSValueIsUndefined(jscContext(), jscValue()));
379380
}
380381
bool isNull() const {
381-
return storage_->kind == jscengine::ValueStorage::Kind::Null ||
382-
(storage_->kind == jscengine::ValueStorage::Kind::JSC &&
383-
JSValueIsNull(storage_->context, storage_->value)) ||
384-
(storage_->kind == jscengine::ValueStorage::Kind::JSCBorrowed &&
385-
JSValueIsNull(storage_->context, storage_->value));
382+
return kind_ == jscengine::ValueStorage::Kind::Null ||
383+
(isJSC() && JSValueIsNull(jscContext(), jscValue()));
386384
}
387385
bool isBool() const {
388-
return storage_->kind == jscengine::ValueStorage::Kind::Bool ||
389-
(storage_->kind == jscengine::ValueStorage::Kind::JSC &&
390-
JSValueIsBoolean(storage_->context, storage_->value)) ||
391-
(storage_->kind == jscengine::ValueStorage::Kind::JSCBorrowed &&
392-
JSValueIsBoolean(storage_->context, storage_->value));
386+
return kind_ == jscengine::ValueStorage::Kind::Bool ||
387+
(isJSC() && JSValueIsBoolean(jscContext(), jscValue()));
393388
}
394389
bool getBool() const {
395-
if (storage_->kind == jscengine::ValueStorage::Kind::Bool) {
396-
return storage_->boolValue;
397-
}
398-
if (storage_->kind == jscengine::ValueStorage::Kind::JSC ||
399-
storage_->kind == jscengine::ValueStorage::Kind::JSCBorrowed) {
400-
return JSValueToBoolean(storage_->context, storage_->value);
390+
if (kind_ == jscengine::ValueStorage::Kind::Bool) {
391+
return boolValue_;
401392
}
402-
return false;
393+
return isJSC() && JSValueToBoolean(jscContext(), jscValue());
403394
}
404395
bool isNumber() const {
405-
return storage_->kind == jscengine::ValueStorage::Kind::Number ||
406-
(storage_->kind == jscengine::ValueStorage::Kind::JSC &&
407-
JSValueIsNumber(storage_->context, storage_->value)) ||
408-
(storage_->kind == jscengine::ValueStorage::Kind::JSCBorrowed &&
409-
JSValueIsNumber(storage_->context, storage_->value));
396+
return kind_ == jscengine::ValueStorage::Kind::Number ||
397+
(isJSC() && JSValueIsNumber(jscContext(), jscValue()));
410398
}
411399
double getNumber() const {
412-
if (storage_->kind == jscengine::ValueStorage::Kind::Number) {
413-
return storage_->numberValue;
414-
}
415-
if (storage_->kind == jscengine::ValueStorage::Kind::JSC ||
416-
storage_->kind == jscengine::ValueStorage::Kind::JSCBorrowed) {
417-
return JSValueToNumber(storage_->context, storage_->value, nullptr);
400+
if (kind_ == jscengine::ValueStorage::Kind::Number) {
401+
return numberValue_;
418402
}
419-
return 0;
403+
return isJSC() ? JSValueToNumber(jscContext(), jscValue(), nullptr) : 0;
420404
}
421405

422-
bool isObject() const {
423-
return (storage_->kind == jscengine::ValueStorage::Kind::JSC ||
424-
storage_->kind == jscengine::ValueStorage::Kind::JSCBorrowed) &&
425-
JSValueIsObject(storage_->context, storage_->value);
426-
}
427-
bool isString() const {
428-
return (storage_->kind == jscengine::ValueStorage::Kind::JSC ||
429-
storage_->kind == jscengine::ValueStorage::Kind::JSCBorrowed) &&
430-
JSValueIsString(storage_->context, storage_->value);
431-
}
406+
bool isObject() const { return isJSC() && JSValueIsObject(jscContext(), jscValue()); }
407+
bool isString() const { return isJSC() && JSValueIsString(jscContext(), jscValue()); }
432408
bool isBigInt() const {
433-
if (storage_->kind != jscengine::ValueStorage::Kind::JSC &&
434-
storage_->kind != jscengine::ValueStorage::Kind::JSCBorrowed) {
409+
if (!isJSC()) {
435410
return false;
436411
}
437412
if (__builtin_available(macOS 15.0, iOS 18.0, *)) {
438-
return JSValueIsBigInt(storage_->context, storage_->value);
413+
return JSValueIsBigInt(jscContext(), jscValue());
439414
}
440415
return false;
441416
}
442-
bool isSymbol() const {
443-
return (storage_->kind == jscengine::ValueStorage::Kind::JSC ||
444-
storage_->kind == jscengine::ValueStorage::Kind::JSCBorrowed) &&
445-
JSValueIsSymbol(storage_->context, storage_->value);
446-
}
417+
bool isSymbol() const { return isJSC() && JSValueIsSymbol(jscContext(), jscValue()); }
447418

448419
Object asObject(Runtime& runtime) const;
449420
String asString(Runtime& runtime) const;
450421
BigInt getBigInt(Runtime& runtime) const;
451422

452423
JSValueRef local(Runtime& runtime) const {
453-
switch (storage_->kind) {
424+
switch (kind_) {
454425
case jscengine::ValueStorage::Kind::Undefined:
455426
return JSValueMakeUndefined(runtime.context());
456427
case jscengine::ValueStorage::Kind::Null:
457428
return JSValueMakeNull(runtime.context());
458429
case jscengine::ValueStorage::Kind::Bool:
459-
return JSValueMakeBoolean(runtime.context(), storage_->boolValue);
430+
return JSValueMakeBoolean(runtime.context(), boolValue_);
460431
case jscengine::ValueStorage::Kind::Number:
461-
return JSValueMakeNumber(runtime.context(), storage_->numberValue);
432+
return JSValueMakeNumber(runtime.context(), numberValue_);
462433
case jscengine::ValueStorage::Kind::JSC:
463-
case jscengine::ValueStorage::Kind::JSCBorrowed:
464434
return storage_->value;
435+
case jscengine::ValueStorage::Kind::JSCBorrowed:
436+
return borrowedValue_;
465437
}
466438
}
467439

440+
// Access the shared storage (for Object/Function/Array interop)
441+
std::shared_ptr<jscengine::ValueStorage> storage() const { return storage_; }
442+
443+
static Value fromStorage(std::shared_ptr<jscengine::ValueStorage> s) {
444+
Value v;
445+
v.kind_ = s->kind;
446+
v.boolValue_ = s->boolValue;
447+
v.numberValue_ = s->numberValue;
448+
v.storage_ = std::move(s);
449+
return v;
450+
}
451+
468452
private:
469453
friend class Runtime;
470454
friend class Object;
@@ -473,6 +457,24 @@ class Value {
473457
friend class ArrayBuffer;
474458
friend class Function;
475459
friend class Array;
460+
461+
bool isJSC() const {
462+
return kind_ == jscengine::ValueStorage::Kind::JSC ||
463+
kind_ == jscengine::ValueStorage::Kind::JSCBorrowed;
464+
}
465+
JSContextRef jscContext() const {
466+
return kind_ == jscengine::ValueStorage::Kind::JSCBorrowed ? borrowedContext_
467+
: storage_->context;
468+
}
469+
JSValueRef jscValue() const {
470+
return kind_ == jscengine::ValueStorage::Kind::JSCBorrowed ? borrowedValue_ : storage_->value;
471+
}
472+
473+
jscengine::ValueStorage::Kind kind_ = jscengine::ValueStorage::Kind::Undefined;
474+
bool boolValue_ = false;
475+
double numberValue_ = 0;
476+
JSGlobalContextRef borrowedContext_ = nullptr;
477+
JSValueRef borrowedValue_ = nullptr;
476478
std::shared_ptr<jscengine::ValueStorage> storage_;
477479
};
478480

@@ -626,11 +628,7 @@ class Object {
626628
return reinterpret_cast<JSObjectRef>(const_cast<OpaqueJSValue*>(storage_->value));
627629
}
628630

629-
operator Value() const {
630-
Value value;
631-
value.storage_ = storage_;
632-
return value;
633-
}
631+
operator Value() const { return Value::fromStorage(storage_); }
634632

635633
protected:
636634
friend class Value;
@@ -829,11 +827,7 @@ class BigInt {
829827

830828
JSValueRef local(Runtime& runtime) const { return storage_->value; }
831829

832-
operator Value() const {
833-
Value value;
834-
value.storage_ = storage_;
835-
return value;
836-
}
830+
operator Value() const { return Value::fromStorage(storage_); }
837831

838832
private:
839833
friend class Value;

NativeScript/ffi/jsc/NativeApiJSCValue.mm

Lines changed: 36 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -20,19 +20,44 @@
2020
return jscengine::valueToUtf8(runtime.context(), storage_->value);
2121
}
2222

23-
String::operator Value() const {
24-
Value value;
25-
value.storage_ = storage_;
26-
return value;
27-
}
23+
String::operator Value() const { return Value::fromStorage(storage_); }
2824

29-
Value::Value(Runtime&, const Object& object) : storage_(object.storage_) {}
30-
Value::Value(Runtime&, const Function& function) : storage_(function.storage_) {}
31-
Value::Value(Runtime&, const Array& array) : storage_(array.storage_) {}
32-
Value::Value(Runtime&, const ArrayBuffer& arrayBuffer) : storage_(arrayBuffer.storage_) {}
33-
Value::Value(Runtime&, const BigInt& bigint) : storage_(bigint.storage_) {}
25+
Value::Value(Runtime&, const String& value) {
26+
storage_ = value.storage_;
27+
kind_ = storage_ ? storage_->kind : jscengine::ValueStorage::Kind::Undefined;
28+
}
29+
Value::Value(Runtime&, const Object& object) {
30+
storage_ = object.storage_;
31+
kind_ = storage_ ? storage_->kind : jscengine::ValueStorage::Kind::Undefined;
32+
}
33+
Value::Value(Runtime&, const Function& function) {
34+
storage_ = function.storage_;
35+
kind_ = storage_ ? storage_->kind : jscengine::ValueStorage::Kind::Undefined;
36+
}
37+
Value::Value(Runtime&, const Array& array) {
38+
storage_ = array.storage_;
39+
kind_ = storage_ ? storage_->kind : jscengine::ValueStorage::Kind::Undefined;
40+
}
41+
Value::Value(Runtime&, const ArrayBuffer& arrayBuffer) {
42+
storage_ = arrayBuffer.storage_;
43+
kind_ = storage_ ? storage_->kind : jscengine::ValueStorage::Kind::Undefined;
44+
}
45+
Value::Value(Runtime&, const BigInt& bigint) {
46+
storage_ = bigint.storage_;
47+
kind_ = storage_ ? storage_->kind : jscengine::ValueStorage::Kind::Undefined;
48+
}
3449

35-
Object Value::asObject(Runtime&) const { return Object::fromValueStorage(storage_); }
50+
Object Value::asObject(Runtime& runtime) const {
51+
if (storage_) {
52+
return Object::fromValueStorage(storage_);
53+
}
54+
// Promote borrowed to owned storage for Object.
55+
auto s = std::make_shared<jscengine::ValueStorage>(jscengine::ValueStorage::Kind::JSC);
56+
s->context = runtime.context();
57+
s->value = borrowedValue_ != nullptr ? borrowedValue_ : JSValueMakeUndefined(runtime.context());
58+
JSValueProtect(runtime.context(), s->value);
59+
return Object::fromValueStorage(std::move(s));
60+
}
3661

3762
String Value::asString(Runtime& runtime) const {
3863
JSValueRef exception = nullptr;

0 commit comments

Comments
 (0)