// Copyright Epic Games, Inc. All Rights Reserved. #include "Serialization/UnversionedPropertySerialization.h" #include "Serialization/UnversionedPropertySerializationTest.h" #include "Interfaces/ITargetPlatform.h" #include "Misc/ScopeRWLock.h" #include "UObject/UnrealType.h" // Caches a property array per UStruct to avoid link-walking and touching FProperty data. // // As a reference point, this uses ~6MB of memory in an internal project and makes all // unversioned property loading except non-numeric SerializeItem() calls about 2x faster. #ifndef CACHE_UNVERSIONED_PROPERTY_SCHEMA // x64 platforms tend to have more memory... #define CACHE_UNVERSIONED_PROPERTY_SCHEMA (PLATFORM_CPU_X86_FAMILY && PLATFORM_64BITS) #endif // Helper to pass around appropriate default value types depending on CACHE_UNVERSIONED_PROPERTY_SCHEMA struct FDefaultStruct { const uint8* Data; #if CACHE_UNVERSIONED_PROPERTY_SCHEMA uint32 StructSize; FDefaultStruct(const uint8* InData, UStruct* InStruct): Data(InData), StructSize(InData ? InStruct->GetPropertiesSize() : 0) {} #else UStruct* Struct; FDefaultStruct(const uint8* InData, UStruct* InStruct): Data(InData), Struct(InStruct) {} #endif }; // Serializes a FProperty at a specific static array index // // Extracts and caches relevant FProperty state when using // CACHE_UNVERSIONED_PROPERTY_SCHEMA to improve data locality. // Otherwise, extracts only the needed state on demand. class FUnversionedPropertySerializer { public: FUnversionedPropertySerializer(FProperty* InProperty, int32 InArrayIndex) : Property(InProperty) #if CACHE_UNVERSIONED_PROPERTY_SCHEMA , Offset(Property->GetOffset_ForInternal() + Property->ElementSize * InArrayIndex), bSerializeAsInteger(CanSerializeAsInteger(Property)), IntType(GetIntType(Property->GetMinAlignment())), FastZeroIntNum(CanSerializeAsZero(InProperty, IntType) ? GetIntNum(InProperty, IntType) : uint8(0)) #else , ArrayIndex(InArrayIndex) #endif {} FProperty* GetProperty() const { return Property; } void* GetValue(uint8* Data) const { #if CACHE_UNVERSIONED_PROPERTY_SCHEMA return Data + Offset; #else return Property->ContainerPtrToValuePtr(Data, ArrayIndex); #endif } const void* GetValue(const uint8* Data) const { return GetValue(const_cast(Data)); } const void* GetDefaultValue(FDefaultStruct Defaults) const { #if CACHE_UNVERSIONED_PROPERTY_SCHEMA return Offset < Defaults.StructSize ? Defaults.Data + Offset : nullptr; #else return Property->ContainerPtrToValuePtrForDefaults(Defaults.Struct, Defaults.Data); #endif } FORCEINLINE void Serialize(FStructuredArchive::FSlot Slot, uint8* Data, FDefaultStruct Defaults) const { #if !CACHE_UNVERSIONED_PROPERTY_SCHEMA bool bSerializeAsInteger = CanSerializeAsInteger(Property); #endif if (bSerializeAsInteger) { #if !CACHE_UNVERSIONED_PROPERTY_SCHEMA EIntegerType IntType = GetIntType(Property->GetMinAlignment()); #endif SerializeAsInteger(Slot, GetValue(Data), IntType); } else { // Note that each bitfield stores a redundant byte -- the zero mask could tell us if the bit is 0 or 1. // // Removing this redundant byte and the FBoolProperty::SerializeItem() calls would add some complexity, // but only reduce data size by ~0.5% in the data set I used to guide trade-offs. It would not make much // difference for loading performance either. --jtorp Property->SerializeItem(Slot, GetValue(Data), GetDefaultValue(Defaults)); } } FORCEINLINE void LoadZero(uint8* Data) const { #if !CACHE_UNVERSIONED_PROPERTY_SCHEMA EIntegerType IntType = GetIntType(Property->GetMinAlignment()); uint32 FastZeroIntNum = GetIntNum(Property, IntType); #endif switch (IntType) { case EIntegerType::Uint8: MemZeroRange(GetValue(Data), FastZeroIntNum); break; case EIntegerType::Uint16: MemZeroRange(GetValue(Data), FastZeroIntNum); break; case EIntegerType::Uint32: MemZeroRange(GetValue(Data), FastZeroIntNum); break; case EIntegerType::Uint64: MemZeroRange(GetValue(Data), FastZeroIntNum); break; default: UE_ASSUME(0); } } bool ShouldSaveAsZero(const uint8* Data) const { #if !CACHE_UNVERSIONED_PROPERTY_SCHEMA EIntegerType IntType = GetIntType(Property->GetMinAlignment()); uint32 FastZeroIntNum = CanSerializeAsZero(Property, IntType) ? GetIntNum(Property, IntType) : uint8(0); #endif // Can simplified and faster using a switch() statement like LoadZero() if (FastZeroIntNum == 1) { return IsIntZero(GetValue(Data), IntType); } else { return FastZeroIntNum > 0 && IsIntRangeZero(GetValue(Data), FastZeroIntNum, IntType); } } bool IsDefault(const uint8* Data, FDefaultStruct Defaults, uint32 PortFlags) const { return Property->Identical(GetValue(Data), GetDefaultValue(Defaults), PortFlags); } private: enum class EIntegerType : uint8 { Uint8, Uint16, Uint32, Uint64 }; static uint32 GetIntNum(const FProperty* Property, EIntegerType IntType) { return Property->ElementSize / GetSizeOf(IntType); } static bool CanSerializeAsZero(const FProperty* Property, EIntegerType IntType) { static constexpr uint32 MaxZeroComparisons = 16; uint64 CastFlags = Property->GetClass()->GetCastFlags(); if ((CastFlags & (CASTCLASS_FStructProperty | CASTCLASS_FBoolProperty)) == 0) { checkf(GetIntNum(Property, IntType) < MaxZeroComparisons, TEXT("Unexpectedly large property type encountered %s"), *Property->GetName()); return true; } else if ((CastFlags & CASTCLASS_FBoolProperty) != 0) { return static_cast(Property)->IsNativeBool(); } else { bool bIsAtomic = !!(static_cast(Property)->Struct->StructFlags & STRUCT_Atomic); return bIsAtomic && GetIntNum(Property, IntType) < MaxZeroComparisons; } } static bool CanSerializeAsInteger(FProperty* Property) { uint64 CastFlags = Property->GetClass()->GetCastFlags(); if ((CastFlags & CASTCLASS_FBoolProperty) != 0) { return static_cast(Property)->IsNativeBool(); } return (CastFlags & (CASTCLASS_FNumericProperty | CASTCLASS_FEnumProperty)) != 0; } template static void SerializeAs(FStructuredArchive::FSlot Slot, void* Value) { Slot << *reinterpret_cast(Value); } static void SerializeAsInteger(FStructuredArchive::FSlot Slot, void* Value, EIntegerType IntType) { switch (IntType) { case EIntegerType::Uint8: Slot << *reinterpret_cast(Value); break; case EIntegerType::Uint16: Slot << *reinterpret_cast(Value); break; case EIntegerType::Uint32: Slot << *reinterpret_cast(Value); break; case EIntegerType::Uint64: Slot << *reinterpret_cast(Value); break; default: UE_ASSUME(0); } } template static bool IsZero(const void* Value) { return *reinterpret_cast(Value) == 0; } static bool IsIntZero(const void* Value, EIntegerType IntType) { static constexpr bool (*Functions[4])(const void*) = {&IsZero, &IsZero, &IsZero, &IsZero}; return (*Functions[static_cast(IntType)])(Value); } template static bool IsRangeZero(const void* Value, uint32 Num) { T Out = 0; for (uint32 Idx = 0; Idx < Num; ++Idx) { Out |= reinterpret_cast(Value)[Idx]; } return Out == 0; } static bool IsIntRangeZero(const void* Value, uint32 Num, EIntegerType IntType) { static constexpr bool (*Functions[4])(const void*, uint32 Num) = {&IsRangeZero, &IsRangeZero, &IsRangeZero, &IsRangeZero}; return (*Functions[static_cast(IntType)])(Value, Num); } template FORCEINLINE static void MemZeroRange(void* Value, uint32 Num) { UE_ASSUME(Num > 0); for (uint32 Idx = 0; Idx < Num; ++Idx) { reinterpret_cast(Value)[Idx] = 0; } } static uint32 GetSizeOf(EIntegerType Type) { return 1u << static_cast(Type); } // @param Bytes must be 1, 2, 4 or 8 static constexpr uint32 Log2For1248(uint32 Bytes) { return (Bytes >> 1) - (Bytes >> 3); } static EIntegerType GetIntType(uint32 Alignment) { if (Alignment >= 8) { return EIntegerType::Uint64; } return static_cast(Log2For1248(Alignment)); } FProperty* Property; #if CACHE_UNVERSIONED_PROPERTY_SCHEMA uint32 Offset; bool bSerializeAsInteger; EIntegerType IntType; uint8 FastZeroIntNum; #else uint32 ArrayIndex; #endif }; #if CACHE_UNVERSIONED_PROPERTY_SCHEMA // Serialization is based on indices into this property array struct FUnversionedStructSchema { uint32 Num; FUnversionedPropertySerializer Serializers[0]; static FUnversionedStructSchema* Create(const UStruct* Struct) { TArray> Serializers; for (FProperty* Property = Struct->PropertyLink; Property; Property = Property->PropertyLinkNext) { if (!Property->IsEditorOnlyProperty()) { for (int32 ArrayIdx = 0, ArrayDim = Property->ArrayDim; ArrayIdx < ArrayDim; ++ArrayIdx) { Serializers.Add(FUnversionedPropertySerializer(Property, ArrayIdx)); } } } uint32 Bytes = sizeof(FUnversionedStructSchema) + Serializers.Num() * sizeof(FUnversionedPropertySerializer); FUnversionedStructSchema* Schema = reinterpret_cast(FMemory::Malloc(Bytes, alignof(FUnversionedPropertySerializer))); Schema->Num = Serializers.Num(); FMemory::Memcpy(Schema->Serializers, Serializers.GetData(), Serializers.Num() * sizeof(FUnversionedPropertySerializer)); return Schema; } }; const FUnversionedStructSchema& GetOrCreateUnversionedSchema(const UStruct* Struct) { if (const FUnversionedStructSchema* ExistingSchema = Struct->UnversionedSchema) { return *ExistingSchema; } FUnversionedStructSchema* CreatedSchema = FUnversionedStructSchema::Create(Struct); void** CachedSchemaPtr = reinterpret_cast(const_cast(&Struct->UnversionedSchema)); if (const FUnversionedStructSchema* ExistingSchema = reinterpret_cast(FPlatformAtomics::InterlockedCompareExchangePointer(CachedSchemaPtr, CreatedSchema, nullptr))) { delete CreatedSchema; return *ExistingSchema; } return *CreatedSchema; } using FUnversionedSchemaIterator = const FUnversionedPropertySerializer*; #else struct FLinkWalkingSchemaIterator { FProperty* Property = nullptr; uint32 ArrayIndex = 0; FLinkWalkingSchemaIterator() {} explicit FLinkWalkingSchemaIterator(FProperty* FirstProperty) : Property(SkipEditorOnlyProperties(FirstProperty)) {} void operator++() { check(Property); if (ArrayIndex + 1 == Property->ArrayDim) { Property = SkipEditorOnlyProperties(Property->PropertyLinkNext); ArrayIndex = 0; } else { ++ArrayIndex; } } void operator+=(uint32 Num) { while (Num--) { this->operator++(); } } FUnversionedPropertySerializer operator*() const { return FUnversionedPropertySerializer(Property, ArrayIndex); } bool operator!=(FLinkWalkingSchemaIterator Rhs) const { return (Property != Rhs.Property) | (ArrayIndex != Rhs.ArrayIndex); } static FProperty* SkipEditorOnlyProperties(FProperty* Property) { #if WITH_EDITORONLY_DATA while (Property && Property->IsEditorOnlyProperty()) { Property = Property->PropertyLinkNext; } #endif return Property; } }; using FUnversionedSchemaIterator = FLinkWalkingSchemaIterator; #endif // CACHE_UNVERSIONED_PROPERTY_SCHEMA struct FUnversionedSchemaRange { explicit FUnversionedSchemaRange(const UStruct* Struct) { #if CACHE_UNVERSIONED_PROPERTY_SCHEMA const FUnversionedStructSchema& Schema = GetOrCreateUnversionedSchema(Struct); Begin = Schema.Serializers; End = Schema.Serializers + Schema.Num; #else Begin = FUnversionedSchemaIterator(Struct->PropertyLink); // End is default-initialized #endif } FUnversionedSchemaIterator begin() const { return Begin; } FUnversionedSchemaIterator end() const { return End; } FUnversionedSchemaIterator Begin; FUnversionedSchemaIterator End; }; // List of serialized property indices and which of them are non-zero. // // Serialized as a stream of 16-bit skip-x keep-y fragments and a zero bitmask. class FUnversionedHeader { public: void Save(FStructuredArchive::FStream Stream) const { for (FFragment Fragment: Fragments) { uint16 Packed = Fragment.Pack(); Stream.EnterElement() << Packed; } if (ZeroMask.Num() > 0) { SaveZeroMaskData(Stream, ZeroMask.Num(), ZeroMask.GetData()); } } void Load(FStructuredArchive::FStream Stream) { FFragment Fragment; uint32 ZeroMaskNum = 0; uint32 UnmaskedNum = 0; do { uint16 Packed; Stream.EnterElement() << Packed; Fragment = FFragment::Unpack(Packed); Fragments.Add(Fragment); (Fragment.bHasAnyZeroes ? ZeroMaskNum : UnmaskedNum) += Fragment.ValueNum; } while (!Fragment.bIsLast); if (ZeroMaskNum > 0) { ZeroMask.SetNumUninitialized(ZeroMaskNum); LoadZeroMaskData(Stream, ZeroMaskNum, ZeroMask.GetData()); bHasNonZeroValues = UnmaskedNum > 0 || ZeroMask.Find(false) != INDEX_NONE; } else { bHasNonZeroValues = UnmaskedNum > 0; } } bool HasValues() const { return bHasNonZeroValues | (ZeroMask.Num() > 0); } bool HasNonZeroValues() const { return bHasNonZeroValues; } protected: struct FFragment { static constexpr uint32 SkipMax = 127; static constexpr uint32 ValueMax = 127; uint8 SkipNum = 0; // Number of properties to skip before values bool bHasAnyZeroes = false; uint8 ValueNum = 0; // Number of subsequent property values stored bool bIsLast = 0; // Is this the last fragment of the header? static constexpr uint32 SkipNumMask = 0x007fu; static constexpr uint32 HasZeroMask = 0x0080u; static constexpr uint32 ValueNumShift = 9u; static constexpr uint32 IsLastMask = 0x0100u; uint16 Pack() const { return SkipNum | (bHasAnyZeroes ? HasZeroMask : 0) | (uint16)ValueNum << ValueNumShift | (bIsLast ? IsLastMask : 0); } static FFragment Unpack(uint16 Int) { FFragment Fragment; Fragment.SkipNum = static_cast(Int & SkipNumMask); Fragment.bHasAnyZeroes = (Int & HasZeroMask) != 0; Fragment.ValueNum = static_cast(Int >> ValueNumShift); Fragment.bIsLast = (Int & IsLastMask) != 0; return Fragment; } }; using FZeroMask = TBitArray>; alignas(PLATFORM_CACHE_LINE_SIZE) TArray> Fragments; bool bHasNonZeroValues = false; FZeroMask ZeroMask; void SaveZeroMaskData(FStructuredArchive::FStream Stream, uint32 NumBits, const uint32* Data) const { check(NumBits > 0); uint32 LastWordMask = ~0u >> ((32u - NumBits) % 32u); if (NumBits <= 8) { uint8 Word = static_cast(*Data & LastWordMask); checkf(Word != 0, TEXT("Zero mask shouldn't be saved when no bits are set")); Stream << Word; } else if (NumBits <= 16) { uint16 Word = static_cast(*Data & LastWordMask); checkf(Word != 0, TEXT("Zero mask shouldn't be saved when no bits are set")); Stream << Word; } else { uint32 NumWords = FMath::DivideAndRoundUp(NumBits, 32u); for (uint32 WordIdx = 0; WordIdx < NumWords - 1; ++WordIdx) { uint32 Word = Data[WordIdx]; Stream << Word; } uint32 LastWord = Data[NumWords - 1] & LastWordMask; Stream << LastWord; } } void LoadZeroMaskData(FStructuredArchive::FStream Stream, uint32 NumBits, uint32* Data) { if (NumBits <= 8) { uint8 Int; Stream << Int; *Data = Int; } else if (NumBits <= 16) { uint16 Int; Stream << Int; *Data = Int; } else { for (uint32 Idx = 0, Num = FMath::DivideAndRoundUp(NumBits, 32u); Idx < Num; ++Idx) { Stream << Data[Idx]; } } } public: class FIterator { public: FORCEINLINE FIterator(const FUnversionedHeader& Header, const FUnversionedSchemaRange& Schema) : SchemaIt(Schema.Begin), ZeroMask(Header.ZeroMask), FragmentIt(Header.Fragments.GetData()), bDone(!Header.HasValues()) #if DO_CHECK , SchemaEnd(Schema.End) #endif { if (!bDone) { Skip(); } } void Next() { ++SchemaIt; --RemainingFragmentValues; ZeroMaskIndex += FragmentIt->bHasAnyZeroes; if (RemainingFragmentValues == 0) { if (FragmentIt->bIsLast) { bDone = true; } else { ++FragmentIt; Skip(); } } } explicit operator bool() const { return !bDone; } FUnversionedPropertySerializer GetSerializer() const { check(SchemaIt != SchemaEnd); return *SchemaIt; } bool IsNonZero() const { return !FragmentIt->bHasAnyZeroes || !ZeroMask[ZeroMaskIndex]; } private: FUnversionedSchemaIterator SchemaIt; const FZeroMask& ZeroMask; const FFragment* FragmentIt = nullptr; bool bDone = false; uint32 ZeroMaskIndex = 0; uint32 RemainingFragmentValues = 0; #if DO_CHECK FUnversionedSchemaIterator SchemaEnd; #endif void Skip() { SchemaIt += FragmentIt->SkipNum; while (FragmentIt->ValueNum == 0) { check(!FragmentIt->bIsLast); ++FragmentIt; SchemaIt += FragmentIt->SkipNum; } RemainingFragmentValues = FragmentIt->ValueNum; } }; }; class FUnversionedHeaderBuilder: public FUnversionedHeader { public: FUnversionedHeaderBuilder() { Fragments.AddDefaulted(); } void IncludeProperty(bool bIsZero) { if (Fragments.Last().ValueNum == FFragment::ValueMax) { TrimZeroMask(Fragments.Last()); Fragments.AddDefaulted(); } ++Fragments.Last().ValueNum; Fragments.Last().bHasAnyZeroes |= bIsZero; ZeroMask.Add(bIsZero); bHasNonZeroValues |= !bIsZero; } void ExcludeProperty() { if (Fragments.Last().ValueNum || Fragments.Last().SkipNum == FFragment::SkipMax) { TrimZeroMask(Fragments.Last()); Fragments.AddDefaulted(); } ++Fragments.Last().SkipNum; } void Finalize() { TrimZeroMask(Fragments.Last()); // Trim trailing skips while (Fragments.Num() > 1 && Fragments.Last().ValueNum == 0) { checkf(!Fragments.Last().bHasAnyZeroes, TEXT("No values implies no zero-values")); Fragments.Pop(/* allow shrink */ false); } Fragments.Last().bIsLast = true; } private: void TrimZeroMask(const FFragment& Fragment) { if (!Fragment.bHasAnyZeroes) { ZeroMask.RemoveAt(ZeroMask.Num() - Fragment.ValueNum, Fragment.ValueNum); } } }; static bool CanUseUnversionedPropertySerialization() { bool bTemp; static bool bAllow = GConfig->GetBool(TEXT("Core.System"), TEXT("CanUseUnversionedPropertySerialization"), bTemp, GEngineIni) && bTemp; return bAllow; } static bool CanUseUnversionedPropertySerialization(FConfigFile& TargetIni) { bool bAllow; return TargetIni.GetBool(TEXT("Core.System"), TEXT("CanUseUnversionedPropertySerialization"), /* out */ bAllow) && bAllow; } static bool CanUseUnversionedPropertySerializationForServerOnly(FConfigFile& TargetIni) { bool bAllow; return TargetIni.GetBool(TEXT("Core.System"), TEXT("CanUseUnversionedPropertySerializationForServerOnly"), /* out */ bAllow) && bAllow; } static struct { FRWLock Lock; TMap PlatformValues; } GUPSIniValueCache; bool CanUseUnversionedPropertySerialization(const ITargetPlatform* Target) { if (!Target) { // Use current platform settings return CanUseUnversionedPropertySerialization(); } const bool bIsServerOnly = Target->IsServerOnly(); const int32 IsServerOnlyBit = bIsServerOnly << 31; const int32 TargetID = IsServerOnlyBit | Target->GetPlatformOrdinal(); FWriteScopeLock Scope(GUPSIniValueCache.Lock); if (const bool* CachedValue = GUPSIniValueCache.PlatformValues.Find(TargetID)) { return *CachedValue; } FConfigFile TargetIni; FConfigCacheIni::LoadLocalIniFile(TargetIni, TEXT("Engine"), /* base INI */ true, *Target->IniPlatformName()); bool bTargetValue = CanUseUnversionedPropertySerialization(TargetIni); if (bIsServerOnly) { bTargetValue = bTargetValue && CanUseUnversionedPropertySerializationForServerOnly(TargetIni); } GUPSIniValueCache.PlatformValues.Add(TargetID, bTargetValue); return bTargetValue; } void DestroyUnversionedSchema(const UStruct* Struct) { #if CACHE_UNVERSIONED_PROPERTY_SCHEMA delete Struct->UnversionedSchema; Struct->UnversionedSchema = nullptr; #endif } void SerializeUnversionedProperties(const UStruct* Struct, FStructuredArchive::FSlot Slot, uint8* Data, UStruct* DefaultsStruct, uint8* DefaultsData) { FArchive& UnderlyingArchive = Slot.GetUnderlyingArchive(); FStructuredArchive::FRecord StructRecord = Slot.EnterRecord(); if (UnderlyingArchive.IsLoading()) { check(CanUseUnversionedPropertySerialization()); FUnversionedHeader Header; Header.Load(StructRecord.EnterStream(SA_FIELD_NAME(TEXT("Header")))); if (Header.HasValues()) { FUnversionedSchemaRange Schema(Struct); if (Header.HasNonZeroValues()) { FDefaultStruct Defaults(DefaultsData, DefaultsStruct); FStructuredArchive::FStream ValueStream = StructRecord.EnterStream(SA_FIELD_NAME(TEXT("Values"))); for (FUnversionedHeader::FIterator It(Header, Schema); It; It.Next()) { if (It.IsNonZero()) { It.GetSerializer().Serialize(ValueStream.EnterElement(), Data, Defaults); } else { It.GetSerializer().LoadZero(Data); } } } else { for (FUnversionedHeader::FIterator It(Header, Schema); It; It.Next()) { check(!It.IsNonZero()); It.GetSerializer().LoadZero(Data); } } } } else { FUnversionedPropertyTestRunner TestRunner({Struct, Data, DefaultsStruct, DefaultsData}); FUnversionedPropertyTestCollector TestCollector; // Decide which properties to save and build header based on schema property indices const bool bDense = !UnderlyingArchive.DoDelta() || UnderlyingArchive.IsTransacting() || (!DefaultsData && !dynamic_cast(Struct)); FDefaultStruct Defaults(DefaultsData, DefaultsStruct); FUnversionedSchemaRange Schema(Struct); FUnversionedHeaderBuilder Header; for (FUnversionedPropertySerializer Serializer: Schema) { if (Serializer.GetProperty()->ShouldSerializeValue(UnderlyingArchive) && (bDense || !Serializer.IsDefault(Data, Defaults, UnderlyingArchive.GetPortFlags()))) { Header.IncludeProperty(Serializer.ShouldSaveAsZero(Data)); TestCollector.RecordSavedProperty(Serializer.GetProperty()); } else { Header.ExcludeProperty(); } } Header.Finalize(); // Save header and non-zero values Header.Save(StructRecord.EnterStream(SA_FIELD_NAME(TEXT("Header")))); if (Header.HasNonZeroValues()) { FStructuredArchive::FStream ValueStream = StructRecord.EnterStream(SA_FIELD_NAME(TEXT("Values"))); for (FUnversionedHeader::FIterator It(Header, Schema); It; It.Next()) { if (It.IsNonZero()) { It.GetSerializer().Serialize(ValueStream.EnterElement(), Data, Defaults); } } } } }