diff --git a/Code/Framework/AzCore/AzCore/DOM/DomUtils.cpp b/Code/Framework/AzCore/AzCore/DOM/DomUtils.cpp index 2e78661518..46fa5b9bac 100644 --- a/Code/Framework/AzCore/AzCore/DOM/DomUtils.cpp +++ b/Code/Framework/AzCore/AzCore/DOM/DomUtils.cpp @@ -21,4 +21,16 @@ namespace AZ::Dom::Utils { return backend.ReadFromBufferInPlace(string.data(), string.size(), visitor); } + + AZ::Outcome AZ::Dom::Utils::WriteToValue(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)); + } } // namespace AZ::Dom::Utils diff --git a/Code/Framework/AzCore/AzCore/DOM/DomUtils.h b/Code/Framework/AzCore/AzCore/DOM/DomUtils.h index 84a6eb5687..f03e5b66b8 100644 --- a/Code/Framework/AzCore/AzCore/DOM/DomUtils.h +++ b/Code/Framework/AzCore/AzCore/DOM/DomUtils.h @@ -9,9 +9,12 @@ #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(Backend::WriteCallback writeCallback); } // namespace AZ::Dom::Utils diff --git a/Code/Framework/AzCore/AzCore/DOM/DomValue.cpp b/Code/Framework/AzCore/AzCore/DOM/DomValue.cpp index d9398da116..d8650601ba 100644 --- a/Code/Framework/AzCore/AzCore/DOM/DomValue.cpp +++ b/Code/Framework/AzCore/AzCore/DOM/DomValue.cpp @@ -19,7 +19,7 @@ namespace AZ::Dom { if (refCountedPointer.use_count() > 1) { - AZStd::shared_ptr newPointer = AZStd::make_shared(); + AZStd::shared_ptr newPointer = AZStd::allocate_shared(AZStdAlloc()); *newPointer = *refCountedPointer; refCountedPointer = AZStd::move(newPointer); } @@ -71,8 +71,8 @@ namespace AZ::Dom } Value::Value(Value&& value) noexcept - : m_value(value.m_value) { + operator=(value); } Value::Value(AZStd::string_view string, bool copy) @@ -174,7 +174,7 @@ namespace AZ::Dom Value& Value::operator=(Value&& other) noexcept { - m_value = other.m_value; + m_value.swap(other.m_value); return *this; } @@ -212,7 +212,7 @@ namespace AZ::Dom void Value::Swap(Value& other) noexcept { - AZStd::swap(m_value, other.m_value); + m_value.swap(other.m_value); } Type Dom::Value::GetType() const @@ -229,14 +229,15 @@ namespace AZ::Dom return AZStd::get(m_value) ? Type::TrueType : Type::FalseType; case 5: // AZStd::string_view case 6: // AZStd::shared_ptr + case 7: // ShortStringType return Type::StringType; - case 7: // ObjectPtr + case 8: // ObjectPtr return Type::ObjectType; - case 8: // ArrayPtr + case 9: // ArrayPtr return Type::ArrayType; - case 9: // NodePtr + case 10: // NodePtr return Type::NodeType; - case 10: // AZStd::any* + case 11: // AZStd::any* return Type::OpaqueType; } AZ_Assert(false, "AZ::Dom::Value::GetType: m_value has an unexpected type"); @@ -310,7 +311,7 @@ namespace AZ::Dom Value& Value::SetObject() { - m_value = AZStd::make_shared(); + m_value = AZStd::allocate_shared(AZStdAlloc()); return *this; } @@ -511,6 +512,7 @@ namespace AZ::Dom Value& Value::AddMember(KeyType name, const Value& value) { Object::ContainerType& object = GetObjectInternal(); + object.reserve((object.size() / Object::ReserveIncrement + 1) * Object::ReserveIncrement); if (auto memberIt = FindMutableMember(name); memberIt != object.end()) { memberIt->second = value; @@ -613,7 +615,7 @@ namespace AZ::Dom Value& Value::SetArray() { - m_value = AZStd::make_shared(); + m_value = AZStd::allocate_shared(AZStdAlloc()); return *this; } @@ -685,7 +687,9 @@ namespace AZ::Dom Value& Value::PushBack(Value value) { - GetArrayInternal().push_back(AZStd::move(value)); + Array::ContainerType& array = GetArrayInternal(); + array.reserve((array.size() / Array::ReserveIncrement + 1) * Array::ReserveIncrement); + array.push_back(AZStd::move(value)); return *this; } @@ -717,7 +721,7 @@ namespace AZ::Dom void Value::SetNode(AZ::Name name) { - m_value = AZStd::make_shared(name); + m_value = AZStd::allocate_shared(AZStdAlloc(), name); } void Value::SetNode(AZStd::string_view name) @@ -899,6 +903,11 @@ namespace AZ::Dom return AZStd::get(m_value); case 6: // AZStd::shared_ptr return *AZStd::get>(m_value); + case 7: // ShortStringType + { + const ShortStringType& ShortString = AZStd::get(m_value); + return { ShortString.m_data.data(), ShortString.m_size }; + } } AZ_Assert(false, "AZ::Dom::Value: Called GetString on a non-string type"); return {}; @@ -911,12 +920,26 @@ namespace AZ::Dom void Value::SetString(AZStd::string_view value) { + if (value.size() <= ShortStringSize) + { + ShortStringType buffer; + buffer.m_size = value.size(); + memcpy(buffer.m_data.data(), value.data(), buffer.m_size); + m_value = buffer; + } m_value = value; } void Value::CopyFromString(AZStd::string_view value) { - m_value = AZStd::make_shared(value); + if (value.size() <= ShortStringSize) + { + SetString(value); + } + else + { + m_value = AZStd::allocate_shared(AZStdAlloc(), value); + } } AZStd::any& Value::GetOpaqueValue() const diff --git a/Code/Framework/AzCore/AzCore/DOM/DomValue.h b/Code/Framework/AzCore/AzCore/DOM/DomValue.h index 39ae5a5c37..4dd3bde197 100644 --- a/Code/Framework/AzCore/AzCore/DOM/DomValue.h +++ b/Code/Framework/AzCore/AzCore/DOM/DomValue.h @@ -11,11 +11,13 @@ #include #include #include +#include #include #include #include #include #include +#include namespace AZ::Dom { @@ -34,6 +36,22 @@ namespace AZ::Dom OpaqueType = 8, }; + class ValueAllocator final : public ThreadPoolBase + { + public: + AZ_CLASS_ALLOCATOR(ValueAllocator, SystemAllocator, 0); + AZ_TYPE_INFO(ValueAllocator, "{5BC8B389-72C7-459E-B502-12E74D61869F}"); + + ValueAllocator() + : ThreadPoolBase("DomValueAllocator", "Allocator for AZ::Dom::Value") + { + } + }; + + // class ValueAllocator : public Internal::PoolAllocatorHelper; using Iterator = ContainerType::iterator; using ConstIterator = ContainerType::const_iterator; + static constexpr const size_t ReserveIncrement = 4; private: ContainerType m_values; @@ -59,6 +78,7 @@ namespace AZ::Dom using ContainerType = AZStd::vector; using Iterator = ContainerType::iterator; using ConstIterator = ContainerType::const_iterator; + static constexpr const size_t ReserveIncrement = 8; private: ContainerType m_values; @@ -293,6 +313,18 @@ namespace AZ::Dom explicit Value(AZStd::any* opaqueValue); + static constexpr const size_t ShortStringSize = sizeof(AZStd::string_view) - sizeof(size_t); + struct ShortStringType + { + AZStd::array m_data; + size_t m_size; + + bool operator==(const ShortStringType& other) const + { + return m_size == other.m_size ? memcmp(m_data.data(), other.m_data.data(), m_size) == 0 : false; + } + }; + // If using the the copy on write model, anything stored internally as a shared_ptr will // detach and copy when doing a mutating operation if use_count() > 1. @@ -311,6 +343,7 @@ namespace AZ::Dom // StringType AZStd::string_view, AZStd::shared_ptr, + ShortStringType, // ObjectType ObjectPtr, // ArrayType @@ -320,6 +353,10 @@ namespace AZ::Dom // OpaqueType AZStd::any*>; + static_assert( + sizeof(ValueType) == sizeof(AZStd::variant), + "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 index a760cc5b28..cf46a77d54 100644 --- a/Code/Framework/AzCore/AzCore/DOM/DomValueWriter.cpp +++ b/Code/Framework/AzCore/AzCore/DOM/DomValueWriter.cpp @@ -103,35 +103,68 @@ namespace AZ::Dom } const ValueInfo& topEntry = m_entryStack.top(); - if (topEntry.m_container.GetType() != containerType) + 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 (topEntry.m_attributeCount != attributeCount) + if (buffer.m_attributes.size() != attributeCount) { return VisitorFailure( VisitorErrorCode::InternalError, AZStd::string::format( "AZ::Dom::ValueWriter: %s expected %llu attributes but received %llu attributes instead", endMethodName, attributeCount, - topEntry.m_attributeCount)); + buffer.m_attributes.size())); } - if (topEntry.m_elementCount != elementCount) + if (buffer.m_elements.size() != elementCount) { return VisitorFailure( VisitorErrorCode::InternalError, AZStd::string::format( "AZ::Dom::ValueWriter: %s expected %llu elements but received %llu elements instead", endMethodName, elementCount, - topEntry.m_elementCount)); + buffer.m_elements.size())); + } + if (buffer.m_attributes.size() > 0) + { + container.MemberReserve(buffer.m_attributes.size()); + for (AZStd::pair& entry : buffer.m_attributes) + { + container.AddMember(AZStd::move(entry.first), AZStd::move(entry.second)); + } + buffer.m_attributes.clear(); + } + + if(buffer.m_elements.size() > 0) + { + container.Reserve(buffer.m_elements.size()); + for (Value& entry : buffer.m_elements) + { + container.PushBack(AZStd::move(entry)); + } + buffer.m_elements.clear(); } 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::ObjectType, attributeCount, 0); @@ -192,16 +225,16 @@ namespace AZ::Dom m_entryStack.top().m_value.Swap(value); ValueInfo& newEntry = m_entryStack.top(); + constexpr const size_t reserveSize = 8; + if (!newEntry.m_key.IsEmpty()) { - newEntry.m_container.AddMember(newEntry.m_key, AZStd::move(value)); + GetValueBuffer().m_attributes.emplace_back(AZStd::move(newEntry.m_key), AZStd::move(value)); newEntry.m_key = AZ::Name(); - ++newEntry.m_attributeCount; } else { - newEntry.m_container.PushBack(AZStd::move(value)); - ++newEntry.m_elementCount; + GetValueBuffer().m_elements.emplace_back(AZStd::move(value)); } return VisitorSuccess(); diff --git a/Code/Framework/AzCore/AzCore/DOM/DomValueWriter.h b/Code/Framework/AzCore/AzCore/DOM/DomValueWriter.h index fb5f4324a6..617184a926 100644 --- a/Code/Framework/AzCore/AzCore/DOM/DomValueWriter.h +++ b/Code/Framework/AzCore/AzCore/DOM/DomValueWriter.h @@ -48,11 +48,18 @@ namespace AZ::Dom KeyType m_key; Value m_value; Value& m_container; - AZ::u64 m_attributeCount = 0; - AZ::u64 m_elementCount = 0; }; + struct ValueBuffer + { + AZStd::vector m_elements; + AZStd::vector> m_attributes; + }; + + ValueBuffer& GetValueBuffer(); + Value& m_result; AZStd::stack m_entryStack; + AZStd::vector m_valueBuffers; }; } // namespace AZ::Dom diff --git a/Code/Framework/AzCore/AzCore/Memory/PoolAllocator.h b/Code/Framework/AzCore/AzCore/Memory/PoolAllocator.h index 72418b3d6e..bf24f04156 100644 --- a/Code/Framework/AzCore/AzCore/Memory/PoolAllocator.h +++ b/Code/Framework/AzCore/AzCore/Memory/PoolAllocator.h @@ -25,12 +25,12 @@ namespace AZ * Template you can use to create your own thread pool allocators, as you can't inherit from ThreadPoolAllocator. * This is the case because we use tread local storage and we need separate "static" instance for each allocator. */ - template + template class PoolAllocatorHelper - : public SimpleSchemaAllocator + : public SimpleSchemaAllocator { public: - using Base = SimpleSchemaAllocator; + using Base = SimpleSchemaAllocator; using pointer_type = typename Base::pointer_type; using size_type = typename Base::size_type; using difference_type = typename Base::difference_type; @@ -140,13 +140,13 @@ namespace AZ * use PoolAllocatorThreadSafe or do the sync yourself. */ class PoolAllocator - : public Internal::PoolAllocatorHelper + : public Internal::PoolAllocatorHelper { public: AZ_CLASS_ALLOCATOR(PoolAllocator, SystemAllocator, 0); AZ_TYPE_INFO(PoolAllocator, "{D3DC61AF-0949-4BFA-87E0-62FA03A4C025}"); - using Base = Internal::PoolAllocatorHelper; + using Base = Internal::PoolAllocatorHelper; PoolAllocator(const char* name = "PoolAllocator", const char* desc = "Generic pool allocator for small objects") : Base(name, desc) @@ -154,21 +154,21 @@ namespace AZ } }; - template - using ThreadPoolBase = Internal::PoolAllocatorHelper >; + template + using ThreadPoolBase = Internal::PoolAllocatorHelper, ProfileAllocations >; /*! * Thread safe pool allocator. If you want to create your own thread pool heap, * inherit from ThreadPoolBase, as we need unique static variable for allocator type. */ class ThreadPoolAllocator final - : public ThreadPoolBase + : public ThreadPoolBase { public: AZ_CLASS_ALLOCATOR(ThreadPoolAllocator, SystemAllocator, 0); AZ_TYPE_INFO(ThreadPoolAllocator, "{05B4857F-CD06-4942-99FD-CA6A7BAE855A}"); - using Base = ThreadPoolBase; + using Base = ThreadPoolBase; ThreadPoolAllocator() : Base("PoolAllocatorThreadSafe", "Generic thread safe pool allocator for small objects") diff --git a/Code/Framework/AzCore/Tests/DOM/DomJsonBenchmarks.cpp b/Code/Framework/AzCore/Tests/DOM/DomJsonBenchmarks.cpp index 0baa4aeb53..3e6143e805 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,22 +26,26 @@ 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); } @@ -143,6 +148,30 @@ namespace Benchmark } BENCHMARK_REGISTER_JSON(DomJsonBenchmark, DomDeserializeToDocumentInPlace) + BENCHMARK_DEFINE_F(DomJsonBenchmark, DomDeserializeToDomValueInPlace)(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); + }); + + benchmark::DoNotOptimize(result.GetValue()); + } + + state.SetBytesProcessed(serializedPayload.size() * state.iterations()); + } + BENCHMARK_REGISTER_JSON(DomJsonBenchmark, DomDeserializeToDomValueInPlace) + BENCHMARK_DEFINE_F(DomJsonBenchmark, DomDeserializeToDocument)(benchmark::State& state) { AZ::Dom::JsonBackend backend; @@ -179,6 +208,17 @@ namespace Benchmark } BENCHMARK_REGISTER_JSON(DomJsonBenchmark, JsonUtilsDeserializeToDocument) + BENCHMARK_DEFINE_F(DomJsonBenchmark, RapidjsonPayloadGeneration)(benchmark::State& state) + { + for (auto _ : state) + { + benchmark::DoNotOptimize(GenerateDomJsonBenchmarkPayload(state.range(0), state.range(1))); + } + + state.SetItemsProcessed(state.range(0) * state.range(0) * state.iterations()); + } + BENCHMARK_REGISTER_JSON(DomJsonBenchmark, RapidjsonPayloadGeneration) + #undef BENCHMARK_REGISTER_JSON } // namespace Benchmark diff --git a/Code/Framework/AzCore/Tests/DOM/DomValueBenchmarks.cpp b/Code/Framework/AzCore/Tests/DOM/DomValueBenchmarks.cpp index e69de29bb2..d11ef7f69e 100644 --- a/Code/Framework/AzCore/Tests/DOM/DomValueBenchmarks.cpp +++ b/Code/Framework/AzCore/Tests/DOM/DomValueBenchmarks.cpp @@ -0,0 +1,117 @@ +/* + * 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::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::ObjectType); + + 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::ObjectType); + entry.AddMember("string", createString(n)); + entry.AddMember("int", n); + entry.AddMember("double", static_cast(n) * 0.5); + entry.AddMember("bool", n % 2 == 0); + entry.AddMember("null", Value(Type::NullType)); + return entry; + }; + + auto createArray = [&]() -> Value + { + Value array(Type::ArrayType); + for (int i = 0; i < entryCount; ++i) + { + array.PushBack(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; + } + }; + + BENCHMARK_DEFINE_F(DomValueBenchmark, ValuePayloadGeneration)(benchmark::State& state) + { + for (auto _ : state) + { + benchmark::DoNotOptimize(GenerateDomBenchmarkPayload(state.range(0), state.range(1))); + } + + state.SetItemsProcessed(state.range(0) * state.range(0) * state.iterations()); + } + BENCHMARK_REGISTER_F(DomValueBenchmark, ValuePayloadGeneration) + ->Args({ 10, 5 }) + ->Args({ 10, 500 }) + ->Args({ 100, 5 }) + ->Args({ 100, 500 }) + ->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 index 84e2e16958..125f426e02 100644 --- a/Code/Framework/AzCore/Tests/DOM/DomValueTests.cpp +++ b/Code/Framework/AzCore/Tests/DOM/DomValueTests.cpp @@ -25,12 +25,14 @@ namespace AZ::Dom::Tests { UnitTest::AllocatorsFixture::SetUp(); NameDictionary::Create(); + AZ::AllocatorInstance::Create(); } void TearDown() override { m_value = Value(); + AZ::AllocatorInstance::Destroy(); NameDictionary::Destroy(); UnitTest::AllocatorsFixture::TearDown(); } @@ -279,19 +281,24 @@ namespace AZ::Dom::Tests 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 = "foo"; + AZStd::string stringToReference = s1; m_value["no_copy"] = Value(stringToReference, false); - AZStd::string stringToCopy = "bar"; + AZStd::string stringToCopy = s2; m_value["copy"] = Value(stringToCopy, true); EXPECT_EQ(m_value["no_copy"].GetType(), Type::StringType); - EXPECT_EQ(m_value["no_copy"].GetString(), stringToReference); - EXPECT_EQ(m_value["no_copy"].GetString().data(), stringToReference.data()); + 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::StringType); - EXPECT_EQ(m_value["copy"].GetString(), stringToCopy); - EXPECT_NE(m_value["copy"].GetString().data(), stringToCopy.data()); + EXPECT_EQ(m_value["copy"].GetString(), s2); + stringToCopy.at(0) = 'F'; + EXPECT_EQ(m_value["copy"].GetString(), s2); PerformValueChecks(); }