From ded09544b344e62b14207965ee61e276ac9c8a49 Mon Sep 17 00:00:00 2001 From: Eduardo Speroni Date: Tue, 11 Aug 2026 13:40:40 -0300 Subject: [PATCH 1/6] fix(metadata-generator): share the string-interning map with the encoding serializer BinaryTypeEncodingSerializer held its BinaryWriter by value. The writer owns the string-interning map, so the copy interned into a second map and every string reachable from both the meta path and the type-encoding path was written to the heap twice. 4519 strings were stored at two offsets; every one of them was the cross-path case. --- metadata-generator/src/Binary/binaryTypeEncodingSerializer.h | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/metadata-generator/src/Binary/binaryTypeEncodingSerializer.h b/metadata-generator/src/Binary/binaryTypeEncodingSerializer.h index 6a275202..2414a848 100644 --- a/metadata-generator/src/Binary/binaryTypeEncodingSerializer.h +++ b/metadata-generator/src/Binary/binaryTypeEncodingSerializer.h @@ -17,7 +17,10 @@ namespace binary { class BinaryTypeEncodingSerializer : public ::Meta::TypeVisitor > { private: - BinaryWriter _heapWriter; + // Must alias the serializer's writer, not copy it: BinaryWriter owns the + // string-interning map, and a copy interns into a second map, emitting a + // duplicate copy of every string reachable from both paths. + BinaryWriter& _heapWriter; unique_ptr serializeRecordEncoding( const binary::BinaryTypeEncodingType encodingType, From b53e67ed6e98728183ed8abcba04345a36c833c0 Mon Sep 17 00:00:00 2001 From: Eduardo Speroni Date: Tue, 11 Aug 2026 13:40:54 -0300 Subject: [PATCH 2/6] perf(metadata-generator): emit a single shared empty binary array push_binaryArray wrote a fresh count-of-zero for every empty array, and nothing deduplicated them. Empty protocol lists alone accounted for 95k of them. Empty arrays carry no payload, so they can all share one offset. Offset 0 stays the null sentinel: the heap reserves a marker byte there, so no real array can land on it. --- metadata-generator/src/Binary/binaryWriter.cpp | 12 ++++++++++++ metadata-generator/src/Binary/binaryWriter.h | 1 + 2 files changed, 13 insertions(+) diff --git a/metadata-generator/src/Binary/binaryWriter.cpp b/metadata-generator/src/Binary/binaryWriter.cpp index d186ad84..6f0a0a6a 100644 --- a/metadata-generator/src/Binary/binaryWriter.cpp +++ b/metadata-generator/src/Binary/binaryWriter.cpp @@ -45,11 +45,23 @@ binary::MetaFileOffset binary::BinaryWriter::push_arrayCount(MetaArrayCount coun binary::MetaFileOffset binary::BinaryWriter::push_binaryArray(std::vector& binaryArray) { + // Empty arrays carry no payload, so every one of them can share a single + // count-of-zero in the heap. Offset 0 is the null sentinel and the heap + // reserves a marker byte there, so a real array never lands on it. + if (binaryArray.empty() && this->emptyArrayOffset != 0) { + return this->emptyArrayOffset; + } + binary::MetaFileOffset offset = this->_stream->position(); this->push_arrayCount((binary::MetaArrayCount)binaryArray.size()); for (binary::MetaFileOffset element : binaryArray) { this->push_pointer(element); } + + if (binaryArray.empty()) { + this->emptyArrayOffset = offset; + } + return offset; } diff --git a/metadata-generator/src/Binary/binaryWriter.h b/metadata-generator/src/Binary/binaryWriter.h index eecb265a..50a5f0df 100644 --- a/metadata-generator/src/Binary/binaryWriter.h +++ b/metadata-generator/src/Binary/binaryWriter.h @@ -14,6 +14,7 @@ namespace binary { class BinaryWriter : public BinaryOperation { private: std::map uniqueStrings; + MetaFileOffset emptyArrayOffset = 0; MetaFileOffset push_number(long number, int bytesCount); From 3c7c91e31d63dda007c5cd02da3f5d4808b09f45 Mon Sep 17 00:00:00 2001 From: Eduardo Speroni Date: Tue, 11 Aug 2026 14:06:47 -0300 Subject: [PATCH 3/6] perf(metadata): omit the constructorTokens slot when it is empty 57933 of 60589 methods carry no constructor tokens, yet every MethodMeta paid a 4-byte pointer for the field. It is the trailing field and MethodMeta has no subclass, so it can be left out entirely and gated on a flag. Also widens the member flag mask: it cleared bits 8 and up alongside the type bits, which would silently discard any member flag stored there. No member in the SDK sets bit 8 today, so this changes nothing on its own. --- NativeScript/runtime/Metadata.h | 38 +++++++++++------- .../src/Binary/binarySerializer.cpp | 11 +++++- .../src/Binary/binaryStructures.cpp | 7 +++- .../src/Binary/binaryStructures.h | 39 ++++++++++--------- 4 files changed, 58 insertions(+), 37 deletions(-) diff --git a/NativeScript/runtime/Metadata.h b/NativeScript/runtime/Metadata.h index 0d0dc701..f7bcb76b 100644 --- a/NativeScript/runtime/Metadata.h +++ b/NativeScript/runtime/Metadata.h @@ -59,20 +59,23 @@ inline uint8_t getMinorVersion(uint8_t encodedVersion) { // Bit indices in flags section enum MetaFlags { - HasDemangledName = 8, - HasName = 7, - // IsIosAppExtensionAvailable = 6, the flag exists in metadata generator but we never use it in the runtime - FunctionReturnsUnmanaged = 3, - FunctionIsVariadic = 5, - FunctionOwnsReturnedCocoaObject = 4, - MemberIsOptional = 0, // Mustn't equal any Method or Property flag since it can be applicable to both - MethodIsInitializer = 1, - MethodIsVariadic = 2, - MethodIsNullTerminatedVariadic = 3, - MethodOwnsReturnedCocoaObject = 4, - MethodHasErrorOutParameter = 5, - PropertyHasGetter = 2, - PropertyHasSetter = 3, + HasDemangledName = 8, + HasName = 7, + // IsIosAppExtensionAvailable = 6, the flag exists in metadata generator but + // we never use it in the runtime + FunctionReturnsUnmanaged = 3, + FunctionIsVariadic = 5, + FunctionOwnsReturnedCocoaObject = 4, + MemberIsOptional = 0, // Mustn't equal any Method or Property flag since it + // can be applicable to both + MethodIsInitializer = 1, + MethodIsVariadic = 2, + MethodIsNullTerminatedVariadic = 3, + MethodOwnsReturnedCocoaObject = 4, + MethodHasErrorOutParameter = 5, + MethodHasConstructorTokens = 9, + PropertyHasGetter = 2, + PropertyHasSetter = 3, }; @@ -788,8 +791,13 @@ struct MethodMeta : MemberMeta { return this->_encodings.valuePtr(); } + // The trailing _constructorTokens slot is only written when the flag is + // set, so it must not be read otherwise — the bytes past _encodings belong + // to whatever the generator emitted next. inline const char* constructorTokens() const { - return this->_constructorTokens.valuePtr(); + return this->flag(MetaFlags::MethodHasConstructorTokens) + ? this->_constructorTokens.valuePtr() + : ""; } bool isImplementedInClass(Class klass, bool isStatic) const; diff --git a/metadata-generator/src/Binary/binarySerializer.cpp b/metadata-generator/src/Binary/binarySerializer.cpp index 27d99f3b..363eee1c 100644 --- a/metadata-generator/src/Binary/binarySerializer.cpp +++ b/metadata-generator/src/Binary/binarySerializer.cpp @@ -140,7 +140,10 @@ void binary::BinarySerializer::serializeBaseClass(::Meta::BaseClassMeta* meta, b void binary::BinarySerializer::serializeMember(::Meta::Meta* meta, binary::MemberMeta& binaryMetaStruct) { this->serializeBase(meta, binaryMetaStruct); - binaryMetaStruct._flags &= 0b11111000; // this clears the type information written in the lower 3 bits + // Clear only the type information in the lower 3 bits. The old mask also + // dropped bits 8 and up, which would silently discard any member flag + // stored there. + binaryMetaStruct._flags &= ~0b111; if (meta->getFlags(::Meta::MetaFlags::MemberIsOptional)) binaryMetaStruct._flags |= BinaryFlags::MemberIsOptional; @@ -163,7 +166,11 @@ void binary::BinarySerializer::serializeMethod(::Meta::MethodMeta* meta, binary: binaryMetaStruct._flags |= BinaryFlags::MethodIsInitializer; binaryMetaStruct._encoding = this->typeEncodingSerializer.visit(meta->signature); - binaryMetaStruct._constructorTokens = this->heapWriter.push_string(meta->constructorTokens); + if (!meta->constructorTokens.empty()) { + binaryMetaStruct._flags |= BinaryFlags::MethodHasConstructorTokens; + binaryMetaStruct._constructorTokens = + this->heapWriter.push_string(meta->constructorTokens); + } } void binary::BinarySerializer::serializeProperty(::Meta::PropertyMeta* meta, binary::PropertyMeta& binaryMetaStruct) diff --git a/metadata-generator/src/Binary/binaryStructures.cpp b/metadata-generator/src/Binary/binaryStructures.cpp index 6f2c1e15..6502fb83 100644 --- a/metadata-generator/src/Binary/binaryStructures.cpp +++ b/metadata-generator/src/Binary/binaryStructures.cpp @@ -43,7 +43,12 @@ binary::MetaFileOffset binary::MethodMeta::save(BinaryWriter& writer) { binary::MetaFileOffset offset = MemberMeta::save(writer); writer.push_pointer(this->_encoding); - writer.push_pointer(this->_constructorTokens); + // Trailing field, and MethodMeta has no subclass, so it can simply be left + // out when unset. The reader keys off MethodHasConstructorTokens and never + // touches the slot otherwise. + if (this->_flags & BinaryFlags::MethodHasConstructorTokens) { + writer.push_pointer(this->_constructorTokens); + } return offset; } diff --git a/metadata-generator/src/Binary/binaryStructures.h b/metadata-generator/src/Binary/binaryStructures.h index 8ddbb47f..4db63c0b 100644 --- a/metadata-generator/src/Binary/binaryStructures.h +++ b/metadata-generator/src/Binary/binaryStructures.h @@ -61,25 +61,26 @@ enum BinaryMetaType : uint8_t { }; enum BinaryFlags : uint16_t { - // Common - HasDemangledName = 1 << 8, - HasName = 1 << 7, - IsIosAppExtensionAvailable = 1 << 6, - // Function - FunctionIsVariadic = 1 << 5, - FunctionOwnsReturnedCocoaObject = 1 << 4, - FunctionReturnsUnmanaged = 1 << 3, - // Member - MemberIsOptional = 1 << 0, - // Method - MethodIsInitializer = 1 << 1, - MethodIsVariadic = 1 << 2, - MethodIsNullTerminatedVariadic = 1 << 3, - MethodOwnsReturnedCocoaObject = 1 << 4, - MethodHasErrorOutParameter = 1 << 5, - // Property - PropertyHasGetter = 1 << 2, - PropertyHasSetter = 1 << 3 + // Common + HasDemangledName = 1 << 8, + HasName = 1 << 7, + IsIosAppExtensionAvailable = 1 << 6, + // Function + FunctionIsVariadic = 1 << 5, + FunctionOwnsReturnedCocoaObject = 1 << 4, + FunctionReturnsUnmanaged = 1 << 3, + // Member + MemberIsOptional = 1 << 0, + // Method + MethodIsInitializer = 1 << 1, + MethodIsVariadic = 1 << 2, + MethodIsNullTerminatedVariadic = 1 << 3, + MethodOwnsReturnedCocoaObject = 1 << 4, + MethodHasErrorOutParameter = 1 << 5, + MethodHasConstructorTokens = 1 << 9, + // Property + PropertyHasGetter = 1 << 2, + PropertyHasSetter = 1 << 3 }; #pragma pack(push, 1) From a908bd7d589b6431695c37de541270dcdebbc718 Mon Sep 17 00:00:00 2001 From: Eduardo Speroni Date: Tue, 11 Aug 2026 14:32:34 -0300 Subject: [PATCH 4/6] perf(metadata): reference interfaces by class-name index MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit An interface reference in a type encoding cost 9 bytes: a tag, a name pointer and a protocols pointer. 88172 of the 92844 references in the SDK carry no protocols at all, and only 2960 distinct class names are referenced. Those references now encode as a tag plus a uint16 index into a new class-name table, 3 bytes instead of 9. References that do carry protocols keep the old form, as does any reference the generator cannot index because the table is full. The table sits between the module table and the heap, so the runtime resolves it once in setInstance rather than walking the preceding tables on every lookup. Readers must test isInterfaceReference() rather than comparing against a single tag, and read the name through interfaceName(); comparing against InterfaceDeclarationReference alone would silently drop the indexed form into whatever the default branch does. Also gates the empty-array interning behind an explicit flag. Header tables are located positionally, so interning one there would move every table after it — harmless while only one such table could be empty, but not once a second exists. --- NativeScript/runtime/ArgConverter.mm | 18 +-- NativeScript/runtime/ClassBuilder.mm | 1 + NativeScript/runtime/FFICall.cpp | 1 + NativeScript/runtime/Interop.mm | 44 +++--- NativeScript/runtime/Metadata.h | 138 +++++++++++++----- NativeScript/runtime/Metadata.mm | 5 + .../src/Binary/binarySerializer.h | 11 +- .../src/Binary/binaryStructures.cpp | 9 ++ .../src/Binary/binaryStructures.h | 81 +++++----- .../Binary/binaryTypeEncodingSerializer.cpp | 29 ++++ .../src/Binary/binaryTypeEncodingSerializer.h | 5 +- .../src/Binary/binaryWriter.cpp | 8 +- metadata-generator/src/Binary/binaryWriter.h | 14 +- metadata-generator/src/Binary/metaFile.cpp | 36 ++++- metadata-generator/src/Binary/metaFile.h | 17 +++ 15 files changed, 293 insertions(+), 124 deletions(-) diff --git a/NativeScript/runtime/ArgConverter.mm b/NativeScript/runtime/ArgConverter.mm index 0615fef9..77808fc1 100644 --- a/NativeScript/runtime/ArgConverter.mm +++ b/NativeScript/runtime/ArgConverter.mm @@ -500,8 +500,7 @@ return; } } else if (value->IsString()) { - if (type == BinaryTypeEncodingType::IdEncoding || - type == BinaryTypeEncodingType::InterfaceDeclarationReference) { + if (type == BinaryTypeEncodingType::IdEncoding || typeEncoding->isInterfaceReference()) { id data = tns::ToNSString(isolate, value); // this feels wrong but follows the other CFBridgingRetain calls // and also solves a leak @@ -511,7 +510,7 @@ return; } } else if (value->IsObject()) { - if (type == BinaryTypeEncodingType::InterfaceDeclarationReference || + if (typeEncoding->isInterfaceReference() || type == BinaryTypeEncodingType::InstanceTypeEncoding || type == BinaryTypeEncodingType::IdEncoding) { BaseDataWrapper* baseWrapper = tns::GetValue(isolate, value); @@ -720,8 +719,8 @@ } Isolate* isolate = v8::Isolate::GetCurrent(); - if (typeEncoding->type == BinaryTypeEncodingType::InterfaceDeclarationReference) { - const char* name = typeEncoding->details.declarationReference.name.valuePtr(); + if (typeEncoding->isInterfaceReference()) { + const char* name = typeEncoding->interfaceName(); if (strcmp(name, "NSNumber") == 0 && tns::IsNumber(arg)) { return true; } @@ -978,9 +977,8 @@ } const Meta* ArgConverter::FindMeta(Class klass, const TypeEncoding* typeEncoding) { - if (typeEncoding != nullptr && - typeEncoding->type == BinaryTypeEncodingType::InterfaceDeclarationReference) { - const char* name = typeEncoding->details.interfaceDeclarationReference.name.valuePtr(); + if (typeEncoding != nullptr && typeEncoding->isInterfaceReference()) { + const char* name = typeEncoding->interfaceName(); const Meta* result = GetMeta(name); if (result != nullptr && result->type() == MetaType::Interface) { return result; @@ -1259,11 +1257,11 @@ } const TypeEncoding* innerTypeEncoding = typeEncoding->details.pointer.getInnerType(); - if (innerTypeEncoding->type != BinaryTypeEncodingType::InterfaceDeclarationReference) { + if (!innerTypeEncoding->isInterfaceReference()) { return false; } - const char* name = innerTypeEncoding->details.declarationReference.name.valuePtr(); + const char* name = innerTypeEncoding->interfaceName(); if (name == nullptr) { return false; } diff --git a/NativeScript/runtime/ClassBuilder.mm b/NativeScript/runtime/ClassBuilder.mm index 61a24d11..2a0373f9 100644 --- a/NativeScript/runtime/ClassBuilder.mm +++ b/NativeScript/runtime/ClassBuilder.mm @@ -537,6 +537,7 @@ void ScopeClassNameToIsolate(std::string& name, int isolateId) { } case BinaryTypeEncodingType::ProtocolEncoding: case BinaryTypeEncodingType::InterfaceDeclarationReference: + case BinaryTypeEncodingType::InterfaceIndexReference: case BinaryTypeEncodingType::InstanceTypeEncoding: case BinaryTypeEncodingType::IdEncoding: { return "@"; diff --git a/NativeScript/runtime/FFICall.cpp b/NativeScript/runtime/FFICall.cpp index ce235b63..3aa23503 100644 --- a/NativeScript/runtime/FFICall.cpp +++ b/NativeScript/runtime/FFICall.cpp @@ -12,6 +12,7 @@ ffi_type* FFICall::GetArgumentType(const TypeEncoding* typeEncoding, bool isStru } case BinaryTypeEncodingType::IdEncoding: case BinaryTypeEncodingType::InterfaceDeclarationReference: + case BinaryTypeEncodingType::InterfaceIndexReference: case BinaryTypeEncodingType::InstanceTypeEncoding: case BinaryTypeEncodingType::SelectorEncoding: case BinaryTypeEncodingType::BlockEncoding: diff --git a/NativeScript/runtime/Interop.mm b/NativeScript/runtime/Interop.mm index fb29bbf2..7c25ba94 100644 --- a/NativeScript/runtime/Interop.mm +++ b/NativeScript/runtime/Interop.mm @@ -153,8 +153,8 @@ } bool Interop::isRefTypeEqual(const TypeEncoding* typeEncoding, const char* clazz) { - std::string n(&typeEncoding->details.interfaceDeclarationReference.name.value()); - return n.compare(clazz) == 0; + const char* name = typeEncoding->interfaceName(); + return name != nullptr && std::string(name).compare(clazz) == 0; } // this is experimental. Maybe we can have something like this to wrap all Local to avoid @@ -246,8 +246,7 @@ inline bool isBool() { FFICall::DisposeFFIType(ffiType, typeEncoding); memset(dest, 0, size); } else if (argHelper.isBool()) { - if (typeEncoding->type == BinaryTypeEncodingType::InterfaceDeclarationReference && - isRefTypeEqual(typeEncoding, "NSNumber")) { + if (typeEncoding->isInterfaceReference() && isRefTypeEqual(typeEncoding, "NSNumber")) { bool value = tns::ToBool(arg); NSNumber* num = [NSNumber numberWithBool:value]; Interop::SetValue(dest, num); @@ -321,15 +320,14 @@ inline bool isBool() { } unichar c = (vector.size() == 0) ? 0 : vector[0]; Interop::SetValue(dest, c); - } else if (argHelper.isString() && - (typeEncoding->type == BinaryTypeEncodingType::InterfaceDeclarationReference || - typeEncoding->type == BinaryTypeEncodingType::IdEncoding)) { + } else if (argHelper.isString() && (typeEncoding->isInterfaceReference() || + typeEncoding->type == BinaryTypeEncodingType::IdEncoding)) { NSString* result = tns::ToNSString(isolate, arg); Interop::SetValue(dest, result); } else if (Interop::IsNumbericType(typeEncoding->type) || tns::IsNumber(arg)) { double value = tns::ToNumber(isolate, arg); - if (typeEncoding->type == BinaryTypeEncodingType::InterfaceDeclarationReference || + if (typeEncoding->isInterfaceReference() || typeEncoding->type == BinaryTypeEncodingType::IdEncoding) { // NSNumber NSNumber* num = [NSNumber numberWithDouble:value]; @@ -657,8 +655,8 @@ inline bool isBool() { } bool isNSArray = false; - if (typeEncoding->type == BinaryTypeEncodingType::InterfaceDeclarationReference) { - std::string name = typeEncoding->details.interfaceDeclarationReference.name.valuePtr(); + if (typeEncoding->isInterfaceReference()) { + std::string name = typeEncoding->interfaceName(); isNSArray = name == "NSArray"; } @@ -1189,7 +1187,7 @@ inline bool isBool() { return instance; } - if (typeEncoding->type == BinaryTypeEncodingType::InterfaceDeclarationReference || + if (typeEncoding->isInterfaceReference() || typeEncoding->type == BinaryTypeEncodingType::IdEncoding || typeEncoding->type == BinaryTypeEncodingType::InstanceTypeEncoding) { id result = call->GetResult(); @@ -1222,8 +1220,8 @@ inline bool isBool() { } if (marshalToPrimitive && [result isKindOfClass:[NSString class]]) { - if (typeEncoding->type == BinaryTypeEncodingType::InterfaceDeclarationReference) { - const char* returnClassName = typeEncoding->details.declarationReference.name.valuePtr(); + if (typeEncoding->isInterfaceReference()) { + const char* returnClassName = typeEncoding->interfaceName(); Class returnClass = objc_getClass(returnClassName); if (returnClass != nil && returnClass == [NSMutableString class]) { marshalToPrimitive = false; @@ -1249,9 +1247,9 @@ inline bool isBool() { return poInstance->Get(isolate); } - // For NSProxy we will try to read the metadata from - // typeEncoding->details.interfaceDeclarationReference.name because class_getSuperclass will - // directly return NSProxy and thus missing to attach all instance members + // For NSProxy we will try to read the metadata from the encoding's interface name because + // class_getSuperclass will directly return NSProxy and thus missing to attach all instance + // members const TypeEncoding* te = [result isProxy] ? typeEncoding : nullptr; ObjCDataWrapper* wrapper = new ObjCDataWrapper(result, te); @@ -1398,9 +1396,7 @@ inline bool isBool() { const char* protocolName = (*it).valuePtr(); additionalProtocols.push_back(protocolName); } - } else if (typeEncoding->type == BinaryTypeEncodingType::InterfaceDeclarationReference && - typeEncoding->details.interfaceDeclarationReference._protocols.offset > 0) { - PtrTo> protocols = typeEncoding->details.interfaceDeclarationReference._protocols; + } else if (const Array* protocols = typeEncoding->interfaceProtocols()) { for (auto it = protocols->begin(); it != protocols->end(); it++) { const char* protocolName = (*it).valuePtr(); additionalProtocols.push_back(protocolName); @@ -1767,9 +1763,9 @@ void ExecuteWriteValueValidationsAndStopExecutionAndLogStackTrace(Local const TypeEncoding* typeEncoding, void* dest, Local arg) { Isolate* isolate = v8::Isolate::GetCurrent(); - std::string destName = typeEncoding->details.interfaceDeclarationReference.name.valuePtr(); Local originArg = arg; - if (typeEncoding->type == BinaryTypeEncodingType::InterfaceDeclarationReference) { + if (typeEncoding->isInterfaceReference()) { + std::string destName = typeEncoding->interfaceName(); if (originArg->IsObject()) { Local originObj = originArg.As(); if ((originObj->IsArrayBuffer() || originObj->IsArrayBufferView() || @@ -1791,7 +1787,7 @@ void ExecuteWriteValueValidationsAndStopExecutionAndLogStackTrace(Local } bool IsTypeEncondingHandldedByDebugMessages(const TypeEncoding* typeEncoding) { - if (typeEncoding->type != BinaryTypeEncodingType::InterfaceDeclarationReference && + if (!typeEncoding->isInterfaceReference() && typeEncoding->type != BinaryTypeEncodingType::StructDeclarationReference && typeEncoding->type != BinaryTypeEncodingType::IdEncoding) { return true; @@ -1803,7 +1799,9 @@ bool IsTypeEncondingHandldedByDebugMessages(const TypeEncoding* typeEncoding) { void LogWriteValueTraceMessage(Local context, const TypeEncoding* typeEncoding, void* dest, Local arg) { Isolate* isolate = v8::Isolate::GetCurrent(); - std::string destName = typeEncoding->details.interfaceDeclarationReference.name.valuePtr(); + std::string destName = typeEncoding->isInterfaceReference() + ? typeEncoding->interfaceName() + : typeEncoding->details.declarationReference.name.valuePtr(); std::string originName = tns::ToString(isolate, arg); if (originName == "") { // empty string diff --git a/NativeScript/runtime/Metadata.h b/NativeScript/runtime/Metadata.h index f7bcb76b..1c6af057 100644 --- a/NativeScript/runtime/Metadata.h +++ b/NativeScript/runtime/Metadata.h @@ -100,40 +100,46 @@ enum MemberType { }; enum BinaryTypeEncodingType : uint8_t { - VoidEncoding, - BoolEncoding, - ShortEncoding, - UShortEncoding, - IntEncoding, - UIntEncoding, - LongEncoding, - ULongEncoding, - LongLongEncoding, - ULongLongEncoding, - CharEncoding, - UCharEncoding, - UnicharEncoding, - CharSEncoding, - CStringEncoding, - FloatEncoding, - DoubleEncoding, - InterfaceDeclarationReference, - StructDeclarationReference, - UnionDeclarationReference, - PointerEncoding, - VaListEncoding, - SelectorEncoding, - ClassEncoding, - ProtocolEncoding, - InstanceTypeEncoding, - IdEncoding, - ConstantArrayEncoding, - IncompleteArrayEncoding, - FunctionPointerEncoding, - BlockEncoding, - AnonymousStructEncoding, - AnonymousUnionEncoding, - ExtVectorEncoding + VoidEncoding, + BoolEncoding, + ShortEncoding, + UShortEncoding, + IntEncoding, + UIntEncoding, + LongEncoding, + ULongEncoding, + LongLongEncoding, + ULongLongEncoding, + CharEncoding, + UCharEncoding, + UnicharEncoding, + CharSEncoding, + CStringEncoding, + FloatEncoding, + DoubleEncoding, + InterfaceDeclarationReference, + StructDeclarationReference, + UnionDeclarationReference, + PointerEncoding, + VaListEncoding, + SelectorEncoding, + ClassEncoding, + ProtocolEncoding, + InstanceTypeEncoding, + IdEncoding, + ConstantArrayEncoding, + IncompleteArrayEncoding, + FunctionPointerEncoding, + BlockEncoding, + AnonymousStructEncoding, + AnonymousUnionEncoding, + ExtVectorEncoding, + // Same meaning as InterfaceDeclarationReference with an empty protocol list, + // but names the class by index into MetaFile::classNames() instead of + // carrying a name pointer and a protocols pointer. Positionally mirrored in + // the generator's binary::BinaryTypeEncodingType — append only, never + // reorder. + InterfaceIndexReference }; #pragma pack(push, 1) @@ -324,6 +330,14 @@ struct ModuleTable { } }; +/// Class names referenced by InterfaceIndexReference encodings, so a reference +/// costs a 2-byte index instead of a 4-byte name pointer. +struct ClassNameTable { + Array names; + + int sizeInBytes() const { return names.sizeInBytes(); } +}; + struct MetaFile { private: GlobalTable _globalTableJs; @@ -333,6 +347,11 @@ struct MetaFile { static MetaFile* setInstance(void* metadataPtr); + /// Resolved once by setInstance. Locating the table means walking every + /// preceding table, which is too much work to repeat per marshalled + /// argument. + static const ClassNameTable* classNames(); + const GlobalTable* globalTableJs() const { return &this->_globalTableJs; } @@ -352,9 +371,15 @@ struct MetaFile { return reinterpret_cast(offset(gt, gt->sizeInBytes())); } + const ClassNameTable* classNamesTable() const { + const ModuleTable* mt = this->topLevelModulesTable(); + return reinterpret_cast( + offset(mt, mt->sizeInBytes())); + } + const void* heap() const { - const ModuleTable* mt = this->topLevelModulesTable(); - return offset(mt, mt->sizeInBytes()); + const ClassNameTable* ct = this->classNamesTable(); + return offset(ct, ct->sizeInBytes()); } }; @@ -426,6 +451,9 @@ union TypeEncodingDetails { String name; PtrTo> _protocols; } interfaceDeclarationReference; + struct InterfaceIndexReferenceDetails { + uint16_t index; + } interfaceIndexReference; struct PointerDetails { const TypeEncoding* getInnerType() const { return reinterpret_cast(this); @@ -452,6 +480,39 @@ struct TypeEncoding { BinaryTypeEncodingType type; TypeEncodingDetails details; + /// An interface reference has two spellings; test with this rather than + /// comparing against InterfaceDeclarationReference, or the indexed form + /// silently falls through to whatever the default branch does. + bool isInterfaceReference() const { + return this->type == + BinaryTypeEncodingType::InterfaceDeclarationReference || + this->type == BinaryTypeEncodingType::InterfaceIndexReference; + } + + /// Referenced class name, for either spelling. nullptr if not an interface + /// reference. + const char* interfaceName() const { + switch (this->type) { + case BinaryTypeEncodingType::InterfaceDeclarationReference: + return this->details.interfaceDeclarationReference.name.valuePtr(); + case BinaryTypeEncodingType::InterfaceIndexReference: + return MetaFile::classNames() + ->names[this->details.interfaceIndexReference.index] + .valuePtr(); + default: + return nullptr; + } + } + + /// Conformed protocols, for either spelling. The indexed form only encodes + /// references that had none, so it reports an empty list. + const Array* interfaceProtocols() const { + return this->type == BinaryTypeEncodingType::InterfaceDeclarationReference + ? this->details.interfaceDeclarationReference._protocols + .valuePtr() + : nullptr; + } + const TypeEncoding* next() const { const TypeEncoding* afterTypePtr = reinterpret_cast(offset(this, sizeof(type))); @@ -488,6 +549,11 @@ struct TypeEncoding { case BinaryTypeEncodingType::InterfaceDeclarationReference: { return reinterpret_cast(offset(afterTypePtr, sizeof(TypeEncodingDetails::InterfaceDeclarationReferenceDetails))); } + case BinaryTypeEncodingType::InterfaceIndexReference: { + return reinterpret_cast(offset( + afterTypePtr, + sizeof(TypeEncodingDetails::InterfaceIndexReferenceDetails))); + } case BinaryTypeEncodingType::StructDeclarationReference: case BinaryTypeEncodingType::UnionDeclarationReference: { return reinterpret_cast(offset(afterTypePtr, sizeof(TypeEncodingDetails::DeclarationReferenceDetails))); diff --git a/NativeScript/runtime/Metadata.mm b/NativeScript/runtime/Metadata.mm index 7f75c459..298f4184 100644 --- a/NativeScript/runtime/Metadata.mm +++ b/NativeScript/runtime/Metadata.mm @@ -324,8 +324,13 @@ void collectInheritanceChainMembers(const char* identifier, size_t length, Membe MetaFile* MetaFile::instance() { return metaFileInstance; } +static const ClassNameTable* classNamesInstance = nullptr; + +const ClassNameTable* MetaFile::classNames() { return classNamesInstance; } + MetaFile* MetaFile::setInstance(void* metadataPtr) { metaFileInstance = reinterpret_cast(metadataPtr); + classNamesInstance = metaFileInstance->classNamesTable(); return metaFileInstance; } } // namespace tns diff --git a/metadata-generator/src/Binary/binarySerializer.h b/metadata-generator/src/Binary/binarySerializer.h index be6f65e6..7cf37394 100644 --- a/metadata-generator/src/Binary/binarySerializer.h +++ b/metadata-generator/src/Binary/binarySerializer.h @@ -33,12 +33,11 @@ class BinarySerializer : public ::Meta::MetaVisitor { void serializeLibrary(clang::Module::LinkLibrary* library, binary::LibraryMeta& binaryLib); public: - BinarySerializer(MetaFile* file) - : heapWriter(file->heap_writer()) - , typeEncodingSerializer(heapWriter) - { - this->file = file; - } + BinarySerializer(MetaFile* file) + : heapWriter(file->heap_writer()), + typeEncodingSerializer(heapWriter, file) { + this->file = file; + } void serializeContainer(std::vector > >& container); diff --git a/metadata-generator/src/Binary/binaryStructures.cpp b/metadata-generator/src/Binary/binaryStructures.cpp index 6502fb83..84dc29e2 100644 --- a/metadata-generator/src/Binary/binaryStructures.cpp +++ b/metadata-generator/src/Binary/binaryStructures.cpp @@ -140,6 +140,15 @@ binary::MetaFileOffset binary::DeclarationReferenceEncoding::save(binary::Binary return offset; } +binary::MetaFileOffset binary::InterfaceIndexReferenceEncoding::save( + binary::BinaryWriter& writer) { + binary::MetaFileOffset offset = TypeEncoding::save(writer); + // push_short only fixes the width; push_number masks each byte, so the full + // uint16 range round-trips regardless of the signed parameter type. + writer.push_short((int16_t)this->_index); + return offset; +} + binary::MetaFileOffset binary::InterfaceDeclarationReferenceEncoding::save(binary::BinaryWriter& writer) { binary::MetaFileOffset offset = DeclarationReferenceEncoding::save(writer); diff --git a/metadata-generator/src/Binary/binaryStructures.h b/metadata-generator/src/Binary/binaryStructures.h index 4db63c0b..21fb6f1c 100644 --- a/metadata-generator/src/Binary/binaryStructures.h +++ b/metadata-generator/src/Binary/binaryStructures.h @@ -12,40 +12,43 @@ class MetaFile; class BinaryWriter; enum BinaryTypeEncodingType : uint8_t { - Void, - Bool, - Short, - UShort, - Int, - UInt, - Long, - ULong, - LongLong, - ULongLong, - Char, - UChar, - Unichar, - CharS, - CString, - Float, - Double, - InterfaceDeclarationReference, - StructDeclarationReference, - UnionDeclarationReference, - Pointer, - VaList, - Selector, - Class, - ProtocolType, - InstanceType, - Id, - ConstantArray, - IncompleteArray, - FunctionPointer, - Block, - AnonymousStruct, - AnonymousUnion, - Vector + Void, + Bool, + Short, + UShort, + Int, + UInt, + Long, + ULong, + LongLong, + ULongLong, + Char, + UChar, + Unichar, + CharS, + CString, + Float, + Double, + InterfaceDeclarationReference, + StructDeclarationReference, + UnionDeclarationReference, + Pointer, + VaList, + Selector, + Class, + ProtocolType, + InstanceType, + Id, + ConstantArray, + IncompleteArray, + FunctionPointer, + Block, + AnonymousStruct, + AnonymousUnion, + Vector, + // Mirrors tns::BinaryTypeEncodingType in the runtime's Metadata.h by + // position — append only, never reorder. + InterfaceIndexReference }; // BinaryMetaType values must not exceed @@ -320,6 +323,16 @@ struct DeclarationReferenceEncoding : public TypeEncoding { virtual MetaFileOffset save(BinaryWriter& writer) override; }; +struct InterfaceIndexReferenceEncoding : public TypeEncoding { + public: + InterfaceIndexReferenceEncoding() + : TypeEncoding(BinaryTypeEncodingType::InterfaceIndexReference) {} + + uint16_t _index = 0; + + virtual MetaFileOffset save(BinaryWriter& writer) override; +}; + struct InterfaceDeclarationReferenceEncoding : public DeclarationReferenceEncoding { public: InterfaceDeclarationReferenceEncoding() diff --git a/metadata-generator/src/Binary/binaryTypeEncodingSerializer.cpp b/metadata-generator/src/Binary/binaryTypeEncodingSerializer.cpp index 8589f759..41a15ff8 100644 --- a/metadata-generator/src/Binary/binaryTypeEncodingSerializer.cpp +++ b/metadata-generator/src/Binary/binaryTypeEncodingSerializer.cpp @@ -3,6 +3,23 @@ #include #include "../Meta/MetaEntities.h" +#include "metaFile.h" + +// An interface reference with no protocols is by far the common case, so it is +// encoded as a 2-byte index into the class name table instead of a name pointer +// plus a protocols pointer. Falls back to the pointer form if the table is +// full. +static unique_ptr makeIndexedInterface( + binary::MetaFile* file, binary::BinaryWriter& writer, + const std::string& name) { + uint16_t index = 0; + if (file == nullptr || !file->internClassName(name, writer, index)) { + return nullptr; + } + auto* s = new binary::InterfaceIndexReferenceEncoding(); + s->_index = index; + return unique_ptr(s); +} binary::MetaFileOffset binary::BinaryTypeEncodingSerializer::visit( std::vector< ::Meta::Type*>& types) { @@ -179,6 +196,13 @@ binary::BinaryTypeEncodingSerializer::visitIncompleteArray( unique_ptr binary::BinaryTypeEncodingSerializer::visitInterface( const ::Meta::InterfaceType& type) { + if (type.protocols.empty()) { + if (auto indexed = makeIndexedInterface(this->_file, this->_heapWriter, + type.interface->name)) { + return indexed; + } + } + auto* s = new binary::InterfaceDeclarationReferenceEncoding(); s->_name = this->_heapWriter.push_string(type.interface->name); @@ -202,6 +226,11 @@ binary::BinaryTypeEncodingSerializer::visitBridgedInterface( "BridgedInterfaceType with name '") + type.name + "'."); } + if (auto indexed = makeIndexedInterface(this->_file, this->_heapWriter, + type.bridgedInterface->name)) { + return indexed; + } + auto s = new binary::InterfaceDeclarationReferenceEncoding(); s->_name = this->_heapWriter.push_string(type.bridgedInterface->name); diff --git a/metadata-generator/src/Binary/binaryTypeEncodingSerializer.h b/metadata-generator/src/Binary/binaryTypeEncodingSerializer.h index 2414a848..c69c1b64 100644 --- a/metadata-generator/src/Binary/binaryTypeEncodingSerializer.h +++ b/metadata-generator/src/Binary/binaryTypeEncodingSerializer.h @@ -21,14 +21,15 @@ class BinaryTypeEncodingSerializer // string-interning map, and a copy interns into a second map, emitting a // duplicate copy of every string reachable from both paths. BinaryWriter& _heapWriter; + MetaFile* _file; unique_ptr serializeRecordEncoding( const binary::BinaryTypeEncodingType encodingType, const std::vector< ::Meta::RecordField>& fields); public: - BinaryTypeEncodingSerializer(BinaryWriter& heapWriter) - : _heapWriter(heapWriter) {} + BinaryTypeEncodingSerializer(BinaryWriter& heapWriter, MetaFile* file) + : _heapWriter(heapWriter), _file(file) {} MetaFileOffset visit(std::vector< ::Meta::Type*>& types); diff --git a/metadata-generator/src/Binary/binaryWriter.cpp b/metadata-generator/src/Binary/binaryWriter.cpp index 6f0a0a6a..ae54f960 100644 --- a/metadata-generator/src/Binary/binaryWriter.cpp +++ b/metadata-generator/src/Binary/binaryWriter.cpp @@ -43,12 +43,12 @@ binary::MetaFileOffset binary::BinaryWriter::push_arrayCount(MetaArrayCount coun return this->push_number(count, sizeof(MetaArrayCount)); } -binary::MetaFileOffset binary::BinaryWriter::push_binaryArray(std::vector& binaryArray) -{ +binary::MetaFileOffset binary::BinaryWriter::push_binaryArray( + std::vector& binaryArray, bool shouldIntern) { // Empty arrays carry no payload, so every one of them can share a single // count-of-zero in the heap. Offset 0 is the null sentinel and the heap // reserves a marker byte there, so a real array never lands on it. - if (binaryArray.empty() && this->emptyArrayOffset != 0) { + if (shouldIntern && binaryArray.empty() && this->emptyArrayOffset != 0) { return this->emptyArrayOffset; } @@ -58,7 +58,7 @@ binary::MetaFileOffset binary::BinaryWriter::push_binaryArray(std::vectorpush_pointer(element); } - if (binaryArray.empty()) { + if (shouldIntern && binaryArray.empty()) { this->emptyArrayOffset = offset; } diff --git a/metadata-generator/src/Binary/binaryWriter.h b/metadata-generator/src/Binary/binaryWriter.h index 50a5f0df..4926fe02 100644 --- a/metadata-generator/src/Binary/binaryWriter.h +++ b/metadata-generator/src/Binary/binaryWriter.h @@ -55,11 +55,15 @@ class BinaryWriter : public BinaryOperation { MetaFileOffset push_arrayCount(MetaArrayCount count); /* - * \brief Writes a binary array - * A binary array is a collection of offsets - * \param binaryArray - */ - MetaFileOffset push_binaryArray(std::vector& binaryArray); + * \brief Writes a binary array + * A binary array is a collection of offsets + * \param binaryArray + * \param shouldIntern Whether an empty array may reuse a previously + * written one. Only safe where the array is reached by offset; + * pass \c false when writing a table that is located positionally. + */ + MetaFileOffset push_binaryArray(std::vector& binaryArray, + bool shouldIntern = true); /* * \brief Writes a 4 byte integer. diff --git a/metadata-generator/src/Binary/metaFile.cpp b/metadata-generator/src/Binary/metaFile.cpp index 61ef75a7..d7bf3307 100644 --- a/metadata-generator/src/Binary/metaFile.cpp +++ b/metadata-generator/src/Binary/metaFile.cpp @@ -35,6 +35,26 @@ binary::MetaFileOffset binary::MetaFile::getFromTopLevelModulesTable(const std:: return (it != this->_topLevelModules.end()) ? it->second : 0; } +bool binary::MetaFile::internClassName(const std::string& name, + binary::BinaryWriter& heapWriter, + uint16_t& index) { + auto it = this->_classNameIndices.find(name); + if (it != this->_classNameIndices.end()) { + index = it->second; + return true; + } + + // The index is serialized as uint16, so the table cannot grow past 2^16. + if (this->_classNames.size() > UINT16_MAX) { + return false; + } + + index = (uint16_t)this->_classNames.size(); + this->_classNames.push_back(heapWriter.push_string(name)); + this->_classNameIndices.emplace(name, index); + return true; +} + binary::BinaryWriter binary::MetaFile::heap_writer() { return binary::BinaryWriter(this->_heap); @@ -58,18 +78,26 @@ void binary::MetaFile::save(std::shared_ptr stream) BinaryWriter globalTableStreamWriter = BinaryWriter(stream); BinaryWriter heapWriter = this->heap_writer(); std::vector jsOffsets = this->_globalTableSymbolsJs->serialize(heapWriter); - globalTableStreamWriter.push_binaryArray(jsOffsets); + globalTableStreamWriter.push_binaryArray(jsOffsets, /*shouldIntern*/ false); std::vector nativeProtocolOffsets = this->_globalTableSymbolsNativeProtocols->serialize(heapWriter); - globalTableStreamWriter.push_binaryArray(nativeProtocolOffsets); + globalTableStreamWriter.push_binaryArray(nativeProtocolOffsets, + /*shouldIntern*/ false); std::vector nativeInterfaceOffsets = this->_globalTableSymbolsNativeInterfaces->serialize(heapWriter); - globalTableStreamWriter.push_binaryArray(nativeInterfaceOffsets); + globalTableStreamWriter.push_binaryArray(nativeInterfaceOffsets, + /*shouldIntern*/ false); std::vector modulesOffsets; for (std::pair pair : this->_topLevelModules) modulesOffsets.push_back(pair.second); - globalTableStreamWriter.push_binaryArray(modulesOffsets); + globalTableStreamWriter.push_binaryArray(modulesOffsets, + /*shouldIntern*/ false); + + // Must stay the last table before the heap: the runtime locates the heap by + // walking these tables in order (MetaFile::heap in Metadata.h). + globalTableStreamWriter.push_binaryArray(this->_classNames, + /*shouldIntern*/ false); // dump heap for (auto byteIter = this->_heap->begin(); byteIter != this->_heap->end(); ++byteIter) { diff --git a/metadata-generator/src/Binary/metaFile.h b/metadata-generator/src/Binary/metaFile.h index a65564e1..dd4d4fa3 100644 --- a/metadata-generator/src/Binary/metaFile.h +++ b/metadata-generator/src/Binary/metaFile.h @@ -29,6 +29,10 @@ class MetaFile { std::unique_ptr _globalTableSymbolsNativeInterfaces; std::map _topLevelModules; + // Class names referenced by InterfaceIndexReference encodings, in index + // order, alongside the reverse map used to assign those indices. + std::vector _classNames; + std::map _classNameIndices; std::shared_ptr _heap; public: @@ -85,6 +89,19 @@ class MetaFile { */ binary::MetaFileOffset getFromTopLevelModulesTable(const std::string& moduleName); + /// class name table + /* + * \brief Interns a class name and returns its index in the class name + * table. + * \param name The native class name + * \param heapWriter Writer used to intern the name string in the heap + * \param index Receives the assigned index + * \return false if the table is full, in which case the caller must fall + * back to an encoding that carries a name pointer + */ + bool internClassName(const std::string& name, BinaryWriter& heapWriter, + uint16_t& index); + /// heap /* * \brief Creates a \c BinaryWriter for this file heap From 55ff4964d47d7103989d6bd35549d53c797350a5 Mon Sep 17 00:00:00 2001 From: Eduardo Speroni Date: Tue, 11 Aug 2026 16:13:08 -0300 Subject: [PATCH 5/6] perf(metadata): share identical type-encoding lists Every method and function signature wrote its own encoding list, so common shapes were stored thousands of times over. Lists are now serialized to scratch and interned by their exact bytes; nothing in them depends on where the list lands, so identical lists are interchangeable. This is worth far more after the class-name index than before it: encoding the common interface reference as a bare index removed the per-reference pointers that used to make otherwise-identical signatures differ. Sharing lists means two declarations can now point at one encoding, which the ffi_cif cache was not prepared for -- it keyed on the encoding pointer alone while callers pass different initialParameterIndex/argsCount, so aliasing would hand back a cif describing the wrong stack. It now keys on all three. --- NativeScript/runtime/FFICall.cpp | 15 ++++++----- NativeScript/runtime/FFICall.h | 26 ++++++++++++++++++- .../Binary/binaryTypeEncodingSerializer.cpp | 24 ++++++++++++++--- metadata-generator/src/Binary/metaFile.cpp | 15 +++++++++++ metadata-generator/src/Binary/metaFile.h | 17 ++++++++++++ 5 files changed, 87 insertions(+), 10 deletions(-) diff --git a/NativeScript/runtime/FFICall.cpp b/NativeScript/runtime/FFICall.cpp index 3aa23503..ee851105 100644 --- a/NativeScript/runtime/FFICall.cpp +++ b/NativeScript/runtime/FFICall.cpp @@ -278,10 +278,11 @@ StructInfo FFICall::GetStructInfo(size_t fieldsCount, const TypeEncoding* fieldE } ParametrizedCall* ParametrizedCall::Get(const TypeEncoding* typeEncoding, const int initialParameterIndex, const int argsCount) { - auto it = callsCache_.find(typeEncoding); - if (it != callsCache_.end()) { - return it->second; - } + CallKey key{typeEncoding, initialParameterIndex, argsCount}; + auto it = callsCache_.find(key); + if (it != callsCache_.end()) { + return it->second; + } const ffi_type** parameterTypesFFITypes = new const ffi_type*[argsCount](); ffi_type* returnType = FFICall::GetArgumentType(typeEncoding); @@ -301,12 +302,14 @@ ParametrizedCall* ParametrizedCall::Get(const TypeEncoding* typeEncoding, const tns::Assert(status == FFI_OK); ParametrizedCall* call = new ParametrizedCall(cif); - callsCache_.emplace(typeEncoding, call); + callsCache_.emplace(key, call); return call; } -robin_hood::unordered_map ParametrizedCall::callsCache_; +robin_hood::unordered_map + ParametrizedCall::callsCache_; robin_hood::unordered_map FFICall::structInfosCache_; } diff --git a/NativeScript/runtime/FFICall.h b/NativeScript/runtime/FFICall.h index 46666405..77f65344 100644 --- a/NativeScript/runtime/FFICall.h +++ b/NativeScript/runtime/FFICall.h @@ -67,7 +67,31 @@ class ParametrizedCall { std::vector ArgValueOffsets; private: - static robin_hood::unordered_map + // The cif is built from the encoding *and* the two counts, so all three + // identify it. Keying on the encoding alone aliases distinct call shapes onto + // one cif, which mis-describes the stack rather than failing outright. + struct CallKey { + const TypeEncoding* encoding; + int initialParameterIndex; + int argsCount; + + bool operator==(const CallKey& other) const { + return encoding == other.encoding && + initialParameterIndex == other.initialParameterIndex && + argsCount == other.argsCount; + } + }; + + struct CallKeyHash { + size_t operator()(const CallKey& key) const { + size_t hash = robin_hood::hash()(key.encoding); + hash = hash * 31 + static_cast(key.initialParameterIndex); + hash = hash * 31 + static_cast(key.argsCount); + return hash; + } + }; + + static robin_hood::unordered_map callsCache_; }; diff --git a/metadata-generator/src/Binary/binaryTypeEncodingSerializer.cpp b/metadata-generator/src/Binary/binaryTypeEncodingSerializer.cpp index 41a15ff8..55c63745 100644 --- a/metadata-generator/src/Binary/binaryTypeEncodingSerializer.cpp +++ b/metadata-generator/src/Binary/binaryTypeEncodingSerializer.cpp @@ -29,10 +29,28 @@ binary::MetaFileOffset binary::BinaryTypeEncodingSerializer::visit( binaryEncodings.push_back(std::move(binaryEncoding)); } - binary::MetaFileOffset offset = - this->_heapWriter.push_arrayCount(types.size()); + // Serialize to scratch first so identical lists can share one copy. Nothing + // in the bytes depends on where the list lands, so they are interchangeable. + auto scratch = std::make_shared(); + binary::BinaryWriter scratchWriter(scratch); + scratchWriter.push_arrayCount(types.size()); for (unique_ptr& binaryEncoding : binaryEncodings) { - binaryEncoding->save(this->_heapWriter); + binaryEncoding->save(scratchWriter); + } + std::string bytes(scratch->begin(), scratch->end()); + + binary::MetaFileOffset offset = 0; + if (this->_file != nullptr && + this->_file->tryGetEncodingList(bytes, offset)) { + return offset; + } + + offset = this->_heapWriter.currentPosition(); + for (char byte : bytes) { + this->_heapWriter.push_byte((uint8_t)byte); + } + if (this->_file != nullptr) { + this->_file->recordEncodingList(bytes, offset); } return offset; } diff --git a/metadata-generator/src/Binary/metaFile.cpp b/metadata-generator/src/Binary/metaFile.cpp index d7bf3307..844d9265 100644 --- a/metadata-generator/src/Binary/metaFile.cpp +++ b/metadata-generator/src/Binary/metaFile.cpp @@ -55,6 +55,21 @@ bool binary::MetaFile::internClassName(const std::string& name, return true; } +bool binary::MetaFile::tryGetEncodingList(const std::string& bytes, + binary::MetaFileOffset& offset) { + auto it = this->_encodingLists.find(bytes); + if (it == this->_encodingLists.end()) { + return false; + } + offset = it->second; + return true; +} + +void binary::MetaFile::recordEncodingList(const std::string& bytes, + binary::MetaFileOffset offset) { + this->_encodingLists.emplace(bytes, offset); +} + binary::BinaryWriter binary::MetaFile::heap_writer() { return binary::BinaryWriter(this->_heap); diff --git a/metadata-generator/src/Binary/metaFile.h b/metadata-generator/src/Binary/metaFile.h index dd4d4fa3..473a798f 100644 --- a/metadata-generator/src/Binary/metaFile.h +++ b/metadata-generator/src/Binary/metaFile.h @@ -33,6 +33,10 @@ class MetaFile { // order, alongside the reverse map used to assign those indices. std::vector _classNames; std::map _classNameIndices; + // Serialized encoding lists keyed by their exact bytes. The bytes hold + // absolute heap offsets but none that depend on where the list itself + // lands, so identical lists are interchangeable. + std::map _encodingLists; std::shared_ptr _heap; public: @@ -102,6 +106,19 @@ class MetaFile { bool internClassName(const std::string& name, BinaryWriter& heapWriter, uint16_t& index); + /// type encodings + /* + * \brief Looks up an already-written encoding list with identical bytes. + * \return true and sets \c offset when one exists + */ + bool tryGetEncodingList(const std::string& bytes, MetaFileOffset& offset); + + /* + * \brief Records the offset an encoding list was written at, so identical + * lists can share it. + */ + void recordEncodingList(const std::string& bytes, MetaFileOffset offset); + /// heap /* * \brief Creates a \c BinaryWriter for this file heap From ecaa9b1fb41aa88af363027bcf37b6417bbd75de Mon Sep 17 00:00:00 2001 From: Eduardo Speroni Date: Tue, 11 Aug 2026 16:18:59 -0300 Subject: [PATCH 6/6] perf(metadata): store enums as a name/value table Enums were serialized as JS source, __tsEnum({"A":1,...}), compiled and run on first access. Almost all of that text was redundant: every member name is already interned in the heap because the same enum's constants are emitted as standalone globals, so the source string held a second copy of each name plus JSON punctuation. They now serialize as a count followed by (name offset, int64 value) pairs and the runtime builds the object directly. Entries stay in stored order because __tsEnum's reverse mapping is last-write-wins, so reordering would change which name a duplicated value maps back to. Also fixes push_number, which masked with `255 << pad`. That shifts an int, so it overflowed past the fourth byte and silently zeroed the high half of every 8-byte value. Caught by an enum whose members use bits 62 and 63. --- NativeScript/runtime/Metadata.h | 16 +++++++++ NativeScript/runtime/MetadataBuilder.mm | 35 ++++++++++++++----- .../src/Binary/binarySerializer.cpp | 35 +++++++++++-------- .../src/Binary/binaryStructures.h | 3 ++ .../src/Binary/binaryWriter.cpp | 12 +++++-- metadata-generator/src/Binary/binaryWriter.h | 6 ++++ 6 files changed, 81 insertions(+), 26 deletions(-) diff --git a/NativeScript/runtime/Metadata.h b/NativeScript/runtime/Metadata.h index 1c6af057..b364008e 100644 --- a/NativeScript/runtime/Metadata.h +++ b/NativeScript/runtime/Metadata.h @@ -74,6 +74,7 @@ enum MetaFlags { MethodOwnsReturnedCocoaObject = 4, MethodHasErrorOutParameter = 5, MethodHasConstructorTokens = 9, + JsCodeIsEnumTable = 10, PropertyHasGetter = 2, PropertyHasSetter = 3, @@ -773,15 +774,30 @@ struct FunctionMeta : Meta { } }; +struct EnumField { + String name; + int64_t value; +}; + struct JsCodeMeta : Meta { private: String _jsCode; public: + /// Enums carry a name/value table here instead of JS source; the two are + /// mutually exclusive, so check this before reading either. + inline bool isEnumTable() const { + return this->flag(MetaFlags::JsCodeIsEnumTable); + } + inline const char* jsCode() const { return _jsCode.valuePtr(); } + + inline const Array* enumFields() const { + return reinterpret_cast*>(_jsCode.valuePtr()); + } }; struct VarMeta : Meta { diff --git a/NativeScript/runtime/MetadataBuilder.mm b/NativeScript/runtime/MetadataBuilder.mm index 43d2a3eb..a1370b38 100644 --- a/NativeScript/runtime/MetadataBuilder.mm +++ b/NativeScript/runtime/MetadataBuilder.mm @@ -134,16 +134,33 @@ NamedPropertyHandlerConfiguration config(MetadataBuilder::GlobalPropertyGetter, info.GetReturnValue().Set(result); } else if (meta->type() == MetaType::JsCode) { const JsCodeMeta* jsCodeMeta = static_cast(meta); - std::string jsCode = jsCodeMeta->jsCode(); - Local