diff --git a/Code/Framework/AzCore/AzCore/DOM/Backends/JSON/JsonSerializationUtils.cpp b/Code/Framework/AzCore/AzCore/DOM/Backends/JSON/JsonSerializationUtils.cpp index 17f9bfea54..3baf78b63d 100644 --- a/Code/Framework/AzCore/AzCore/DOM/Backends/JSON/JsonSerializationUtils.cpp +++ b/Code/Framework/AzCore/AzCore/DOM/Backends/JSON/JsonSerializationUtils.cpp @@ -361,7 +361,7 @@ namespace AZ::Dom::Json bool RapidJsonReadHandler::String(const char* str, rapidjson::SizeType length, bool copy) { - const Lifetime lifetime = copy ? m_stringLifetime : Lifetime::Temporary; + const Lifetime lifetime = !copy ? m_stringLifetime : Lifetime::Temporary; return CheckResult(m_visitor->String(AZStd::string_view(str, length), lifetime)); } @@ -373,11 +373,7 @@ namespace AZ::Dom::Json bool RapidJsonReadHandler::Key(const char* str, rapidjson::SizeType length, [[maybe_unused]] bool copy) { AZStd::string_view key = AZStd::string_view(str, length); - if (!m_visitor->SupportsRawKeys()) - { - m_visitor->Key(AZ::Name(key)); - } - const Lifetime lifetime = copy ? m_stringLifetime : Lifetime::Temporary; + const Lifetime lifetime = !copy ? m_stringLifetime : Lifetime::Temporary; return CheckResult(m_visitor->RawKey(key, lifetime)); } diff --git a/Code/Framework/AzCore/AzCore/DOM/DomUtils.cpp b/Code/Framework/AzCore/AzCore/DOM/DomUtils.cpp index 9751b9cecc..c604373296 100644 --- a/Code/Framework/AzCore/AzCore/DOM/DomUtils.cpp +++ b/Code/Framework/AzCore/AzCore/DOM/DomUtils.cpp @@ -21,4 +21,164 @@ namespace AZ::Dom::Utils { return backend.ReadFromBufferInPlace(string.data(), string.size(), visitor); } -} + + AZ::Outcome WriteToValue(const Backend::WriteCallback& writeCallback) + { + Value value; + AZStd::unique_ptr writer = value.GetWriteHandler(); + Visitor::Result result = writeCallback(*writer); + if (!result.IsSuccess()) + { + return AZ::Failure(result.GetError().FormatVisitorErrorMessage()); + } + return AZ::Success(AZStd::move(value)); + } + + bool DeepCompareIsEqual(const Value& lhs, const Value& rhs) + { + const Value::ValueType& lhsValue = lhs.GetInternalValue(); + const Value::ValueType& rhsValue = rhs.GetInternalValue(); + + if (lhs.IsString() && rhs.IsString()) + { + // If we both hold the same ref counted string we don't need to do a full comparison + if (AZStd::holds_alternative(lhsValue) && lhsValue == rhsValue) + { + return true; + } + return lhs.GetString() == rhs.GetString(); + } + + return AZStd::visit( + [&](auto&& ourValue) -> bool + { + using Alternative = AZStd::decay_t; + + if constexpr (AZStd::is_same_v) + { + if (!rhs.IsObject()) + { + return false; + } + auto&& theirValue = AZStd::get>(rhsValue); + if (ourValue == theirValue) + { + return true; + } + + const Object::ContainerType& ourValues = ourValue->GetValues(); + const Object::ContainerType& theirValues = theirValue->GetValues(); + + if (ourValues.size() != theirValues.size()) + { + return false; + } + + for (size_t i = 0; i < ourValues.size(); ++i) + { + const Object::EntryType& lhsChild = ourValues[i]; + const Object::EntryType& rhsChild = theirValues[i]; + if (lhsChild.first != rhsChild.first || !DeepCompareIsEqual(lhsChild.second, rhsChild.second)) + { + return false; + } + } + + return true; + } + else if constexpr (AZStd::is_same_v) + { + if (!rhs.IsArray()) + { + return false; + } + auto&& theirValue = AZStd::get>(rhsValue); + if (ourValue == theirValue) + { + return true; + } + + const Array::ContainerType& ourValues = ourValue->GetValues(); + const Array::ContainerType& theirValues = theirValue->GetValues(); + + if (ourValues.size() != theirValues.size()) + { + return false; + } + + for (size_t i = 0; i < ourValues.size(); ++i) + { + const Value& lhsChild = ourValues[i]; + const Value& rhsChild = theirValues[i]; + if (!DeepCompareIsEqual(lhsChild, rhsChild)) + { + return false; + } + } + + return true; + } + else if constexpr (AZStd::is_same_v) + { + if (!rhs.IsNode()) + { + return false; + } + auto&& theirValue = AZStd::get>(rhsValue); + if (ourValue == theirValue) + { + return true; + } + + const Node& ourNode = *ourValue; + const Node& theirNode = *theirValue; + + const Object::ContainerType& ourProperties = ourNode.GetProperties(); + const Object::ContainerType& theirProperties = theirNode.GetProperties(); + + if (ourProperties.size() != theirProperties.size()) + { + return false; + } + + for (size_t i = 0; i < ourProperties.size(); ++i) + { + const Object::EntryType& lhsChild = ourProperties[i]; + const Object::EntryType& rhsChild = theirProperties[i]; + if (lhsChild.first != rhsChild.first || !DeepCompareIsEqual(lhsChild.second, rhsChild.second)) + { + return false; + } + } + + const Array::ContainerType& ourChildren = ourNode.GetChildren(); + const Array::ContainerType& theirChildren = theirNode.GetChildren(); + + for (size_t i = 0; i < ourChildren.size(); ++i) + { + const Value& lhsChild = ourChildren[i]; + const Value& rhsChild = theirChildren[i]; + if (!DeepCompareIsEqual(lhsChild, rhsChild)) + { + return false; + } + } + + return true; + } + else + { + return lhs == rhs; + } + }, + lhsValue); + } + + Value DeepCopy(const Value& value, bool copyStrings) + { + Value copiedValue; + AZStd::unique_ptr writer = copiedValue.GetWriteHandler(); + value.Accept(*writer, copyStrings); + return copiedValue; + } +} // namespace AZ::Dom::Utils diff --git a/Code/Framework/AzCore/AzCore/DOM/DomUtils.h b/Code/Framework/AzCore/AzCore/DOM/DomUtils.h index 84a6eb5687..5403b93714 100644 --- a/Code/Framework/AzCore/AzCore/DOM/DomUtils.h +++ b/Code/Framework/AzCore/AzCore/DOM/DomUtils.h @@ -9,9 +9,15 @@ #pragma once #include +#include namespace AZ::Dom::Utils { Visitor::Result ReadFromString(Backend& backend, AZStd::string_view string, AZ::Dom::Lifetime lifetime, Visitor& visitor); Visitor::Result ReadFromStringInPlace(Backend& backend, AZStd::string& string, Visitor& visitor); + + AZ::Outcome WriteToValue(const Backend::WriteCallback& writeCallback); + + bool DeepCompareIsEqual(const Value& lhs, const Value& rhs); + Value DeepCopy(const Value& value, bool copyStrings = true); } // namespace AZ::Dom::Utils diff --git a/Code/Framework/AzCore/AzCore/DOM/DomValue.cpp b/Code/Framework/AzCore/AzCore/DOM/DomValue.cpp new file mode 100644 index 0000000000..6d944c9f45 --- /dev/null +++ b/Code/Framework/AzCore/AzCore/DOM/DomValue.cpp @@ -0,0 +1,1207 @@ +/* + * Copyright (c) Contributors to the Open 3D Engine Project. + * For complete copyright and license terms please see the LICENSE at the root of this distribution. + * + * SPDX-License-Identifier: Apache-2.0 OR MIT + * + */ + +#include +#include +#include + +namespace AZ::Dom +{ + namespace Internal + { + template + AZStd::shared_ptr& CheckCopyOnWrite(AZStd::shared_ptr& refCountedPointer) + { + if (refCountedPointer.use_count() == 1) + { + return refCountedPointer; + } + else + { + refCountedPointer = AZStd::allocate_shared(StdValueAllocator(), *refCountedPointer); + return refCountedPointer; + } + } + + template + constexpr size_t GetTypeIndexInternal(size_t index = 0); + + template + constexpr size_t GetTypeIndexInternal(size_t index = 0) + { + if constexpr (AZStd::is_same_v) + { + return index; + } + else + { + return GetTypeIndexInternal(index + 1); + } + } + + template + struct ExtractTypeArgs + { + }; + + template class TypeToExtract, typename... Args> + struct ExtractTypeArgs> + { + template + static constexpr size_t GetTypeIndex() + { + return GetTypeIndexInternal(); + } + }; + } // namespace Internal + + // Helper function, looks up the index of a type within Value::m_value's storage + template + constexpr size_t GetTypeIndex() + { + return Internal::ExtractTypeArgs::GetTypeIndex(); + } + + const Array::ContainerType& Array::GetValues() const + { + return m_values; + } + + const Object::ContainerType& Object::GetValues() const + { + return m_values; + } + + Node::Node(AZ::Name name) + : m_name(AZStd::move(name)) + { + } + + AZ::Name Node::GetName() const + { + return m_name; + } + + void Node::SetName(AZ::Name name) + { + m_name = AZStd::move(name); + } + + Object::ContainerType& Node::GetProperties() + { + return m_properties; + } + + const Object::ContainerType& Node::GetProperties() const + { + return m_properties; + } + + Array::ContainerType& Node::GetChildren() + { + return m_children; + } + + const Array::ContainerType& Node::GetChildren() const + { + return m_children; + } + + Value::Value(SharedStringType sharedString) + : m_value(AZStd::move(sharedString)) + { + } + + Value::Value(const Value& value) + : m_value(value.m_value) + { + } + + Value::Value(Value&& value) noexcept + { + memcpy(this, &value, sizeof(Value)); + memset(&value, 0, sizeof(Value)); + } + + Value::Value(AZStd::string_view stringView, bool copy) + { + if (copy) + { + CopyFromString(stringView); + } + else + { + SetString(stringView); + } + } + + Value::Value(AZStd::any opaqueValue) + : m_value(AZStd::allocate_shared(StdValueAllocator(), AZStd::move(opaqueValue))) + { + } + + Value Value::FromOpaqueValue(const AZStd::any& value) + { + return Value(value); + } + + Value::Value(int8_t value) + : m_value(aznumeric_cast(value)) + { + } + + Value::Value(uint8_t value) + : m_value(aznumeric_cast(value)) + { + } + + Value::Value(int16_t value) + : m_value(aznumeric_cast(value)) + { + } + + Value::Value(uint16_t value) + : m_value(aznumeric_cast(value)) + { + } + + Value::Value(int32_t value) + : m_value(aznumeric_cast(value)) + { + } + + Value::Value(uint32_t value) + : m_value(aznumeric_cast(value)) + { + } + + Value::Value(int64_t value) + : m_value(value) + { + } + + Value::Value(uint64_t value) + : m_value(value) + { + } + + Value::Value(float value) + : m_value(aznumeric_cast(value)) + { + } + + Value::Value(double value) + : m_value(value) + { + } + + Value::Value(bool value) + : m_value(value) + { + } + + Value::Value(Type type) + { + switch (type) + { + case Type::Null: + // Null is the default initialized value + break; + case Type::Bool: + m_value = false; + break; + case Type::Object: + SetObject(); + break; + case Type::Array: + SetArray(); + break; + case Type::String: + SetString(""); + break; + case Type::Int64: + m_value = int64_t{}; + break; + case Type::Uint64: + m_value = uint64_t{}; + break; + case Type::Double: + m_value = double{}; + break; + case Type::Node: + SetNode(""); + break; + case Type::Opaque: + AZ_Assert(false, "AZ::Dom::Value may not be constructed with an empty opaque type"); + break; + } + } + + Value& Value::operator=(const Value& other) + { + m_value = other.m_value; + return *this; + } + + Value& Value::operator=(Value&& other) noexcept + { + SetNull(); + memcpy(this, &other, sizeof(Value)); + memset(&other, 0, sizeof(Value)); + return *this; + } + + bool Value::operator==(const Value& rhs) const + { + if (IsString() && rhs.IsString()) + { + return GetString() == rhs.GetString(); + } + else + { + return m_value == rhs.m_value; + } + } + + bool Value::operator!=(const Value& rhs) const + { + return !operator==(rhs); + } + + void Value::Swap(Value& other) noexcept + { + AZStd::aligned_storage_for_t temp; + memcpy(&temp, this, sizeof(Value)); + memcpy(this, &other, sizeof(Value)); + memcpy(&other, &temp, sizeof(Value)); + } + + Type Dom::Value::GetType() const + { + return AZStd::visit( + [](auto&& value) -> Type + { + using CurrentType = AZStd::decay_t; + if constexpr (AZStd::is_same_v) + { + return Type::Null; + } + else if constexpr (AZStd::is_same_v) + { + return Type::Int64; + } + else if constexpr (AZStd::is_same_v) + { + return Type::Uint64; + } + else if constexpr (AZStd::is_same_v) + { + return Type::Double; + } + else if constexpr (AZStd::is_same_v) + { + return Type::Bool; + } + else if constexpr (AZStd::is_same_v) + { + return Type::String; + } + else if constexpr (AZStd::is_same_v) + { + return Type::String; + } + else if constexpr (AZStd::is_same_v) + { + return Type::String; + } + else if constexpr (AZStd::is_same_v) + { + return Type::Object; + } + else if constexpr (AZStd::is_same_v) + { + return Type::Array; + } + else if constexpr (AZStd::is_same_v) + { + return Type::Node; + } + else if constexpr (AZStd::is_same_v) + { + return Type::Opaque; + } + else + { + AZ_Assert(false, "AZ::Dom::Value::GetType: m_value has an unexpected type"); + } + }, + m_value); + } + + bool Value::IsNull() const + { + return AZStd::holds_alternative(m_value); + } + + bool Value::IsFalse() const + { + const bool* value = AZStd::get_if(&m_value); + return value != nullptr ? !(*value) : false; + } + + bool Value::IsTrue() const + { + const bool* value = AZStd::get_if(&m_value); + return value != nullptr ? *value : false; + } + + bool Value::IsBool() const + { + return AZStd::holds_alternative(m_value); + } + + bool Value::IsNode() const + { + return AZStd::holds_alternative(m_value); + } + + bool Value::IsObject() const + { + return AZStd::holds_alternative(m_value); + } + + bool Value::IsArray() const + { + return AZStd::holds_alternative(m_value); + } + + bool Value::IsOpaqueValue() const + { + return AZStd::holds_alternative(m_value); + } + + bool Value::IsNumber() const + { + return AZStd::visit( + [](auto&& value) -> bool + { + using CurrentType = AZStd::decay_t; + if constexpr (AZStd::is_same_v) + { + return true; + } + else if constexpr (AZStd::is_same_v) + { + return true; + } + else if constexpr (AZStd::is_same_v) + { + return true; + } + else + { + return false; + } + }, + m_value); + } + + bool Value::IsInt() const + { + return AZStd::holds_alternative(m_value); + } + + bool Value::IsUint() const + { + return AZStd::holds_alternative(m_value); + } + + bool Value::IsDouble() const + { + return AZStd::holds_alternative(m_value); + } + + bool Value::IsString() const + { + return AZStd::visit( + [](auto&& value) -> bool + { + using CurrentType = AZStd::decay_t; + if constexpr (AZStd::is_same_v) + { + return true; + } + else if constexpr (AZStd::is_same_v) + { + return true; + } + else if constexpr (AZStd::is_same_v) + { + return true; + } + else + { + return false; + } + }, + m_value); + } + + Value& Value::SetObject() + { + m_value = AZStd::allocate_shared(StdValueAllocator()); + return *this; + } + + const Node& Value::GetNodeInternal() const + { + AZ_Assert(GetType() == Type::Node, "AZ::Dom::Value: attempted to retrieve a node from a non-node value"); + return *AZStd::get(m_value); + } + + Node& Value::GetNodeInternal() + { + AZ_Assert(GetType() == Type::Node, "AZ::Dom::Value: attempted to retrieve a node from a non-node value"); + return *Internal::CheckCopyOnWrite(AZStd::get(m_value)); + } + + const Object::ContainerType& Value::GetObjectInternal() const + { + const Type type = GetType(); + AZ_Assert( + type == Type::Object || type == Type::Node, + "AZ::Dom::Value: attempted to retrieve an object from a value that isn't an object or a node"); + if (type == Type::Object) + { + return AZStd::get(m_value)->m_values; + } + else + { + return AZStd::get(m_value)->GetProperties(); + } + } + + Object::ContainerType& Value::GetObjectInternal() + { + const Type type = GetType(); + AZ_Assert( + type == Type::Object || type == Type::Node, + "AZ::Dom::Value: attempted to retrieve an object from a value that isn't an object or a node"); + if (type == Type::Object) + { + return Internal::CheckCopyOnWrite(AZStd::get(m_value))->m_values; + } + else + { + return Internal::CheckCopyOnWrite(AZStd::get(m_value))->GetProperties(); + } + } + + const Array::ContainerType& Value::GetArrayInternal() const + { + const Type type = GetType(); + AZ_Assert( + type == Type::Array || type == Type::Node, + "AZ::Dom::Value: attempted to retrieve an array from a value that isn't an array or a node"); + if (type == Type::Array) + { + return AZStd::get(m_value)->m_values; + } + else + { + return AZStd::get(m_value)->GetChildren(); + } + } + + Array::ContainerType& Value::GetArrayInternal() + { + const Type type = GetType(); + AZ_Assert( + type == Type::Array || type == Type::Node, + "AZ::Dom::Value: attempted to retrieve an array from a value that isn't an array or node"); + if (type == Type::Array) + { + return Internal::CheckCopyOnWrite(AZStd::get(m_value))->m_values; + } + else + { + return Internal::CheckCopyOnWrite(AZStd::get(m_value))->GetChildren(); + } + } + + size_t Value::MemberCount() const + { + return GetObjectInternal().size(); + } + + size_t Value::MemberCapacity() const + { + return GetObjectInternal().size(); + } + + bool Value::ObjectEmpty() const + { + return GetObjectInternal().empty(); + } + + Value& Value::operator[](KeyType name) + { + Object::ContainerType& object = GetObjectInternal(); + auto existingEntry = AZStd::find_if( + object.begin(), object.end(), + [&name](const Object::EntryType& entry) + { + return entry.first == name; + }); + if (existingEntry != object.end()) + { + return existingEntry->second; + } + else + { + object.emplace_back(name, Value()); + return object[object.size() - 1].second; + } + } + + const Value& Value::operator[](KeyType name) const + { + return FindMember(name)->second; + } + + Value& Value::operator[](AZStd::string_view name) + { + return operator[](AZ::Name(name)); + } + + const Value& Value::operator[](AZStd::string_view name) const + { + return operator[](AZ::Name(name)); + } + + Object::ConstIterator Value::MemberBegin() const + { + return GetObjectInternal().begin(); + } + + Object::ConstIterator Value::MemberEnd() const + { + return GetObjectInternal().end(); + } + + Object::Iterator Value::MemberBegin() + { + return GetObjectInternal().begin(); + } + + Object::Iterator Value::MemberEnd() + { + return GetObjectInternal().end(); + } + + Object::ConstIterator Value::FindMember(KeyType name) const + { + const Object::ContainerType& object = GetObjectInternal(); + return AZStd::find_if( + object.begin(), object.end(), + [&name](const Object::EntryType& entry) + { + return entry.first == name; + }); + } + + Object::ConstIterator Value::FindMember(AZStd::string_view name) const + { + return FindMember(AZ::Name(name)); + } + + Object::Iterator Value::FindMutableMember(KeyType name) + { + Object::ContainerType& object = GetObjectInternal(); + return AZStd::find_if( + object.begin(), object.end(), + [&name](const Object::EntryType& entry) + { + return entry.first == name; + }); + } + + Object::Iterator Value::FindMutableMember(AZStd::string_view name) + { + return FindMutableMember(AZ::Name(name)); + } + + Value& Value::MemberReserve(size_t newCapacity) + { + GetObjectInternal().reserve(newCapacity); + return *this; + } + + bool Value::HasMember(KeyType name) const + { + return FindMember(name) != GetObjectInternal().end(); + } + + bool Value::HasMember(AZStd::string_view name) const + { + return HasMember(AZ::Name(name)); + } + + Value& Value::AddMember(KeyType name, const Value& value) + { + Object::ContainerType& object = GetObjectInternal(); + // Reserve in ReserveIncremenet chunks instead of the default vector doubling strategy + // Profiling has found that this is an aggregate performance gain for typical workflows + object.reserve(AZ_SIZE_ALIGN_UP(object.size() + 1, Object::ReserveIncrement)); + if (auto memberIt = FindMutableMember(name); memberIt != object.end()) + { + memberIt->second = value; + } + else + { + object.emplace_back(AZStd::move(name), value); + } + return *this; + } + + Value& Value::AddMember(AZStd::string_view name, const Value& value) + { + return AddMember(AZ::Name(name), value); + } + + Value& Value::AddMember(AZ::Name name, Value&& value) + { + Object::ContainerType& object = GetObjectInternal(); + if (auto memberIt = FindMutableMember(name); memberIt != object.end()) + { + memberIt->second = value; + } + else + { + object.emplace_back(AZStd::move(name), value); + } + return *this; + } + + Value& Value::AddMember(AZStd::string_view name, Value&& value) + { + return AddMember(AZ::Name(name), value); + } + + void Value::RemoveAllMembers() + { + GetObjectInternal().clear(); + } + + void Value::RemoveMember(KeyType name) + { + Object::ContainerType& object = GetObjectInternal(); + object.erase(AZStd::remove_if( + object.begin(), object.end(), + [&name](const Object::EntryType& entry) + { + return entry.first == name; + })); + } + + void Value::RemoveMember(AZStd::string_view name) + { + RemoveMember(AZ::Name(name)); + } + + Object::Iterator Value::RemoveMember(Object::Iterator pos) + { + Object::ContainerType& object = GetObjectInternal(); + if (!object.empty()) + { + *pos = AZStd::move(object.back()); + object.pop_back(); + } + return object.end(); + } + + Object::Iterator Value::EraseMember(Object::ConstIterator pos) + { + return GetObjectInternal().erase(pos); + } + + Object::Iterator Value::EraseMember(Object::ConstIterator first, Object::ConstIterator last) + { + return GetObjectInternal().erase(first, last); + } + + Object::Iterator Value::EraseMember(KeyType name) + { + return GetObjectInternal().erase(FindMember(name)); + } + + Object::Iterator Value::EraseMember(AZStd::string_view name) + { + return EraseMember(AZ::Name(name)); + } + + Object::ContainerType& Value::GetMutableObject() + { + return GetObjectInternal(); + } + + const Object::ContainerType& Value::GetObject() const + { + return GetObjectInternal(); + } + + Value& Value::SetArray() + { + m_value = AZStd::allocate_shared(StdValueAllocator()); + return *this; + } + + size_t Value::ArraySize() const + { + return GetArrayInternal().size(); + } + + size_t Value::ArrayCapacity() const + { + return GetArrayInternal().capacity(); + } + + bool Value::IsArrayEmpty() const + { + return GetArrayInternal().empty(); + } + + void Value::ClearArray() + { + GetArrayInternal().clear(); + } + + Value& Value::operator[](size_t index) + { + return GetArrayInternal()[index]; + } + + const Value& Value::operator[](size_t index) const + { + return GetArrayInternal()[index]; + } + + Value& Value::MutableArrayAt(size_t index) + { + return operator[](index); + } + + const Value& Value::ArrayAt(size_t index) const + { + return operator[](index); + } + + Array::ConstIterator Value::ArrayBegin() const + { + return GetArrayInternal().begin(); + } + + Array::ConstIterator Value::ArrayEnd() const + { + return GetArrayInternal().end(); + } + + Array::Iterator Value::ArrayBegin() + { + return GetArrayInternal().begin(); + } + + Array::Iterator Value::ArrayEnd() + { + return GetArrayInternal().end(); + } + + Value& Value::ArrayReserve(size_t newCapacity) + { + GetArrayInternal().reserve(newCapacity); + return *this; + } + + Value& Value::ArrayPushBack(Value value) + { + Array::ContainerType& array = GetArrayInternal(); + // Reserve in ReserveIncremenet chunks instead of the default vector doubling strategy + // Profiling has found that this is an aggregate performance gain for typical workflows + array.reserve(AZ_SIZE_ALIGN_UP(array.size() + 1, Array::ReserveIncrement)); + array.push_back(AZStd::move(value)); + return *this; + } + + Value& Value::ArrayPopBack() + { + GetArrayInternal().pop_back(); + return *this; + } + + Array::Iterator Value::ArrayErase(Array::ConstIterator pos) + { + return GetArrayInternal().erase(pos); + } + + Array::Iterator Value::ArrayErase(Array::ConstIterator first, Array::ConstIterator last) + { + return GetArrayInternal().erase(first, last); + } + + Array::ContainerType& Value::GetMutableArray() + { + return GetArrayInternal(); + } + + const Array::ContainerType& Value::GetArray() const + { + return GetArrayInternal(); + } + + void Value::SetNode(AZ::Name name) + { + m_value = AZStd::allocate_shared(StdValueAllocator(), AZStd::move(name)); + } + + void Value::SetNode(AZStd::string_view name) + { + SetNode(AZ::Name(name)); + } + + AZ::Name Value::GetNodeName() const + { + return GetNodeInternal().GetName(); + } + + void Value::SetNodeName(AZ::Name name) + { + GetNodeInternal().SetName(AZStd::move(name)); + } + + void Value::SetNodeName(AZStd::string_view name) + { + SetNodeName(AZ::Name(name)); + } + + void Value::SetNodeValue(Value value) + { + AZ_Assert(GetType() == Type::Node, "AZ::Dom::Value: Attempted to set value for non-node type"); + Array::ContainerType& nodeChildren = GetArrayInternal(); + + // Set the first non-node child, if one is found + for (Value& entry : nodeChildren) + { + if (entry.GetType() != Type::Node) + { + entry = AZStd::move(value); + return; + } + } + + // Otherwise, append the value entry + nodeChildren.push_back(AZStd::move(value)); + } + + Value Value::GetNodeValue() const + { + AZ_Assert(GetType() == Type::Node, "AZ::Dom::Value: Attempted to get value for non-node type"); + const Array::ContainerType& nodeChildren = GetArrayInternal(); + + // Get the first non-node child, if one is found + for (const Value& entry : nodeChildren) + { + if (entry.GetType() != Type::Node) + { + return entry; + } + } + + return Value(); + } + + Node& Value::GetMutableNode() + { + return GetNodeInternal(); + } + + const Node& Value::GetNode() const + { + return GetNodeInternal(); + } + + int64_t Value::GetInt64() const + { + switch (m_value.index()) + { + case GetTypeIndex(): + return AZStd::get(m_value); + case GetTypeIndex(): + return aznumeric_cast(AZStd::get(m_value)); + case GetTypeIndex(): + return aznumeric_cast(AZStd::get(m_value)); + } + AZ_Assert(false, "AZ::Dom::Value: Called GetInt on a non-numeric type"); + return {}; + } + + void Value::SetInt64(int64_t value) + { + m_value = value; + } + + uint64_t Value::GetUint64() const + { + switch (m_value.index()) + { + case GetTypeIndex(): + return aznumeric_cast(AZStd::get(m_value)); + case GetTypeIndex(): + return AZStd::get(m_value); + case GetTypeIndex(): + return aznumeric_cast(AZStd::get(m_value)); + } + AZ_Assert(false, "AZ::Dom::Value: Called GetInt on a non-numeric type"); + return {}; + } + + void Value::SetUint64(uint64_t value) + { + m_value = value; + } + + bool Value::GetBool() const + { + if (IsBool()) + { + return AZStd::get(m_value); + } + AZ_Assert(false, "AZ::Dom::Value: Called GetBool on a non-bool type"); + return {}; + } + + void Value::SetBool(bool value) + { + m_value = value; + } + + double Value::GetDouble() const + { + switch (m_value.index()) + { + case GetTypeIndex(): + return aznumeric_cast(AZStd::get(m_value)); + case GetTypeIndex(): + return aznumeric_cast(AZStd::get(m_value)); + case GetTypeIndex(): + return AZStd::get(m_value); + } + AZ_Assert(false, "AZ::Dom::Value: Called GetInt on a non-numeric type"); + return {}; + } + + void Value::SetDouble(double value) + { + m_value = value; + } + + void Value::SetString(SharedStringType sharedString) + { + m_value = sharedString; + } + + AZStd::string_view Value::GetString() const + { + switch (m_value.index()) + { + case GetTypeIndex(): + return AZStd::get(m_value); + case GetTypeIndex(): + { + auto& buffer = *AZStd::get(m_value); + return { buffer.data(), buffer.size() }; + } + case GetTypeIndex(): + { + const ShortStringType& shortString = AZStd::get(m_value); + return { shortString.data(), shortString.size() }; + } + } + AZ_Assert(false, "AZ::Dom::Value: Called GetString on a non-string type"); + return {}; + } + + size_t Value::GetStringLength() const + { + return GetString().size(); + } + + void Value::SetString(AZStd::string_view value) + { + m_value = value; + } + + void Value::CopyFromString(AZStd::string_view value) + { + if (value.size() <= ShortStringSize) + { + ShortStringType buffer; + buffer.resize_no_construct(value.size()); + memcpy(buffer.data(), value.data(), value.size()); + m_value = buffer; + } + else + { + SharedStringType sharedString = AZStd::allocate_shared(StdValueAllocator(), value.begin(), value.end()); + m_value = AZStd::move(sharedString); + } + } + + const AZStd::any& Value::GetOpaqueValue() const + { + return *AZStd::get(m_value); + } + + void Value::SetOpaqueValue(AZStd::any value) + { + m_value = AZStd::allocate_shared(StdValueAllocator(), AZStd::move(value)); + } + + void Value::SetNull() + { + m_value = AZStd::monostate(); + } + + Visitor::Result Value::Accept(Visitor& visitor, bool copyStrings) const + { + Visitor::Result result = AZ::Success(); + + AZStd::visit( + [&](auto&& arg) + { + using Alternative = AZStd::decay_t; + + if constexpr (AZStd::is_same_v) + { + result = visitor.Null(); + } + else if constexpr (AZStd::is_same_v) + { + result = visitor.Int64(arg); + } + else if constexpr (AZStd::is_same_v) + { + result = visitor.Uint64(arg); + } + else if constexpr (AZStd::is_same_v) + { + result = visitor.Double(arg); + } + else if constexpr (AZStd::is_same_v) + { + result = visitor.Bool(arg); + } + else if constexpr (AZStd::is_same_v) + { + result = visitor.String(arg, copyStrings ? Lifetime::Temporary : Lifetime::Persistent); + } + else if constexpr (AZStd::is_same_v) + { + result = visitor.RefCountedString(arg, copyStrings ? Lifetime::Temporary : Lifetime::Persistent); + } + else if constexpr (AZStd::is_same_v) + { + result = visitor.StartObject(); + if (result.IsSuccess()) + { + const Object::ContainerType& object = GetObjectInternal(); + for (const Object::EntryType& entry : object) + { + result = visitor.Key(entry.first); + if (!result.IsSuccess()) + { + return; + } + result = entry.second.Accept(visitor, copyStrings); + if (!result.IsSuccess()) + { + return; + } + } + result = visitor.EndObject(object.size()); + } + } + else if constexpr (AZStd::is_same_v) + { + result = visitor.StartArray(); + if (result.IsSuccess()) + { + const Array::ContainerType& arrayContainer = GetArrayInternal(); + for (const Value& entry : arrayContainer) + { + result = entry.Accept(visitor, copyStrings); + if (!result.IsSuccess()) + { + return; + } + } + result = visitor.EndArray(arrayContainer.size()); + } + } + else if constexpr (AZStd::is_same_v) + { + const Node& node = *AZStd::get(m_value); + result = visitor.StartNode(node.GetName()); + if (result.IsSuccess()) + { + const Object::ContainerType& object = GetObjectInternal(); + for (const Object::EntryType& entry : object) + { + result = visitor.Key(entry.first); + if (!result.IsSuccess()) + { + return; + } + result = entry.second.Accept(visitor, copyStrings); + if (!result.IsSuccess()) + { + return; + } + } + + const Array::ContainerType& arrayContainer = GetArrayInternal(); + for (const Value& entry : arrayContainer) + { + result = entry.Accept(visitor, copyStrings); + if (!result.IsSuccess()) + { + return; + } + } + + result = visitor.EndNode(object.size(), arrayContainer.size()); + } + } + else if constexpr (AZStd::is_same_v) + { + result = visitor.OpaqueValue(*arg); + } + }, + m_value); + return result; + } + + AZStd::unique_ptr Value::GetWriteHandler() + { + return AZStd::make_unique(*this); + } + + const Value::ValueType& Value::GetInternalValue() const + { + return m_value; + } +} // namespace AZ::Dom diff --git a/Code/Framework/AzCore/AzCore/DOM/DomValue.h b/Code/Framework/AzCore/AzCore/DOM/DomValue.h new file mode 100644 index 0000000000..ecf8326525 --- /dev/null +++ b/Code/Framework/AzCore/AzCore/DOM/DomValue.h @@ -0,0 +1,402 @@ +/* + * Copyright (c) Contributors to the Open 3D Engine Project. + * For complete copyright and license terms please see the LICENSE at the root of this distribution. + * + * SPDX-License-Identifier: Apache-2.0 OR MIT + * + */ + +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace AZ::Dom +{ + using KeyType = AZ::Name; + + //! The type of underlying value stored in a value. \see Value + enum class Type + { + Null, + Bool, + Object, + Array, + String, + Int64, + Uint64, + Double, + Node, + Opaque, + }; + + //! The allocator used by Value. + //! Value heap allocates shared_ptrs for its container storage (Array / Object / Node) alongside + class ValueAllocator final : public SimpleSchemaAllocator + { + public: + AZ_TYPE_INFO(ValueAllocator, "{5BC8B389-72C7-459E-B502-12E74D61869F}"); + + using Base = SimpleSchemaAllocator; + + ValueAllocator() + : Base("DomValueAllocator", "Allocator for AZ::Dom::Value") + { + DisableOverriding(); + } + }; + + using StdValueAllocator = AZStdAlloc; + + class Value; + + //! Internal storage for a Value array: an ordered list of Values. + class Array + { + public: + using ContainerType = AZStd::vector; + using Iterator = ContainerType::iterator; + using ConstIterator = ContainerType::const_iterator; + static constexpr const size_t ReserveIncrement = 4; + static_assert((ReserveIncrement & (ReserveIncrement - 1)) == 0, "ReserveIncremenet must be a power of 2"); + + const ContainerType& GetValues() const; + + private: + ContainerType m_values; + + friend class Value; + }; + + using ArrayPtr = AZStd::shared_ptr; + using ConstArrayPtr = AZStd::shared_ptr; + + //! Internal storage for a Value object: an ordered list of Name / Value pairs. + class Object + { + public: + using EntryType = AZStd::pair; + using ContainerType = AZStd::vector; + using Iterator = ContainerType::iterator; + using ConstIterator = ContainerType::const_iterator; + static constexpr const size_t ReserveIncrement = 8; + static_assert((ReserveIncrement & (ReserveIncrement - 1)) == 0, "ReserveIncremenet must be a power of 2"); + + const ContainerType& GetValues() const; + + private: + ContainerType m_values; + + friend class Value; + }; + + using ObjectPtr = AZStd::shared_ptr; + using ConstObjectPtr = AZStd::shared_ptr; + + //! Storage for a Value node: a named Value with both properties and children. + //! Properties are stored as an ordered list of Name / Value pairs. + //! Children are stored as an oredered list of Values. + class Node + { + public: + Node() = default; + Node(const Node&) = default; + Node(Node&&) = default; + explicit Node(AZ::Name name); + + Node& operator=(const Node&) = default; + Node& operator=(Node&&) = default; + + AZ::Name GetName() const; + void SetName(AZ::Name name); + + Object::ContainerType& GetProperties(); + const Object::ContainerType& GetProperties() const; + + Array::ContainerType& GetChildren(); + const Array::ContainerType& GetChildren() const; + + private: + AZ::Name m_name; + Object::ContainerType m_properties; + Array::ContainerType m_children; + + friend class Value; + }; + + using NodePtr = AZStd::shared_ptr; + using ConstNodePtr = AZStd::shared_ptr; + + //! Value is a typed union of Dom types that can represent the types provdied by AZ::Dom::Visitor. + //! Value can be one of the following types: + //! - Null: a type with no value, this is the default type for Value + //! - Bool: a true or false boolean value + //! - Object: a container with an ordered list of Name/Value pairs, analagous to a JSON object + //! - Array: a container with an ordered list of Values, analagous to a JSON array + //! - String: a UTF-8 string + //! - Int64: a signed, 64-bit integer + //! - Uint64: an unsigned, 64-bit integer + //! - Double: a double precision floating point value + //! - Node: a container with a Name, an ordered list of Name/Values pairs (attributes), and an ordered list of Values (children), + //! analagous to an XML node + //! - Opaque: an arbitrary value stored in an AZStd::any. This is a non-serializable representation of an entry used only for in-memory + //! options. This is intended to be used as an intermediate value over the course of DOM transformation and as a proxy to pass through + //! types of which the DOM has no knowledge to other systems. + //! \note Value is a copy-on-write data structure and may be cheaply returned by value. Heap allocated data larger than the size of the + //! value itself (objects, arrays, and nodes) are copied by new Values only when their contents change, so care should be taken in + //! performance critical code to avoid mutation operations such as operator[] to avoid copies. It is recommended that an immutable Value + //! be explicitly be stored as a `const Value` to avoid accidental detach and copy operations. + class Value final + { + public: + // Determine the short string buffer size based on the size of our largest internal type (string_view) + // minus the size of the short string size field. + static constexpr const size_t ShortStringSize = sizeof(AZStd::string_view) - 2; + using ShortStringType = AZStd::fixed_string; + using SharedStringContainer = AZStd::vector; + using SharedStringType = AZStd::shared_ptr; + using OpaqueStorageType = AZStd::shared_ptr; + + //! The internal storage type for Value. + //! These types do not correspond one-to-one with the Value's external Type as there may be multiple storage classes + //! for the same type in some instances, such as string storage. + using ValueType = AZStd::variant< + // Null + AZStd::monostate, + // Int64 + int64_t, + // Uint64 + uint64_t, + // Double + double, + // Bool + bool, + // String + AZStd::string_view, + SharedStringType, + ShortStringType, + // Object + ObjectPtr, + // Array + ArrayPtr, + // Node + NodePtr, + // Opaque + OpaqueStorageType>; + + // Constructors... + Value() = default; + Value(const Value&); + Value(Value&&) noexcept; + Value(AZStd::string_view stringView, bool copy); + explicit Value(const ValueType&); + explicit Value(ValueType&&); + explicit Value(SharedStringType sharedString); + + explicit Value(int8_t value); + explicit Value(uint8_t value); + explicit Value(int16_t value); + explicit Value(uint16_t value); + explicit Value(int32_t value); + explicit Value(uint32_t value); + explicit Value(int64_t value); + explicit Value(uint64_t value); + explicit Value(float value); + explicit Value(double value); + explicit Value(bool value); + + explicit Value(Type type); + + // Disable accidental calls to Value(bool) with pointer types + template + explicit Value(T*) = delete; + + static Value FromOpaqueValue(const AZStd::any& value); + + // Equality / comparison / swap... + Value& operator=(const Value&); + Value& operator=(Value&&) noexcept; + + //! Assignment operator to allow forwarding types constructible via Value(T) to be assigned + template + auto operator=(T&& arg) + -> AZStd::enable_if_t, Value> && AZStd::is_constructible_v, Value&> + { + return operator=(Value(AZStd::forward(arg))); + } + + bool operator==(const Value& rhs) const; + bool operator!=(const Value& rhs) const; + + void Swap(Value& other) noexcept; + + // Type info... + Type GetType() const; + bool IsNull() const; + bool IsFalse() const; + bool IsTrue() const; + bool IsBool() const; + bool IsNode() const; + bool IsObject() const; + bool IsArray() const; + bool IsOpaqueValue() const; + bool IsNumber() const; + bool IsInt() const; + bool IsUint() const; + bool IsDouble() const; + bool IsString() const; + + // Object API (also used by Node)... + Value& SetObject(); + size_t MemberCount() const; + size_t MemberCapacity() const; + bool ObjectEmpty() const; + + Value& operator[](KeyType name); + const Value& operator[](KeyType name) const; + Value& operator[](AZStd::string_view name); + const Value& operator[](AZStd::string_view name) const; + + Object::ConstIterator MemberBegin() const; + Object::ConstIterator MemberEnd() const; + Object::Iterator MemberBegin(); + Object::Iterator MemberEnd(); + + Object::Iterator FindMutableMember(KeyType name); + Object::Iterator FindMutableMember(AZStd::string_view name); + Object::ConstIterator FindMember(KeyType name) const; + Object::ConstIterator FindMember(AZStd::string_view name) const; + + Value& MemberReserve(size_t newCapacity); + bool HasMember(KeyType name) const; + bool HasMember(AZStd::string_view name) const; + + Value& AddMember(KeyType name, const Value& value); + Value& AddMember(AZStd::string_view name, const Value& value); + Value& AddMember(KeyType name, Value&& value); + Value& AddMember(AZStd::string_view name, Value&& value); + + void RemoveAllMembers(); + void RemoveMember(KeyType name); + void RemoveMember(AZStd::string_view name); + Object::Iterator RemoveMember(Object::Iterator pos); + Object::Iterator EraseMember(Object::ConstIterator pos); + Object::Iterator EraseMember(Object::ConstIterator first, Object::ConstIterator last); + Object::Iterator EraseMember(KeyType name); + Object::Iterator EraseMember(AZStd::string_view name); + + Object::ContainerType& GetMutableObject(); + const Object::ContainerType& GetObject() const; + + // Array API (also used by Node)... + Value& SetArray(); + + size_t ArraySize() const; + size_t ArrayCapacity() const; + bool IsArrayEmpty() const; + void ClearArray(); + + Value& operator[](size_t index); + const Value& operator[](size_t index) const; + + Value& MutableArrayAt(size_t index); + const Value& ArrayAt(size_t index) const; + + Array::ConstIterator ArrayBegin() const; + Array::ConstIterator ArrayEnd() const; + Array::Iterator ArrayBegin(); + Array::Iterator ArrayEnd(); + + Value& ArrayReserve(size_t newCapacity); + Value& ArrayPushBack(Value value); + Value& ArrayPopBack(); + + Array::Iterator ArrayErase(Array::ConstIterator pos); + Array::Iterator ArrayErase(Array::ConstIterator first, Array::ConstIterator last); + + Array::ContainerType& GetMutableArray(); + const Array::ContainerType& GetArray() const; + + // Node API (supports both object + array API, plus a dedicated NodeName)... + void SetNode(AZ::Name name); + void SetNode(AZStd::string_view name); + + AZ::Name GetNodeName() const; + void SetNodeName(AZ::Name name); + void SetNodeName(AZStd::string_view name); + //! Convenience method, sets the first non-node element of a Node. + void SetNodeValue(Value value); + //! Convenience method, gets the first non-node element of a Node. + Value GetNodeValue() const; + + Node& GetMutableNode(); + const Node& GetNode() const; + + // int API... + int64_t GetInt64() const; + void SetInt64(int64_t); + + // uint API... + uint64_t GetUint64() const; + void SetUint64(uint64_t); + + // bool API... + bool GetBool() const; + void SetBool(bool); + + // double API... + double GetDouble() const; + void SetDouble(double); + + // String API... + AZStd::string_view GetString() const; + size_t GetStringLength() const; + void SetString(AZStd::string_view); + void SetString(SharedStringType sharedString); + void CopyFromString(AZStd::string_view); + + // Opaque type API... + const AZStd::any& GetOpaqueValue() const; + //! This sets this Value to represent a value of an type that the DOM has + //! no formal knowledge of. Where possible, it should be preferred to + //! serialize an opaque type into a DOM value instead, as serializers + //! and other systems will have no means of dealing with fully arbitrary + //! values. + void SetOpaqueValue(AZStd::any); + + // Null API... + void SetNull(); + + // Visitor API... + Visitor::Result Accept(Visitor& visitor, bool copyStrings) const; + AZStd::unique_ptr GetWriteHandler(); + + //! Gets the internal value of this Value. Note that this value's types may not correspond one-to-one with the Type enumeration, + //! as internally the same type might have different storage mechanisms. Where possible, prefer using the typed API. + const ValueType& GetInternalValue() const; + + private: + const Node& GetNodeInternal() const; + Node& GetNodeInternal(); + const Object::ContainerType& GetObjectInternal() const; + Object::ContainerType& GetObjectInternal(); + const Array::ContainerType& GetArrayInternal() const; + Array::ContainerType& GetArrayInternal(); + + explicit Value(AZStd::any opaqueValue); + + static_assert( + sizeof(ValueType) == sizeof(ShortStringType) + sizeof(size_t), "ValueType should have no members larger than ShortStringType"); + + ValueType m_value; + }; +} // namespace AZ::Dom diff --git a/Code/Framework/AzCore/AzCore/DOM/DomValueWriter.cpp b/Code/Framework/AzCore/AzCore/DOM/DomValueWriter.cpp new file mode 100644 index 0000000000..433e650d04 --- /dev/null +++ b/Code/Framework/AzCore/AzCore/DOM/DomValueWriter.cpp @@ -0,0 +1,262 @@ +/* + * Copyright (c) Contributors to the Open 3D Engine Project. + * For complete copyright and license terms please see the LICENSE at the root of this distribution. + * + * SPDX-License-Identifier: Apache-2.0 OR MIT + * + */ + +#include + +namespace AZ::Dom +{ + ValueWriter::ValueWriter(Value& outputValue) + : m_result(outputValue) + { + } + + VisitorFlags ValueWriter::GetVisitorFlags() const + { + return VisitorFlags::SupportsRawKeys | VisitorFlags::SupportsArrays | VisitorFlags::SupportsObjects | VisitorFlags::SupportsNodes; + } + + ValueWriter::ValueInfo::ValueInfo(Value& container) + : m_container(container) + { + } + + Visitor::Result ValueWriter::Null() + { + CurrentValue().SetNull(); + return FinishWrite(); + } + + Visitor::Result ValueWriter::Bool(bool value) + { + CurrentValue().SetBool(value); + return FinishWrite(); + } + + Visitor::Result ValueWriter::Int64(AZ::s64 value) + { + CurrentValue().SetInt64(value); + return FinishWrite(); + } + + Visitor::Result ValueWriter::Uint64(AZ::u64 value) + { + CurrentValue().SetUint64(value); + return FinishWrite(); + } + + Visitor::Result ValueWriter::Double(double value) + { + CurrentValue().SetDouble(value); + return FinishWrite(); + } + + Visitor::Result ValueWriter::String(AZStd::string_view value, Lifetime lifetime) + { + if (lifetime == Lifetime::Persistent) + { + CurrentValue().SetString(value); + } + else + { + CurrentValue().CopyFromString(value); + } + return FinishWrite(); + } + + Visitor::Result ValueWriter::RefCountedString(AZStd::shared_ptr> value, [[maybe_unused]] Lifetime lifetime) + { + CurrentValue().SetString(AZStd::move(value)); + return FinishWrite(); + } + + Visitor::Result ValueWriter::StartObject() + { + CurrentValue().SetObject(); + + m_entryStack.emplace(CurrentValue()); + return VisitorSuccess(); + } + + template + void MoveVectorMemory(AZStd::vector& dest, AZStd::vector& source) + { + dest.resize_no_construct(source.size()); + const size_t size = sizeof(T) * source.size(); + memcpy(dest.data(), source.data(), size); + memset(source.data(), 0, size); + source.resize(0); + } + + Visitor::Result ValueWriter::EndContainer(Type containerType, AZ::u64 attributeCount, AZ::u64 elementCount) + { + const char* endMethodName; + switch (containerType) + { + case Type::Object: + endMethodName = "EndObject"; + break; + case Type::Array: + endMethodName = "EndArray"; + break; + case Type::Node: + endMethodName = "EndNode"; + break; + default: + AZ_Assert(false, "Invalid container type specified"); + return VisitorFailure(VisitorErrorCode::InternalError, "AZ::Dom::ValueWriter: EndContainer called with invalid container type"); + } + + if (m_entryStack.empty()) + { + return VisitorFailure( + VisitorErrorCode::InternalError, + AZStd::string::format("AZ::Dom::ValueWriter: %s called without a matching call", endMethodName)); + } + + const ValueInfo& topEntry = m_entryStack.top(); + Value& container = topEntry.m_container; + ValueBuffer& buffer = GetValueBuffer(); + + if (container.GetType() != containerType) + { + return VisitorFailure( + VisitorErrorCode::InternalError, + AZStd::string::format("AZ::Dom::ValueWriter: %s called from within a different container type", endMethodName)); + } + + if (aznumeric_cast(buffer.m_attributes.size()) != attributeCount) + { + return VisitorFailure( + VisitorErrorCode::InternalError, + AZStd::string::format( + "AZ::Dom::ValueWriter: %s expected %llu attributes but received %zu attributes instead", endMethodName, attributeCount, + buffer.m_attributes.size())); + } + + if (aznumeric_cast(buffer.m_elements.size()) != elementCount) + { + return VisitorFailure( + VisitorErrorCode::InternalError, + AZStd::string::format( + "AZ::Dom::ValueWriter: %s expected %llu elements but received %zu elements instead", endMethodName, elementCount, + buffer.m_elements.size())); + } + + if (buffer.m_attributes.size() > 0) + { + MoveVectorMemory(container.GetMutableObject(), buffer.m_attributes); + } + + if(buffer.m_elements.size() > 0) + { + MoveVectorMemory(container.GetMutableArray(), buffer.m_elements); + } + + m_entryStack.pop(); + return FinishWrite(); + } + + ValueWriter::ValueBuffer& ValueWriter::GetValueBuffer() + { + if (m_entryStack.size() <= m_valueBuffers.size()) + { + return m_valueBuffers[m_entryStack.size() - 1]; + } + + m_valueBuffers.resize(m_entryStack.size()); + return m_valueBuffers[m_entryStack.size() - 1]; + } + + Visitor::Result ValueWriter::EndObject(AZ::u64 attributeCount) + { + return EndContainer(Type::Object, attributeCount, 0); + } + + Visitor::Result ValueWriter::Key(AZ::Name key) + { + AZ_Assert(!m_entryStack.empty(), "Attempmted to push a key with no object"); + AZ_Assert(!m_entryStack.top().m_container.IsArray(), "Attempted to push a key to an array"); + m_entryStack.top().m_key = AZStd::move(key); + return VisitorSuccess(); + } + + Visitor::Result ValueWriter::RawKey(AZStd::string_view key, [[maybe_unused]] Lifetime lifetime) + { + return Key(AZ::Name(key)); + } + + Visitor::Result ValueWriter::StartArray() + { + CurrentValue().SetArray(); + + m_entryStack.emplace(CurrentValue()); + return VisitorSuccess(); + } + + Visitor::Result ValueWriter::EndArray(AZ::u64 elementCount) + { + return EndContainer(Type::Array, 0, elementCount); + } + + Visitor::Result ValueWriter::StartNode(AZ::Name name) + { + CurrentValue().SetNode(name); + + m_entryStack.emplace(CurrentValue()); + return VisitorSuccess(); + } + + Visitor::Result ValueWriter::RawStartNode(AZStd::string_view name, [[maybe_unused]] Lifetime lifetime) + { + return StartNode(AZ::Name(name)); + } + + Visitor::Result ValueWriter::EndNode(AZ::u64 attributeCount, AZ::u64 elementCount) + { + return EndContainer(Type::Node, attributeCount, elementCount); + } + + Visitor::Result ValueWriter::OpaqueValue(OpaqueType& value) + { + CurrentValue().SetOpaqueValue(value); + return FinishWrite(); + } + + Visitor::Result ValueWriter::FinishWrite() + { + if (m_entryStack.empty()) + { + return VisitorSuccess(); + } + + Value value; + m_entryStack.top().m_value.Swap(value); + ValueInfo& newEntry = m_entryStack.top(); + + if (!newEntry.m_key.IsEmpty()) + { + GetValueBuffer().m_attributes.emplace_back(AZStd::move(newEntry.m_key), AZStd::move(value)); + newEntry.m_key = AZ::Name(); + } + else + { + GetValueBuffer().m_elements.emplace_back(AZStd::move(value)); + } + + return VisitorSuccess(); + } + + Value& ValueWriter::CurrentValue() + { + if (m_entryStack.empty()) + { + return m_result; + } + return m_entryStack.top().m_value; + } +} // namespace AZ::Dom diff --git a/Code/Framework/AzCore/AzCore/DOM/DomValueWriter.h b/Code/Framework/AzCore/AzCore/DOM/DomValueWriter.h new file mode 100644 index 0000000000..4fdea293eb --- /dev/null +++ b/Code/Framework/AzCore/AzCore/DOM/DomValueWriter.h @@ -0,0 +1,72 @@ +/* + * Copyright (c) Contributors to the Open 3D Engine Project. + * For complete copyright and license terms please see the LICENSE at the root of this distribution. + * + * SPDX-License-Identifier: Apache-2.0 OR MIT + * + */ + +#pragma once + +#include +#include + +namespace AZ::Dom +{ + //! Visitor that writes to a Value. + //! Supports all Visitor operations. + class ValueWriter : public Visitor + { + public: + ValueWriter(Value& outputValue); + + VisitorFlags GetVisitorFlags() const override; + Result Null() override; + Result Bool(bool value) override; + Result Int64(AZ::s64 value) override; + Result Uint64(AZ::u64 value) override; + Result Double(double value) override; + + Result String(AZStd::string_view value, Lifetime lifetime) override; + Result RefCountedString(AZStd::shared_ptr> value, Lifetime lifetime) override; + Result StartObject() override; + Result EndObject(AZ::u64 attributeCount) override; + Result Key(AZ::Name key) override; + Result RawKey(AZStd::string_view key, Lifetime lifetime) override; + Result StartArray() override; + Result EndArray(AZ::u64 elementCount) override; + Result StartNode(AZ::Name name) override; + Result RawStartNode(AZStd::string_view name, Lifetime lifetime) override; + Result EndNode(AZ::u64 attributeCount, AZ::u64 elementCount) override; + Result OpaqueValue(OpaqueType& value) override; + + private: + Result FinishWrite(); + Value& CurrentValue(); + Visitor::Result EndContainer(Type containerType, AZ::u64 attributeCount, AZ::u64 elementCount); + + struct ValueInfo + { + ValueInfo(Value& container); + + KeyType m_key; + Value m_value; + Value& m_container; + }; + + struct ValueBuffer + { + Array::ContainerType m_elements; + Object::ContainerType m_attributes; + }; + + ValueBuffer& GetValueBuffer(); + + Value& m_result; + // Stores info about the current value being processed + AZStd::stack>> m_entryStack; + // Provides temporary storage for elements and attributes to prevent extra heap allocations + // These buffers persist to be reused even as the entry stack changes + AZStd::vector> m_valueBuffers; + }; +} // namespace AZ::Dom diff --git a/Code/Framework/AzCore/AzCore/DOM/DomVisitor.cpp b/Code/Framework/AzCore/AzCore/DOM/DomVisitor.cpp index ad314da385..0da4bcd5d3 100644 --- a/Code/Framework/AzCore/AzCore/DOM/DomVisitor.cpp +++ b/Code/Framework/AzCore/AzCore/DOM/DomVisitor.cpp @@ -105,7 +105,12 @@ namespace AZ::Dom return VisitorSuccess(); } - Visitor::Result Visitor::OpaqueValue([[maybe_unused]] const OpaqueType& value, [[maybe_unused]] Lifetime lifetime) + Visitor::Result Visitor::RefCountedString(AZStd::shared_ptr> value, Lifetime lifetime) + { + return String({ value->data(), value->size() }, lifetime); + } + + Visitor::Result Visitor::OpaqueValue([[maybe_unused]] OpaqueType& value) { if (!SupportsOpaqueValues()) { diff --git a/Code/Framework/AzCore/AzCore/DOM/DomVisitor.h b/Code/Framework/AzCore/AzCore/DOM/DomVisitor.h index bbe78131c3..f9930f93c6 100644 --- a/Code/Framework/AzCore/AzCore/DOM/DomVisitor.h +++ b/Code/Framework/AzCore/AzCore/DOM/DomVisitor.h @@ -11,6 +11,8 @@ #include #include #include +#include +#include #include namespace AZ::Dom @@ -167,15 +169,20 @@ namespace AZ::Dom virtual Result Uint64(AZ::u64 value); //! Operates on a double precision, 64 bit floating point value. virtual Result Double(double value); - //! Operates on a string value. As strings are a reference type. - //! Storage semantics are provided to indicate where the value may be stored persistently or requires a copy. + //! Operates on a string value. As strings are a reference type, + //! storage semantics are provided to indicate where the value may be stored persistently or requires a copy. + //! \param lifetime Specifies the lifetime of this string - if the string has a temporary lifetime, it cannot + //! safely be stored as a reference. virtual Result String(AZStd::string_view value, Lifetime lifetime); + //! Operates on a ref-counted string value. S + //! \param lifetime Specifies the lifetime of this string. If the string has a temporary lifetime, it may not + //! be safely stored as a reference, but may still be safely stored as a ref-counted shared_ptr. + virtual Result RefCountedString(AZStd::shared_ptr> value, Lifetime lifetime); //! Operates on an opaque value. As opaque values are a reference type, storage semantics are provided to //! indicate where the value may be stored persistently or requires a copy. //! The base implementation of OpaqueValue rejects the operation, as opaque values are meant for special //! cases with specific implementations, not generic usage. - //! Storage semantics are provided to indicate where the value may be stored persistently or requires a copy. - virtual Result OpaqueValue(const OpaqueType& value, Lifetime lifetime); + virtual Result OpaqueValue(OpaqueType& value); //! Operates on a raw value encoded as a UTF-8 string that hasn't had its type deduced. //! Visitors that support raw values (\see VisitorFlags::SupportsRawValues) may parse the raw value and //! forward it to the corresponding value call or calls of their choice. diff --git a/Code/Framework/AzCore/AzCore/azcore_files.cmake b/Code/Framework/AzCore/AzCore/azcore_files.cmake index 0dfce11247..d25a28aa24 100644 --- a/Code/Framework/AzCore/AzCore/azcore_files.cmake +++ b/Code/Framework/AzCore/AzCore/azcore_files.cmake @@ -117,6 +117,10 @@ set(FILES DOM/DomBackend.h DOM/DomUtils.cpp DOM/DomUtils.h + DOM/DomValue.cpp + DOM/DomValue.h + DOM/DomValueWriter.cpp + DOM/DomValueWriter.h DOM/DomVisitor.cpp DOM/DomVisitor.h DOM/Backends/JSON/JsonBackend.h diff --git a/Code/Framework/AzCore/Tests/DOM/DomJsonBenchmarks.cpp b/Code/Framework/AzCore/Tests/DOM/DomJsonBenchmarks.cpp index 0baa4aeb53..8eda110e7b 100644 --- a/Code/Framework/AzCore/Tests/DOM/DomJsonBenchmarks.cpp +++ b/Code/Framework/AzCore/Tests/DOM/DomJsonBenchmarks.cpp @@ -8,9 +8,10 @@ #if defined(HAVE_BENCHMARK) -#include #include #include +#include +#include #include #include #include @@ -25,27 +26,31 @@ namespace Benchmark { UnitTest::AllocatorsBenchmarkFixture::SetUp(st); AZ::NameDictionary::Create(); + AZ::AllocatorInstance::Create(); } void SetUp(::benchmark::State& st) override { UnitTest::AllocatorsBenchmarkFixture::SetUp(st); AZ::NameDictionary::Create(); + AZ::AllocatorInstance::Create(); } void TearDown(::benchmark::State& st) override { + AZ::AllocatorInstance::Destroy(); AZ::NameDictionary::Destroy(); UnitTest::AllocatorsBenchmarkFixture::TearDown(st); } void TearDown(const ::benchmark::State& st) override { + AZ::AllocatorInstance::Destroy(); AZ::NameDictionary::Destroy(); UnitTest::AllocatorsBenchmarkFixture::TearDown(st); } - AZStd::string GenerateDomJsonBenchmarkPayload(int64_t entryCount, int64_t stringTemplateLength) + rapidjson::Document GenerateDomJsonBenchmarkDocument(int64_t entryCount, int64_t stringTemplateLength) { rapidjson::Document document; document.SetObject(); @@ -103,11 +108,28 @@ namespace Benchmark document.SetObject(); document.AddMember("entries", createObject(), document.GetAllocator()); + return document; + } + + AZStd::string GenerateDomJsonBenchmarkPayload(int64_t entryCount, int64_t stringTemplateLength) + { + rapidjson::Document document = GenerateDomJsonBenchmarkDocument(entryCount, stringTemplateLength); + AZStd::string serializedJson; auto result = AZ::JsonSerializationUtils::WriteJsonString(document, serializedJson); AZ_Assert(result.IsSuccess(), "Failed to serialize generated JSON"); return serializedJson; } + + template + void TakeAndDiscardWithoutTimingDtor(T&& value, benchmark::State& state) + { + { + T instance = AZStd::move(value); + state.PauseTiming(); + } + state.ResumeTiming(); + } }; // Helper macro for registering JSON benchmarks @@ -119,7 +141,7 @@ namespace Benchmark ->Args({ 100, 500 }) \ ->Unit(benchmark::kMillisecond); - BENCHMARK_DEFINE_F(DomJsonBenchmark, DomDeserializeToDocumentInPlace)(benchmark::State& state) + BENCHMARK_DEFINE_F(DomJsonBenchmark, AzDomDeserializeToRapidjsonInPlace)(benchmark::State& state) { AZ::Dom::JsonBackend backend; AZStd::string serializedPayload = GenerateDomJsonBenchmarkPayload(state.range(0), state.range(1)); @@ -136,14 +158,38 @@ namespace Benchmark return AZ::Dom::Utils::ReadFromStringInPlace(backend, payloadCopy, visitor); }); - benchmark::DoNotOptimize(result.GetValue()); + TakeAndDiscardWithoutTimingDtor(result.TakeValue(), state); } state.SetBytesProcessed(serializedPayload.size() * state.iterations()); } - BENCHMARK_REGISTER_JSON(DomJsonBenchmark, DomDeserializeToDocumentInPlace) + BENCHMARK_REGISTER_JSON(DomJsonBenchmark, AzDomDeserializeToRapidjsonInPlace) - BENCHMARK_DEFINE_F(DomJsonBenchmark, DomDeserializeToDocument)(benchmark::State& state) + BENCHMARK_DEFINE_F(DomJsonBenchmark, AzDomDeserializeToAzDomValueInPlace)(benchmark::State& state) + { + AZ::Dom::JsonBackend backend; + AZStd::string serializedPayload = GenerateDomJsonBenchmarkPayload(state.range(0), state.range(1)); + + for (auto _ : state) + { + state.PauseTiming(); + AZStd::string payloadCopy = serializedPayload; + state.ResumeTiming(); + + auto result = AZ::Dom::Utils::WriteToValue( + [&](AZ::Dom::Visitor& visitor) + { + return AZ::Dom::Utils::ReadFromStringInPlace(backend, payloadCopy, visitor); + }); + + TakeAndDiscardWithoutTimingDtor(result.TakeValue(), state); + } + + state.SetBytesProcessed(serializedPayload.size() * state.iterations()); + } + BENCHMARK_REGISTER_JSON(DomJsonBenchmark, AzDomDeserializeToAzDomValueInPlace) + + BENCHMARK_DEFINE_F(DomJsonBenchmark, AzDomDeserializeToRapidjson)(benchmark::State& state) { AZ::Dom::JsonBackend backend; AZStd::string serializedPayload = GenerateDomJsonBenchmarkPayload(state.range(0), state.range(1)); @@ -156,14 +202,34 @@ namespace Benchmark return AZ::Dom::Utils::ReadFromString(backend, serializedPayload, AZ::Dom::Lifetime::Temporary, visitor); }); - benchmark::DoNotOptimize(result.GetValue()); + TakeAndDiscardWithoutTimingDtor(result.TakeValue(), state); } state.SetBytesProcessed(serializedPayload.size() * state.iterations()); } - BENCHMARK_REGISTER_JSON(DomJsonBenchmark, DomDeserializeToDocument) + BENCHMARK_REGISTER_JSON(DomJsonBenchmark, AzDomDeserializeToRapidjson) - BENCHMARK_DEFINE_F(DomJsonBenchmark, JsonUtilsDeserializeToDocument)(benchmark::State& state) + BENCHMARK_DEFINE_F(DomJsonBenchmark, AzDomDeserializeToAzDomValue)(benchmark::State& state) + { + AZ::Dom::JsonBackend backend; + AZStd::string serializedPayload = GenerateDomJsonBenchmarkPayload(state.range(0), state.range(1)); + + for (auto _ : state) + { + auto result = AZ::Dom::Utils::WriteToValue( + [&](AZ::Dom::Visitor& visitor) + { + return AZ::Dom::Utils::ReadFromString(backend, serializedPayload, AZ::Dom::Lifetime::Temporary, visitor); + }); + + TakeAndDiscardWithoutTimingDtor(result.TakeValue(), state); + } + + state.SetBytesProcessed(serializedPayload.size() * state.iterations()); + } + BENCHMARK_REGISTER_JSON(DomJsonBenchmark, AzDomDeserializeToAzDomValue) + + BENCHMARK_DEFINE_F(DomJsonBenchmark, RapidjsonDeserializeToRapidjson)(benchmark::State& state) { AZ::Dom::JsonBackend backend; AZStd::string serializedPayload = GenerateDomJsonBenchmarkPayload(state.range(0), state.range(1)); @@ -172,12 +238,78 @@ namespace Benchmark { auto result = AZ::JsonSerializationUtils::ReadJsonString(serializedPayload); - benchmark::DoNotOptimize(result.GetValue()); + TakeAndDiscardWithoutTimingDtor(result.TakeValue(), state); } state.SetBytesProcessed(serializedPayload.size() * state.iterations()); } - BENCHMARK_REGISTER_JSON(DomJsonBenchmark, JsonUtilsDeserializeToDocument) + BENCHMARK_REGISTER_JSON(DomJsonBenchmark, RapidjsonDeserializeToRapidjson) + + BENCHMARK_DEFINE_F(DomJsonBenchmark, RapidjsonMakeComplexObject)(benchmark::State& state) + { + for (auto _ : state) + { + TakeAndDiscardWithoutTimingDtor(GenerateDomJsonBenchmarkPayload(state.range(0), state.range(1)), state); + } + + state.SetItemsProcessed(state.range(0) * state.range(0) * state.iterations()); + } + BENCHMARK_REGISTER_JSON(DomJsonBenchmark, RapidjsonMakeComplexObject) + + BENCHMARK_DEFINE_F(DomJsonBenchmark, RapidjsonLookupMemberByString)(benchmark::State& state) + { + rapidjson::Document document(rapidjson::kObjectType); + AZStd::vector keys; + for (int64_t i = 0; i < state.range(0); ++i) + { + AZStd::string key(AZStd::string::format("key%" PRId64, i)); + keys.push_back(key); + document.AddMember(rapidjson::Value(key.data(), static_cast(key.size()), document.GetAllocator()), rapidjson::Value(i), document.GetAllocator()); + } + + for (auto _ : state) + { + for (const AZStd::string& key : keys) + { + benchmark::DoNotOptimize(document.FindMember(key.data())); + } + } + + state.SetItemsProcessed(state.iterations() * state.range(0)); + } + BENCHMARK_REGISTER_F(DomJsonBenchmark, RapidjsonLookupMemberByString)->Arg(100)->Arg(1000)->Arg(10000)->Unit(benchmark::kMillisecond); + + BENCHMARK_DEFINE_F(DomJsonBenchmark, RapidjsonDeepCopy)(benchmark::State& state) + { + rapidjson::Document original = GenerateDomJsonBenchmarkDocument(state.range(0), state.range(1)); + + for (auto _ : state) + { + rapidjson::Document copy; + copy.CopyFrom(original, copy.GetAllocator(), true); + TakeAndDiscardWithoutTimingDtor(AZStd::move(copy), state); + } + + state.SetItemsProcessed(state.iterations()); + } + + BENCHMARK_REGISTER_JSON(DomJsonBenchmark, RapidjsonDeepCopy) + + BENCHMARK_DEFINE_F(DomJsonBenchmark, RapidjsonCopyAndMutate)(benchmark::State& state) + { + rapidjson::Document original = GenerateDomJsonBenchmarkDocument(state.range(0), state.range(1)); + + for (auto _ : state) + { + rapidjson::Document copy; + copy.CopyFrom(original, copy.GetAllocator(), true); + copy["entries"]["Key0"].PushBack(42, copy.GetAllocator()); + TakeAndDiscardWithoutTimingDtor(AZStd::move(copy), state); + } + + state.SetItemsProcessed(state.iterations()); + } + BENCHMARK_REGISTER_JSON(DomJsonBenchmark, RapidjsonCopyAndMutate) #undef BENCHMARK_REGISTER_JSON } // namespace Benchmark diff --git a/Code/Framework/AzCore/Tests/DOM/DomValueBenchmarks.cpp b/Code/Framework/AzCore/Tests/DOM/DomValueBenchmarks.cpp new file mode 100644 index 0000000000..40b96e148b --- /dev/null +++ b/Code/Framework/AzCore/Tests/DOM/DomValueBenchmarks.cpp @@ -0,0 +1,263 @@ +/* + * Copyright (c) Contributors to the Open 3D Engine Project. + * For complete copyright and license terms please see the LICENSE at the root of this distribution. + * + * SPDX-License-Identifier: Apache-2.0 OR MIT + * + */ + +#include +#include +#include +#include +#include + +namespace AZ::Dom::Benchmark +{ + class DomValueBenchmark : public UnitTest::AllocatorsBenchmarkFixture + { + public: + void SetUp(const ::benchmark::State& st) override + { + UnitTest::AllocatorsBenchmarkFixture::SetUp(st); + AZ::NameDictionary::Create(); + AZ::AllocatorInstance::Create(); + } + + void SetUp(::benchmark::State& st) override + { + UnitTest::AllocatorsBenchmarkFixture::SetUp(st); + AZ::NameDictionary::Create(); + AZ::AllocatorInstance::Create(); + } + + void TearDown(::benchmark::State& st) override + { + AZ::AllocatorInstance::Destroy(); + AZ::NameDictionary::Destroy(); + UnitTest::AllocatorsBenchmarkFixture::TearDown(st); + } + + void TearDown(const ::benchmark::State& st) override + { + AZ::AllocatorInstance::Destroy(); + AZ::NameDictionary::Destroy(); + UnitTest::AllocatorsBenchmarkFixture::TearDown(st); + } + + Value GenerateDomBenchmarkPayload(int64_t entryCount, int64_t stringTemplateLength) + { + Value root(Type::Object); + + AZStd::string entryTemplate; + while (entryTemplate.size() < static_cast(stringTemplateLength)) + { + entryTemplate += "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor "; + } + entryTemplate.resize(stringTemplateLength); + AZStd::string buffer; + + auto createString = [&](int n) -> Value + { + return Value(AZStd::string::format("#%i %s", n, entryTemplate.c_str()), true); + }; + + auto createEntry = [&](int n) -> Value + { + Value entry(Type::Object); + entry.AddMember("string", createString(n)); + entry.AddMember("int", Value(n)); + entry.AddMember("double", Value(static_cast(n) * 0.5)); + entry.AddMember("bool", Value(n % 2 == 0)); + entry.AddMember("null", Value(Type::Null)); + return entry; + }; + + auto createArray = [&]() -> Value + { + Value array(Type::Array); + for (int i = 0; i < entryCount; ++i) + { + array.ArrayPushBack(createEntry(i)); + } + return array; + }; + + auto createObject = [&]() -> Value + { + Value object; + object.SetObject(); + for (int i = 0; i < entryCount; ++i) + { + buffer = AZStd::string::format("Key%i", i); + object.AddMember(AZ::Name(buffer), createArray()); + } + return object; + }; + + root["entries"] = createObject(); + + return root; + } + + template + void TakeAndDiscardWithoutTimingDtor(T&& value, benchmark::State& state) + { + { + T instance = AZStd::move(value); + state.PauseTiming(); + } + state.ResumeTiming(); + } + }; + + BENCHMARK_DEFINE_F(DomValueBenchmark, AzDomValueMakeComplexObject)(benchmark::State& state) + { + for (auto _ : state) + { + TakeAndDiscardWithoutTimingDtor(GenerateDomBenchmarkPayload(state.range(0), state.range(1)), state); + } + + state.SetItemsProcessed(state.range(0) * state.range(0) * state.iterations()); + } + BENCHMARK_REGISTER_F(DomValueBenchmark, AzDomValueMakeComplexObject) + ->Args({ 10, 5 }) + ->Args({ 10, 500 }) + ->Args({ 100, 5 }) + ->Args({ 100, 500 }) + ->Unit(benchmark::kMillisecond); + + BENCHMARK_DEFINE_F(DomValueBenchmark, AzDomValueShallowCopy)(benchmark::State& state) + { + Value original = GenerateDomBenchmarkPayload(state.range(0), state.range(1)); + + for (auto _ : state) + { + Value copy = original; + benchmark::DoNotOptimize(copy); + } + + state.SetItemsProcessed(state.iterations()); + } + BENCHMARK_REGISTER_F(DomValueBenchmark, AzDomValueShallowCopy) + ->Args({ 10, 5 }) + ->Args({ 10, 500 }) + ->Args({ 100, 5 }) + ->Args({ 100, 500 }) + ->Unit(benchmark::kNanosecond); + + BENCHMARK_DEFINE_F(DomValueBenchmark, AzDomValueCopyAndMutate)(benchmark::State& state) + { + Value original = GenerateDomBenchmarkPayload(state.range(0), state.range(1)); + + for (auto _ : state) + { + Value copy = original; + copy["entries"]["Key0"].ArrayPushBack(Value(42)); + TakeAndDiscardWithoutTimingDtor(AZStd::move(copy), state); + } + + state.SetItemsProcessed(state.iterations()); + } + BENCHMARK_REGISTER_F(DomValueBenchmark, AzDomValueCopyAndMutate) + ->Args({ 10, 5 }) + ->Args({ 10, 500 }) + ->Args({ 100, 5 }) + ->Args({ 100, 500 }) + ->Unit(benchmark::kNanosecond); + + BENCHMARK_DEFINE_F(DomValueBenchmark, AzDomValueDeepCopy)(benchmark::State& state) + { + Value original = GenerateDomBenchmarkPayload(state.range(0), state.range(1)); + + for (auto _ : state) + { + Value copy = Utils::DeepCopy(original); + TakeAndDiscardWithoutTimingDtor(AZStd::move(copy), state); + } + + state.SetItemsProcessed(state.iterations()); + } + BENCHMARK_REGISTER_F(DomValueBenchmark, AzDomValueDeepCopy) + ->Args({ 10, 5 }) + ->Args({ 10, 500 }) + ->Args({ 100, 5 }) + ->Args({ 100, 500 }) + ->Unit(benchmark::kMillisecond); + + BENCHMARK_DEFINE_F(DomValueBenchmark, LookupMemberByName)(benchmark::State& state) + { + Value value(Type::Object); + AZStd::vector keys; + for (int64_t i = 0; i < state.range(0); ++i) + { + AZ::Name key(AZStd::string::format("key%" PRId64, i)); + keys.push_back(key); + value[key] = i; + } + + for (auto _ : state) + { + for (const AZ::Name& key : keys) + { + benchmark::DoNotOptimize(value[key]); + } + } + + state.SetItemsProcessed(state.iterations() * state.range(0)); + } + BENCHMARK_REGISTER_F(DomValueBenchmark, LookupMemberByName)->Arg(100)->Arg(1000)->Arg(10000)->Unit(benchmark::kMillisecond); + + BENCHMARK_DEFINE_F(DomValueBenchmark, LookupMemberByString)(benchmark::State& state) + { + Value value(Type::Object); + AZStd::vector keys; + for (int64_t i = 0; i < state.range(0); ++i) + { + AZStd::string key(AZStd::string::format("key%" PRId64, i)); + keys.push_back(key); + value[key] = i; + } + + for (auto _ : state) + { + for (const AZStd::string& key : keys) + { + benchmark::DoNotOptimize(value[key]); + } + } + + state.SetItemsProcessed(state.iterations() * state.range(0)); + } + BENCHMARK_REGISTER_F(DomValueBenchmark, LookupMemberByString)->Arg(100)->Arg(1000)->Arg(10000)->Unit(benchmark::kMillisecond); + + BENCHMARK_DEFINE_F(DomValueBenchmark, LookupMemberByStringComparison)(benchmark::State& state) + { + Value value(Type::Object); + AZStd::vector keys; + for (int64_t i = 0; i < state.range(0); ++i) + { + AZStd::string key(AZStd::string::format("key%" PRId64, i)); + keys.push_back(key); + value[key] = i; + } + + for (auto _ : state) + { + for (const AZStd::string& key : keys) + { + const Object::ContainerType& object = value.GetObject(); + benchmark::DoNotOptimize(AZStd::find_if( + object.cbegin(), object.cend(), + [&key](const Object::EntryType& entry) + { + return key == entry.first.GetStringView(); + })); + } + } + + state.SetItemsProcessed(state.iterations() * state.range(0)); + } + BENCHMARK_REGISTER_F(DomValueBenchmark, LookupMemberByStringComparison)->Arg(100)->Arg(1000)->Arg(10000)->Unit(benchmark::kMillisecond); + +} // namespace AZ::Dom::Benchmark diff --git a/Code/Framework/AzCore/Tests/DOM/DomValueTests.cpp b/Code/Framework/AzCore/Tests/DOM/DomValueTests.cpp new file mode 100644 index 0000000000..10e9f29a44 --- /dev/null +++ b/Code/Framework/AzCore/Tests/DOM/DomValueTests.cpp @@ -0,0 +1,392 @@ +/* + * Copyright (c) Contributors to the Open 3D Engine Project. + * For complete copyright and license terms please see the LICENSE at the root of this distribution. + * + * SPDX-License-Identifier: Apache-2.0 OR MIT + * + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace AZ::Dom::Tests +{ + class DomValueTests : public UnitTest::AllocatorsFixture + { + public: + void SetUp() override + { + UnitTest::AllocatorsFixture::SetUp(); + NameDictionary::Create(); + AZ::AllocatorInstance::Create(); + } + + void TearDown() override + { + m_value = Value(); + + AZ::AllocatorInstance::Destroy(); + NameDictionary::Destroy(); + UnitTest::AllocatorsFixture::TearDown(); + } + + void PerformValueChecks() + { + Value shallowCopy = m_value; + EXPECT_EQ(m_value, shallowCopy); + EXPECT_TRUE(Utils::DeepCompareIsEqual(m_value, shallowCopy)); + + Value deepCopy = Utils::DeepCopy(m_value); + EXPECT_TRUE(Utils::DeepCompareIsEqual(m_value, deepCopy)); + } + + Value m_value; + }; + + TEST_F(DomValueTests, EmptyArray) + { + m_value.SetArray(); + + EXPECT_TRUE(m_value.IsArray()); + EXPECT_EQ(m_value.ArraySize(), 0); + + PerformValueChecks(); + } + + TEST_F(DomValueTests, SimpleArray) + { + m_value.SetArray(); + + for (int i = 0; i < 5; ++i) + { + m_value.ArrayPushBack(Value(i)); + EXPECT_EQ(m_value.ArraySize(), i + 1); + EXPECT_EQ(m_value[i].GetInt64(), i); + } + + PerformValueChecks(); + } + + TEST_F(DomValueTests, NestedArrays) + { + Value x(5); + m_value.SetArray(); + for (int j = 0; j < 5; ++j) + { + Value nestedArray(Type::Array); + for (int i = 0; i < 5; ++i) + { + nestedArray.ArrayPushBack(Value(i)); + } + m_value.ArrayPushBack(AZStd::move(nestedArray)); + } + + EXPECT_EQ(m_value.ArraySize(), 5); + for (int i = 0; i < 3; ++i) + { + EXPECT_EQ(m_value[i].ArraySize(), 5); + for (int j = 0; j < 5; ++j) + { + EXPECT_EQ(m_value[i][j].GetInt64(), j); + } + } + + PerformValueChecks(); + } + + TEST_F(DomValueTests, EmptyObject) + { + m_value.SetObject(); + EXPECT_EQ(m_value.MemberCount(), 0); + + PerformValueChecks(); + } + + TEST_F(DomValueTests, SimpleObject) + { + m_value.SetObject(); + for (int i = 0; i < 5; ++i) + { + AZStd::string key = AZStd::string::format("Key%i", i); + m_value.AddMember(key, Value(i)); + EXPECT_EQ(m_value.MemberCount(), i + 1); + EXPECT_EQ(m_value[key].GetInt64(), i); + } + + PerformValueChecks(); + } + + TEST_F(DomValueTests, NestedObjects) + { + m_value.SetObject(); + for (int j = 0; j < 3; ++j) + { + Value nestedObject(Type::Object); + for (int i = 0; i < 5; ++i) + { + nestedObject.AddMember(AZStd::string::format("Key%i", i), Value(i)); + } + m_value.AddMember(AZStd::string::format("Obj%i", j), AZStd::move(nestedObject)); + } + + EXPECT_EQ(m_value.MemberCount(), 3); + for (int j = 0; j < 3; ++j) + { + const Value& nestedObject = m_value[AZStd::string::format("Obj%i", j)]; + EXPECT_EQ(nestedObject.MemberCount(), 5); + for (int i = 0; i < 5; ++i) + { + EXPECT_EQ(nestedObject[AZStd::string::format("Key%i", i)].GetInt64(), i); + } + } + + PerformValueChecks(); + } + + TEST_F(DomValueTests, EmptyNode) + { + m_value.SetNode("Test"); + EXPECT_EQ(m_value.GetNodeName(), AZ::Name("Test")); + EXPECT_EQ(m_value.MemberCount(), 0); + EXPECT_EQ(m_value.ArraySize(), 0); + + PerformValueChecks(); + } + + TEST_F(DomValueTests, SimpleNode) + { + m_value.SetNode("Test"); + + for (int i = 0; i < 10; ++i) + { + m_value.ArrayPushBack(Value(i)); + EXPECT_EQ(m_value.ArraySize(), i + 1); + EXPECT_EQ(m_value[i].GetInt64(), i); + + if (i < 5) + { + AZ::Name key = AZ::Name(AZStd::string::format("TwoTimes%i", i)); + m_value.AddMember(key, Value(i * 2)); + EXPECT_EQ(m_value.MemberCount(), i + 1); + EXPECT_EQ(m_value[key].GetInt64(), i * 2); + } + } + + PerformValueChecks(); + } + + TEST_F(DomValueTests, NestedNodes) + { + m_value.SetNode("TopLevel"); + + const AZ::Name childNodeName("ChildNode"); + + for (int i = 0; i < 5; ++i) + { + Value childNode(Type::Node); + childNode.SetNodeName(childNodeName); + childNode.SetNodeValue(Value(i)); + + childNode.AddMember("foo", Value(i)); + childNode.AddMember("bar", Value("test", false)); + + m_value.ArrayPushBack(childNode); + } + + EXPECT_EQ(m_value.ArraySize(), 5); + for (int i = 0; i < 5; ++i) + { + const Value& childNode = m_value[i]; + EXPECT_EQ(childNode.GetNodeName(), childNodeName); + EXPECT_EQ(childNode.GetNodeValue().GetInt64(), i); + EXPECT_EQ(childNode["foo"].GetInt64(), i); + EXPECT_EQ(childNode["bar"].GetString(), "test"); + } + + PerformValueChecks(); + } + + TEST_F(DomValueTests, Int64) + { + m_value.SetObject(); + m_value["int64_min"] = AZStd::numeric_limits::min(); + m_value["int64_max"] = AZStd::numeric_limits::max(); + + EXPECT_EQ(m_value["int64_min"].GetType(), Type::Int64); + EXPECT_EQ(m_value["int64_min"].GetInt64(), AZStd::numeric_limits::min()); + EXPECT_EQ(m_value["int64_max"].GetType(), Type::Int64); + EXPECT_EQ(m_value["int64_max"].GetInt64(), AZStd::numeric_limits::max()); + + PerformValueChecks(); + } + + TEST_F(DomValueTests, Uint64) + { + m_value.SetObject(); + m_value["uint64_min"] = AZStd::numeric_limits::min(); + m_value["uint64_max"] = AZStd::numeric_limits::max(); + + EXPECT_EQ(m_value["uint64_min"].GetType(), Type::Uint64); + EXPECT_EQ(m_value["uint64_min"].GetInt64(), AZStd::numeric_limits::min()); + EXPECT_EQ(m_value["uint64_max"].GetType(), Type::Uint64); + EXPECT_EQ(m_value["uint64_max"].GetInt64(), AZStd::numeric_limits::max()); + + PerformValueChecks(); + } + + TEST_F(DomValueTests, Double) + { + m_value.SetObject(); + m_value["double_min"] = AZStd::numeric_limits::min(); + m_value["double_max"] = AZStd::numeric_limits::max(); + + EXPECT_EQ(m_value["double_min"].GetType(), Type::Double); + EXPECT_EQ(m_value["double_min"].GetDouble(), AZStd::numeric_limits::min()); + EXPECT_EQ(m_value["double_max"].GetType(), Type::Double); + EXPECT_EQ(m_value["double_max"].GetDouble(), AZStd::numeric_limits::max()); + + PerformValueChecks(); + } + + TEST_F(DomValueTests, Null) + { + m_value.SetObject(); + m_value["null_value"] = Value(Type::Null); + + EXPECT_EQ(m_value["null_value"].GetType(), Type::Null); + EXPECT_EQ(m_value["null_type"], Value()); + + PerformValueChecks(); + } + + TEST_F(DomValueTests, Bool) + { + m_value.SetObject(); + m_value["true_value"] = true; + m_value["false_value"] = false; + + EXPECT_EQ(m_value["true_value"].GetType(), Type::Bool); + EXPECT_EQ(m_value["true_value"].GetBool(), true); + EXPECT_EQ(m_value["false_value"].GetType(), Type::Bool); + EXPECT_EQ(m_value["false_value"].GetBool(), false); + + PerformValueChecks(); + } + + TEST_F(DomValueTests, String) + { + const char* s1 = "reference string long enough to avoid SSO"; + const char* s2 = "copy string long enough to avoid SSO"; + + m_value.SetObject(); + AZStd::string stringToReference = s1; + m_value["no_copy"] = Value(stringToReference, false); + AZStd::string stringToCopy = s2; + m_value["copy"] = Value(stringToCopy, true); + + EXPECT_EQ(m_value["no_copy"].GetType(), Type::String); + EXPECT_EQ(m_value["no_copy"].GetString(), s1); + stringToReference.at(0) = 'F'; + EXPECT_NE(m_value["no_copy"].GetString(), s1); + + EXPECT_EQ(m_value["copy"].GetType(), Type::String); + EXPECT_EQ(m_value["copy"].GetString(), s2); + stringToCopy.at(0) = 'F'; + EXPECT_EQ(m_value["copy"].GetString(), s2); + + PerformValueChecks(); + } + + TEST_F(DomValueTests, CopyOnWrite_Object) + { + Value v1(Type::Object); + v1["foo"] = 5; + + Value nestedObject(Type::Object); + v1["obj"] = nestedObject; + + Value v2 = v1; + EXPECT_EQ(&v1.GetObject(), &v2.GetObject()); + EXPECT_EQ(&v1.FindMember("obj")->second.GetObject(), &v2.FindMember("obj")->second.GetObject()); + + v2["foo"] = 0; + + EXPECT_NE(&v1.GetObject(), &v2.GetObject()); + EXPECT_EQ(&v1.FindMember("obj")->second.GetObject(), &v2.FindMember("obj")->second.GetObject()); + + v2["obj"]["key"] = true; + + EXPECT_NE(&v1.GetObject(), &v2.GetObject()); + EXPECT_NE(&v1.FindMember("obj")->second.GetObject(), &v2.FindMember("obj")->second.GetObject()); + + v2 = v1; + + EXPECT_EQ(&v1.GetObject(), &v2.GetObject()); + EXPECT_EQ(&v1.FindMember("obj")->second.GetObject(), &v2.FindMember("obj")->second.GetObject()); + } + + TEST_F(DomValueTests, CopyOnWrite_Array) + { + Value v1(Type::Array); + v1.ArrayPushBack(Value(1)); + v1.ArrayPushBack(Value(2)); + + Value nestedArray(Type::Array); + v1.ArrayPushBack(nestedArray); + Value v2 = v1; + + EXPECT_EQ(&v1.GetArray(), &v2.GetArray()); + EXPECT_EQ(&v1.ArrayAt(2).GetArray(), &v2.ArrayAt(2).GetArray()); + + v2[0] = 0; + + EXPECT_NE(&v1.GetArray(), &v2.GetArray()); + EXPECT_EQ(&v1.ArrayAt(2).GetArray(), &v2.ArrayAt(2).GetArray()); + + v2[2].ArrayPushBack(Value(42)); + + EXPECT_NE(&v1.GetArray(), &v2.GetArray()); + EXPECT_NE(&v1.ArrayAt(2).GetArray(), &v2.ArrayAt(2).GetArray()); + + v2 = v1; + + EXPECT_EQ(&v1.GetArray(), &v2.GetArray()); + EXPECT_EQ(&v1.ArrayAt(2).GetArray(), &v2.ArrayAt(2).GetArray()); + } + + TEST_F(DomValueTests, CopyOnWrite_Node) + { + Value v1; + v1.SetNode("TopLevel"); + + v1.ArrayPushBack(Value(1)); + v1.ArrayPushBack(Value(2)); + v1["obj"].SetNode("Nested"); + Value v2 = v1; + + EXPECT_EQ(&v1.GetNode(), &v2.GetNode()); + EXPECT_EQ(&v1["obj"].GetNode(), &v2["obj"].GetNode()); + + v2[0] = 0; + + EXPECT_NE(&v1.GetNode(), &v2.GetNode()); + EXPECT_EQ(&v1["obj"].GetNode(), &v2["obj"].GetNode()); + + v2["obj"].ArrayPushBack(Value(42)); + + EXPECT_NE(&v1.GetNode(), &v2.GetNode()); + EXPECT_NE(&v1["obj"].GetNode(), &v2["obj"].GetNode()); + + v2 = v1; + + EXPECT_EQ(&v1.GetNode(), &v2.GetNode()); + EXPECT_EQ(&v1["obj"].GetNode(), &v2["obj"].GetNode()); + } +} // namespace AZ::Dom::Tests diff --git a/Code/Framework/AzCore/Tests/azcoretests_files.cmake b/Code/Framework/AzCore/Tests/azcoretests_files.cmake index 834e3431b6..7be3afb4a9 100644 --- a/Code/Framework/AzCore/Tests/azcoretests_files.cmake +++ b/Code/Framework/AzCore/Tests/azcoretests_files.cmake @@ -217,6 +217,8 @@ set(FILES AZStd/VectorAndArray.cpp DOM/DomJsonTests.cpp DOM/DomJsonBenchmarks.cpp + DOM/DomValueTests.cpp + DOM/DomValueBenchmarks.cpp ) # Prevent the following files from being grouped in UNITY builds