// Copyright Epic Games, Inc. All Rights Reserved. #include "Async/ParallelFor.h" #include "DerivedDataBackendInterface.h" #include "DerivedDataCacheKey.h" #include "DerivedDataCacheRecord.h" #include "DerivedDataRequestOwner.h" #include "DerivedDataValue.h" #include "DerivedDataValueId.h" #include "Experimental/ZenServerInterface.h" #include "HAL/FileManager.h" #include "Memory/CompositeBuffer.h" #include "Misc/AutomationTest.h" #include "Misc/Paths.h" #include "Misc/SecureHash.h" #include "Serialization/CompactBinaryWriter.h" #include "ProfilingDebugging/ScopedTimers.h" // Test is targeted at HttpDerivedDataBackend but with some backend test interface it could be generalized // to function against all backends. #if WITH_DEV_AUTOMATION_TESTS && WITH_HTTP_DDC_BACKEND #define UE_HTTPCACHESTORETEST_USE_ZEN (PLATFORM_WINDOWS && UE_WITH_ZEN) DEFINE_LOG_CATEGORY_STATIC(LogHttpDerivedDataBackendTests, Log, All); #define TEST_NAME_ROOT "System.DerivedDataCache.HttpDerivedDataBackend" #define IMPLEMENT_HTTPDERIVEDDATA_AUTOMATION_TEST( TClass, PrettyName, TFlags ) \ IMPLEMENT_CUSTOM_COMPLEX_AUTOMATION_TEST(TClass, FHttpCacheStoreTestBase, TEXT(TEST_NAME_ROOT PrettyName), TFlags) \ void TClass::GetTests(TArray& OutBeautifiedNames, TArray & OutTestCommands) const \ { \ if (CheckPrequisites()) \ { \ OutBeautifiedNames.Add(TEXT(TEST_NAME_ROOT PrettyName)); \ OutTestCommands.Add(FString()); \ } \ } namespace UE::DerivedData { ILegacyCacheStore* GetAnyHttpCacheStore( FString& OutDomain, FString& OutAccessToken, FString& OutNamespace); ILegacyCacheStore* CreateZenCacheStore(const TCHAR* NodeName, const TCHAR* Config, ICacheStoreOwner* Owner); class FHttpCacheStoreTestBase : public FAutomationTestBase { public: FHttpCacheStoreTestBase(const FString& InName, const bool bInComplexTask) : FAutomationTestBase(InName, bInComplexTask) { } bool CheckPrequisites() const { if (ILegacyCacheStore* Backend = GetTestBackend()) { return true; } return false; } protected: void ConcurrentTestWithStats(TFunctionRef TestFunction, int32 ThreadCount, double Duration) { std::atomic Requests{ 0 }; std::atomic MaxLatency{ 0 }; std::atomic TotalCycles{ 0 }; std::atomic TotalRequests{ 0 }; FEvent* StartEvent = FPlatformProcess::GetSynchEventFromPool(true); FEvent* LastEvent = FPlatformProcess::GetSynchEventFromPool(true); std::atomic StopTime{ 0.0 }; std::atomic ActiveCount{ 0 }; for (int32 ThreadIndex = 0; ThreadIndex < ThreadCount; ++ThreadIndex) { ActiveCount++; Async( ThreadIndex < FTaskGraphInterface::Get().GetNumWorkerThreads() ? EAsyncExecution::TaskGraph : EAsyncExecution::Thread, [&]() { // No false start, wait until everyone is ready before starting the test StartEvent->Wait(); while (FPlatformTime::Seconds() < StopTime.load(std::memory_order_relaxed)) { const uint64 Before = FPlatformTime::Cycles64(); TestFunction(); const uint64 Delta = FPlatformTime::Cycles64() - Before; Requests++; TotalCycles += Delta; TotalRequests++; // Compare exchange loop until we either succeed to set the maximum value // or we bail out because we don't have the maximum value anymore. while (true) { uint64 Snapshot = MaxLatency.load(); if (Delta > Snapshot) { // Only do the exchange if the value has not changed since we confirmed // we had a bigger one. if (MaxLatency.compare_exchange_strong(Snapshot, Delta)) { // Exchange succeeded break; } } else { // We don't have the maximum break; } } } if (--ActiveCount == 0) { LastEvent->Trigger(); } } ); } StopTime = FPlatformTime::Seconds() + Duration; // GO! StartEvent->Trigger(); while (FPlatformTime::Seconds() < StopTime) { FPlatformProcess::Sleep(1.0f); if (TotalRequests) { UE_LOG(LogHttpDerivedDataBackendTests, Display, TEXT("RPS: %" UINT64_FMT ", AvgLatency: %.02f ms, MaxLatency: %.02f s"), Requests.exchange(0), FPlatformTime::ToMilliseconds64(TotalCycles) / double(TotalRequests), FPlatformTime::ToSeconds64(MaxLatency)); } else { UE_LOG(LogHttpDerivedDataBackendTests, Display, TEXT("RPS: %" UINT64_FMT ", AvgLatency: N/A, MaxLatency: %.02f s"), Requests.exchange(0), FPlatformTime::ToSeconds64(MaxLatency)); } } LastEvent->Wait(); FPlatformProcess::ReturnSynchEventToPool(StartEvent); FPlatformProcess::ReturnSynchEventToPool(LastEvent); } static ILegacyCacheStore* GetTestBackend() { static ILegacyCacheStore* CachedBackend = GetAnyHttpCacheStore( TestDomain, TestAccessToken, TestNamespace); return CachedBackend; } bool GetRecords(TConstArrayView Records, FCacheRecordPolicy Policy, TArray& OutRecords) { using namespace UE::DerivedData; ICacheStore* TestBackend = GetTestBackend(); TArray Requests; Requests.Reserve(Records.Num()); for (int32 RecordIndex = 0; RecordIndex < Records.Num(); ++RecordIndex) { const FCacheRecord& Record = Records[RecordIndex]; Requests.Add({ {TEXT("FHttpCacheStoreTestBase")}, Record.GetKey(), Policy, static_cast(RecordIndex) }); } struct FGetOutput { FCacheRecord Record; EStatus Status = EStatus::Error; }; TArray> GetOutputs; GetOutputs.SetNum(Records.Num()); FRequestOwner RequestOwner(EPriority::Blocking); TestBackend->Get(Requests, RequestOwner, [&GetOutputs](FCacheGetResponse&& Response) { FCacheRecordBuilder RecordBuilder(Response.Record.GetKey()); if (Response.Record.GetMeta()) { RecordBuilder.SetMeta(FCbObject::Clone(Response.Record.GetMeta())); } for (const FValueWithId& Value : Response.Record.GetValues()) { if (Value) { RecordBuilder.AddValue(Value); } } GetOutputs[int32(Response.UserData)].Emplace(FGetOutput{ RecordBuilder.Build(), Response.Status }); }); RequestOwner.Wait(); for (int32 RecordIndex = 0; RecordIndex < Records.Num(); ++RecordIndex) { FGetOutput& ReceivedOutput = GetOutputs[RecordIndex].GetValue(); if (ReceivedOutput.Status != EStatus::Ok) { return false; } OutRecords.Add(MoveTemp(ReceivedOutput.Record)); } return true; } bool GetValues(TConstArrayView Values, ECachePolicy Policy, TArray& OutValues, const char* BucketName = nullptr, ICacheStore* CacheStore = GetTestBackend()) { using namespace UE::DerivedData; ICacheStore* TestBackend = CacheStore; FCacheBucket TestCacheBucket(BucketName ? BucketName : "AutoTestDummy"); TArray Requests; Requests.Reserve(Values.Num()); for (int32 ValueIndex = 0; ValueIndex < Values.Num(); ++ValueIndex) { const FValue& Value = Values[ValueIndex]; FCacheKey Key; Key.Bucket = TestCacheBucket; Key.Hash = Value.GetRawHash(); Requests.Add({ {TEXT("FHttpCacheStoreTestBase")}, Key, Policy, static_cast(ValueIndex) }); } struct FGetValueOutput { FValue Value; EStatus Status = EStatus::Error; }; TArray> GetValueOutputs; GetValueOutputs.SetNum(Values.Num()); FRequestOwner RequestOwner(EPriority::Blocking); TestBackend->GetValue(Requests, RequestOwner, [&GetValueOutputs](FCacheGetValueResponse&& Response) { GetValueOutputs[int32(Response.UserData)].Emplace(FGetValueOutput{ Response.Value, Response.Status }); }); RequestOwner.Wait(); for (int32 ValueIndex = 0; ValueIndex < Values.Num(); ++ValueIndex) { FGetValueOutput& ReceivedOutput = GetValueOutputs[ValueIndex].GetValue(); if (ReceivedOutput.Status != EStatus::Ok) { return false; } OutValues.Add(MoveTemp(ReceivedOutput.Value)); } return true; } bool GetRecordChunks(TConstArrayView Records, FCacheRecordPolicy Policy, uint64 Offset, uint64 Size, TArray& OutChunks) { using namespace UE::DerivedData; ICacheStore* TestBackend = GetTestBackend(); TArray Requests; int32 OverallIndex = 0; for (int32 RecordIndex = 0; RecordIndex < Records.Num(); ++RecordIndex) { const FCacheRecord& Record = Records[RecordIndex]; TConstArrayView Values = Record.GetValues(); for (int32 ValueIndex = 0; ValueIndex < Values.Num(); ++ValueIndex) { const FValueWithId& Value = Values[ValueIndex]; Requests.Add({ {TEXT("FHttpCacheStoreTestBase")}, Record.GetKey(), Value.GetId(), Offset, Size, Value.GetRawHash(), Policy.GetValuePolicy(Value.GetId()), static_cast(OverallIndex) }); ++OverallIndex; } } struct FGetChunksOutput { FSharedBuffer Chunk; EStatus Status = EStatus::Error; }; TArray> GetOutputs; GetOutputs.SetNum(Requests.Num()); FRequestOwner RequestOwner(EPriority::Blocking); TestBackend->GetChunks(Requests, RequestOwner, [&GetOutputs](FCacheGetChunkResponse&& Response) { GetOutputs[int32(Response.UserData)].Emplace(FGetChunksOutput { Response.RawData, Response.Status }); }); RequestOwner.Wait(); for (int32 RequestIndex = 0; RequestIndex < Requests.Num(); ++RequestIndex) { FGetChunksOutput& ReceivedOutput = GetOutputs[RequestIndex].GetValue(); if (ReceivedOutput.Status != EStatus::Ok) { return false; } OutChunks.Add(MoveTemp(ReceivedOutput.Chunk)); } return true; } void ValidateRecords(const TCHAR* Name, TConstArrayView RecordsToTest, TConstArrayView ReferenceRecords, FCacheRecordPolicy Policy) { using namespace UE::DerivedData; if (!TestEqual(FString::Printf(TEXT("%s::Record quantity"), Name), RecordsToTest.Num(), ReferenceRecords.Num())) { return; } for (int32 RecordIndex = 0; RecordIndex < RecordsToTest.Num(); ++RecordIndex) { const FCacheRecord& ExpectedRecord = ReferenceRecords[RecordIndex]; const FCacheRecord& RecordToTest = RecordsToTest[RecordIndex]; if (EnumHasAnyFlags(Policy.GetRecordPolicy(), ECachePolicy::SkipMeta)) { TestTrue(FString::Printf(TEXT("%s::Get meta null"), Name), !RecordToTest.GetMeta()); } else { TestTrue(FString::Printf(TEXT("%s::Get meta equality"), Name), ExpectedRecord.GetMeta().Equals(RecordToTest.GetMeta())); } TestEqual(FString::Printf(TEXT("%s::Get value quantity"), Name), ExpectedRecord.GetValues().Num(), RecordToTest.GetValues().Num()); const TConstArrayView ExpectedValues = ExpectedRecord.GetValues(); const TConstArrayView ReceivedValues = RecordToTest.GetValues(); for (int32 ValueIndex = 0; ValueIndex < ExpectedValues.Num(); ++ValueIndex) { if (EnumHasAnyFlags(Policy.GetRecordPolicy(), ECachePolicy::SkipData)) { TestTrue(FString::Printf(TEXT("%s::Get value[%d] !HasData"), Name, ValueIndex), !ReceivedValues[ValueIndex].HasData()); } else { TestTrue(FString::Printf(TEXT("%s::Get value[%d] HasData"), Name, ValueIndex), ReceivedValues[ValueIndex].HasData()); TestTrue(FString::Printf(TEXT("%s::Get value[%d] equality"), Name, ValueIndex), ExpectedValues[ValueIndex] == ReceivedValues[ValueIndex]); TestTrue(FString::Printf(TEXT("%s::Get value[%d] data equality"), Name, ValueIndex), FIoHash::HashBuffer(ReceivedValues[ValueIndex].GetData().GetCompressed()) == FIoHash::HashBuffer(ExpectedValues[ValueIndex].GetData().GetCompressed())); } } } } void ValidateValues(const TCHAR* Name, TConstArrayView ValuesToTest, TConstArrayView ReferenceValues, ECachePolicy Policy) { using namespace UE::DerivedData; if (!TestEqual(FString::Printf(TEXT("%s::Value quantity"), Name), ValuesToTest.Num(), ReferenceValues.Num())) { return; } for (int32 ValueIndex = 0; ValueIndex < ValuesToTest.Num(); ++ValueIndex) { const FValue& ExpectedValue = ReferenceValues[ValueIndex]; const FValue& ValueToTest = ValuesToTest[ValueIndex]; if (EnumHasAnyFlags(Policy, ECachePolicy::SkipData)) { TestTrue(FString::Printf(TEXT("%s::Get value[%d] !HasData"), Name, ValueIndex), !ValueToTest.HasData()); } else { TestTrue(FString::Printf(TEXT("%s::Get value[%d] HasData"), Name, ValueIndex), ValueToTest.HasData()); TestTrue(FString::Printf(TEXT("%s::Get value[%d] equality"), Name, ValueIndex), ExpectedValue == ValueToTest); TestTrue(FString::Printf(TEXT("%s::Get value[%d] data equality"), Name, ValueIndex), FIoHash::HashBuffer(ValueToTest.GetData().GetCompressed()) == FIoHash::HashBuffer(ExpectedValue.GetData().GetCompressed())); } } } void ValidateRecordChunks(const TCHAR* Name, TConstArrayView RecordChunksToTest, TConstArrayView ReferenceRecords, FCacheRecordPolicy Policy, uint64 Offset, uint64 Size) { using namespace UE::DerivedData; int32 TotalChunks = 0; for (int32 RecordIndex = 0; RecordIndex < ReferenceRecords.Num(); ++RecordIndex) { const FCacheRecord& Record = ReferenceRecords[RecordIndex]; TConstArrayView Values = Record.GetValues(); for (int32 ValueIndex = 0; ValueIndex < Values.Num(); ++ValueIndex) { ++TotalChunks; } } if (!TestEqual(FString::Printf(TEXT("%s::Chunk quantity"), Name), RecordChunksToTest.Num(), TotalChunks)) { return; } int32 ChunkIndex = 0; for (int32 RecordIndex = 0; RecordIndex < ReferenceRecords.Num(); ++RecordIndex) { const FCacheRecord& ExpectedRecord = ReferenceRecords[RecordIndex]; const TConstArrayView ExpectedValues = ExpectedRecord.GetValues(); for (int32 ValueIndex = 0; ValueIndex < ExpectedValues.Num(); ++ValueIndex) { const FSharedBuffer& ChunkToTest = RecordChunksToTest[ChunkIndex]; if (EnumHasAnyFlags(Policy.GetRecordPolicy(), ECachePolicy::SkipData)) { TestTrue(FString::Printf(TEXT("%s::Get chunk[%d] IsNull"), Name, ChunkIndex), ChunkToTest.IsNull()); } else { FSharedBuffer ReferenceBuffer = ExpectedValues[ValueIndex].GetData().Decompress(); FMemoryView ReferenceView = ReferenceBuffer.GetView().Mid(Offset, Size); TestTrue(FString::Printf(TEXT("%s::Get chunk[%d] data equality"), Name, ChunkIndex), ReferenceView.EqualBytes(ChunkToTest.GetView())); } ++ChunkIndex; } } } TArray GetAndValidateRecords(const TCHAR* Name, TConstArrayView Records, FCacheRecordPolicy Policy) { using namespace UE::DerivedData; TArray ReceivedRecords; bool bGetSuccessful = GetRecords(Records, Policy, ReceivedRecords); TestTrue(FString::Printf(TEXT("%s::Get status"), Name), bGetSuccessful); if (!bGetSuccessful) { return TArray(); } ValidateRecords(Name, ReceivedRecords, Records, Policy); return ReceivedRecords; } TArray GetAndValidateValues(const TCHAR* Name, TConstArrayView Values, ECachePolicy Policy, ICacheStore* CacheStore = GetTestBackend()) { using namespace UE::DerivedData; TArray ReceivedValues; bool bGetSuccessful = GetValues(Values, Policy, ReceivedValues, nullptr, CacheStore); TestTrue(FString::Printf(TEXT("%s::Get status"), Name), bGetSuccessful); if (!bGetSuccessful) { return TArray(); } ValidateValues(Name, ReceivedValues, Values, Policy); return ReceivedValues; } TArray GetAndValidateRecordChunks(const TCHAR* Name, TConstArrayView Records, FCacheRecordPolicy Policy, uint64 Offset, uint64 Size) { using namespace UE::DerivedData; TArray ReceivedChunks; bool bGetSuccessful = GetRecordChunks(Records, Policy, Offset, Size, ReceivedChunks); TestTrue(FString::Printf(TEXT("%s::GetChunks status"), Name), bGetSuccessful); if (!bGetSuccessful) { return TArray(); } ValidateRecordChunks(Name, ReceivedChunks, Records, Policy, Offset, Size); return ReceivedChunks; } TArray GetAndValidateRecordsAndChunks(const TCHAR* Name, TConstArrayView Records, FCacheRecordPolicy Policy) { GetAndValidateRecordChunks(Name, Records, Policy, 5, 5); return GetAndValidateRecords(Name, Records, Policy); } protected: static inline FString TestDomain; static inline FString TestAccessToken; static inline FString TestNamespace; }; TArray CreateTestCacheRecords(ICacheStore* InTestBackend, uint32 InNumKeys, uint32 InNumValues, FCbObject MetaContents = FCbObject(), const char* BucketName = nullptr, uint8 Salt = 0) { using namespace UE::DerivedData; FCacheBucket TestCacheBucket(BucketName ? BucketName : "AutoTestDummy"); TArray CacheRecords; TArray PutRequests; PutRequests.Reserve(InNumKeys); const uint32 KeySalt = 1; for (uint32 KeyIndex = 0; KeyIndex < InNumKeys; ++KeyIndex) { FIoHashBuilder HashBuilder; HashBuilder.Update(&KeySalt,sizeof(KeySalt)); TArray Values; for (uint32 ValueIndex = 0; ValueIndex < InNumValues; ++ValueIndex) { TArray ValueContents; // Add N zeroed bytes where N corresponds to the value index times 10. const int32 NumBytes = (ValueIndex+1)*10; ValueContents.AddUninitialized(NumBytes); for (int32 ContentIndex = 0; ContentIndex < NumBytes; ++ContentIndex) { ValueContents[ContentIndex] = (uint8)(KeyIndex + ContentIndex + Salt); } Values.Emplace(MakeSharedBufferFromArray(MoveTemp(ValueContents))); HashBuilder.Update(Values.Last().GetView()); } if (MetaContents) { MetaContents.AppendHash(HashBuilder); } FCacheKey Key; Key.Bucket = TestCacheBucket; Key.Hash = HashBuilder.Finalize(); FCacheRecordBuilder RecordBuilder(Key); for (const FSharedBuffer& ValueBuffer : Values) { FIoHash ValueHash(FIoHash::HashBuffer(ValueBuffer)); RecordBuilder.AddValue(FValueId::FromHash(ValueHash), ValueBuffer); } if (MetaContents) { RecordBuilder.SetMeta(MoveTemp(MetaContents)); } PutRequests.Add({ {TEXT("AutoTest")}, RecordBuilder.Build(), ECachePolicy::Default, KeyIndex }); } FRequestOwner Owner(EPriority::Blocking); InTestBackend->Put(PutRequests, Owner, [&CacheRecords, &PutRequests] (FCachePutResponse&& Response) { check(Response.Status == EStatus::Ok); }); Owner.Wait(); CacheRecords.Reserve(PutRequests.Num()); for (const FCachePutRequest& PutRequest : PutRequests) { CacheRecords.Add(PutRequest.Record); } return CacheRecords; } TArray CreateTestCacheValues(ICacheStore* InTestBackend, uint32 InNumValues, const char* BucketName = nullptr, uint8 Salt = 1) { using namespace UE::DerivedData; FCacheBucket TestCacheBucket(BucketName ? BucketName : "AutoTestDummy"); TArray PutValueRequests; PutValueRequests.Reserve(InNumValues); TArray ValueBuffers; for (uint32 ValueIndex = 0; ValueIndex < InNumValues; ++ValueIndex) { TArray ValueContents; // Add N zeroed bytes where N corresponds to the value index times 10. const int32 NumBytes = (ValueIndex+1)*10; ValueContents.AddUninitialized(NumBytes); for (int32 ContentIndex = 0; ContentIndex < NumBytes; ++ContentIndex) { ValueContents[ContentIndex] = (uint8)(ValueIndex + ContentIndex + Salt); } ValueBuffers.Emplace(MakeSharedBufferFromArray(MoveTemp(ValueContents))); } uint64 KeyIndex = 0; for (const FSharedBuffer& ValueBuffer : ValueBuffers) { FIoHash ValueHash(FIoHash::HashBuffer(ValueBuffer)); FCacheKey Key; Key.Bucket = TestCacheBucket; Key.Hash = ValueHash; PutValueRequests.Add({ {TEXT("AutoTest")}, Key, FValue::Compress(ValueBuffer), ECachePolicy::Default, KeyIndex++ }); } TArray Values; FRequestOwner Owner(EPriority::Blocking); InTestBackend->PutValue(PutValueRequests, Owner, [&Values, &PutValueRequests](FCachePutValueResponse&& Response) { check(Response.Status == EStatus::Ok); }); Owner.Wait(); Values.Reserve(PutValueRequests.Num()); for (const FCachePutValueRequest& PutValueRequest : PutValueRequests) { Values.Add(PutValueRequest.Value); } return Values; } // Tests basic functionality for structured cache operations IMPLEMENT_HTTPDERIVEDDATA_AUTOMATION_TEST(FHttpCacheStoreTest, ".CacheStore", EAutomationTestFlags::EditorContext | EAutomationTestFlags::ProductFilter) bool FHttpCacheStoreTest::RunTest(const FString& Parameters) { using namespace UE::DerivedData; ILegacyCacheStore* TestBackend = GetTestBackend(); #if UE_HTTPCACHESTORETEST_USE_ZEN using namespace UE::Zen; FServiceSettings ZenUpstreamTestServiceSettings; FServiceAutoLaunchSettings& ZenUpstreamTestAutoLaunchSettings = ZenUpstreamTestServiceSettings.SettingsVariant.Get(); ZenUpstreamTestAutoLaunchSettings.DataPath = FPaths::ConvertRelativePathToFull(FPaths::Combine(FPaths::EngineSavedDir(), "ZenUpstreamUnitTest")); ZenUpstreamTestAutoLaunchSettings.ExtraArgs = FString::Printf(TEXT("--http asio --upstream-jupiter-url \"%s\" --upstream-jupiter-token \"%s\" --upstream-jupiter-namespace \"%s\""), *TestDomain, *TestAccessToken, *TestNamespace ); ZenUpstreamTestAutoLaunchSettings.DesiredPort = 23337; // Avoid the normal default port ZenUpstreamTestAutoLaunchSettings.bShowConsole = true; ZenUpstreamTestAutoLaunchSettings.bLimitProcessLifetime = true; FScopeZenService ScopeZenUpstreamService(MoveTemp(ZenUpstreamTestServiceSettings)); IFileManager::Get().DeleteDirectory(*FPaths::Combine(FPaths::EngineSavedDir(), "ZenUpstreamSiblingUnitTest"), false, true); FServiceSettings ZenUpstreamSiblingTestServiceSettings; FServiceAutoLaunchSettings& ZenUpstreamSiblingTestAutoLaunchSettings = ZenUpstreamSiblingTestServiceSettings.SettingsVariant.Get(); ZenUpstreamSiblingTestAutoLaunchSettings.DataPath = FPaths::ConvertRelativePathToFull(FPaths::Combine(FPaths::EngineSavedDir(), "ZenUpstreamSiblingUnitTest")); ZenUpstreamSiblingTestAutoLaunchSettings.ExtraArgs = FString::Printf(TEXT("--http asio --upstream-jupiter-url \"%s\" --upstream-jupiter-token \"%s\" --upstream-jupiter-namespace \"%s\""), *TestDomain, *TestAccessToken, *TestNamespace ); ZenUpstreamSiblingTestAutoLaunchSettings.DesiredPort = 23338; // Avoid the normal default port ZenUpstreamSiblingTestAutoLaunchSettings.bShowConsole = true; ZenUpstreamSiblingTestAutoLaunchSettings.bLimitProcessLifetime = true; FScopeZenService ScopeZenUpstreamSiblingService(MoveTemp(ZenUpstreamSiblingTestServiceSettings)); FServiceSettings ZenTestServiceSettings; FServiceAutoLaunchSettings& ZenTestAutoLaunchSettings = ZenTestServiceSettings.SettingsVariant.Get(); ZenTestAutoLaunchSettings.DataPath = FPaths::ConvertRelativePathToFull(FPaths::Combine(FPaths::EngineSavedDir(), "ZenUnitTest")); ZenTestAutoLaunchSettings.ExtraArgs = FString::Printf(TEXT("--http asio --upstream-zen-url \"http://localhost:%d\""), ScopeZenUpstreamService.GetInstance().GetEndpoint().GetPort() ); ZenTestAutoLaunchSettings.DesiredPort = 13337; // Avoid the normal default port ZenTestAutoLaunchSettings.bShowConsole = true; ZenTestAutoLaunchSettings.bLimitProcessLifetime = true; IFileManager::Get().DeleteDirectory(*FPaths::Combine(FPaths::EngineSavedDir(), "ZenUnitTestSibling"), false, true); FServiceSettings ZenTestServiceSiblingSettings; FServiceAutoLaunchSettings& ZenTestSiblingAutoLaunchSettings = ZenTestServiceSiblingSettings.SettingsVariant.Get(); ZenTestSiblingAutoLaunchSettings.DataPath = FPaths::ConvertRelativePathToFull(FPaths::Combine(FPaths::EngineSavedDir(), "ZenUnitTestSibling")); ZenTestSiblingAutoLaunchSettings.ExtraArgs = FString::Printf(TEXT("--http asio --upstream-zen-url \"http://localhost:%d\""), ScopeZenUpstreamSiblingService.GetInstance().GetEndpoint().GetPort() ); ZenTestSiblingAutoLaunchSettings.DesiredPort = 13338; // Avoid the normal default port ZenTestSiblingAutoLaunchSettings.bShowConsole = true; ZenTestSiblingAutoLaunchSettings.bLimitProcessLifetime = true; FScopeZenService ScopeZenSiblingService(MoveTemp(ZenTestServiceSiblingSettings)); TUniquePtr ZenIntermediarySiblingBackend(CreateZenCacheStore(TEXT("TestSibling"), *FString::Printf(TEXT("Host=%s, StructuredNamespace=%s"), *ScopeZenSiblingService.GetInstance().GetEndpoint().GetURL(), *TestNamespace), nullptr)); FScopeZenService ScopeZenService(MoveTemp(ZenTestServiceSettings)); TUniquePtr ZenIntermediaryBackend(CreateZenCacheStore(TEXT("Test"), *FString::Printf(TEXT("Host=%s, StructuredNamespace=%s"), *ScopeZenService.GetInstance().GetEndpoint().GetURL(), *TestNamespace), nullptr)); auto WaitForZenPushToUpstream = [](ILegacyCacheStore* ZenBackend, TConstArrayView Records) { // TODO: Expecting a legitimate means to wait for zen to finish pushing records to its upstream in the future FPlatformProcess::Sleep(1.0f); }; auto WaitForZenPushValuesToUpstream = [](ILegacyCacheStore* ZenBackend, TConstArrayView Values) { // TODO: Expecting a legitimate means to wait for zen to finish pushing records to its upstream in the future FPlatformProcess::Sleep(1.0f); }; #endif // UE_HTTPCACHESTORETEST_USE_ZEN const uint32 RecordsInBatch = 3; const uint32 ValuesInBatch = RecordsInBatch; { TArray PutRecords = CreateTestCacheRecords(TestBackend, RecordsInBatch, 1); TArray RecievedRecords = GetAndValidateRecordsAndChunks(TEXT("SimpleValue"), PutRecords, ECachePolicy::Default); TArray RecievedRecordsSkipMeta = GetAndValidateRecordsAndChunks(TEXT("SimpleValueSkipMeta"), PutRecords, ECachePolicy::Default | ECachePolicy::SkipMeta); TArray RecievedRecordsSkipData = GetAndValidateRecordsAndChunks(TEXT("SimpleValueSkipData"), PutRecords, ECachePolicy::Default | ECachePolicy::SkipData); #if UE_HTTPCACHESTORETEST_USE_ZEN if (ZenIntermediaryBackend) { TArray PutRecordsZen = CreateTestCacheRecords(ZenIntermediaryBackend.Get(), RecordsInBatch, 1, FCbObject(), "AutoTestDummyZen"); WaitForZenPushToUpstream(ZenIntermediaryBackend.Get(), PutRecordsZen); ValidateRecords(TEXT("SimpleValueZenAndDirect"), GetAndValidateRecords(TEXT("SimpleValueZen"), PutRecordsZen, ECachePolicy::Default), RecievedRecords, ECachePolicy::Default); ValidateRecords(TEXT("SimpleValueSkipMetaZenAndDirect"), GetAndValidateRecords(TEXT("SimpleValueSkipMetaZen"), PutRecordsZen, ECachePolicy::Default | ECachePolicy::SkipMeta), RecievedRecordsSkipMeta, ECachePolicy::Default | ECachePolicy::SkipMeta); ValidateRecords(TEXT("SimpleValueSkipDataZenAndDirect"), GetAndValidateRecords(TEXT("SimpleValueSkipDataZen"), PutRecordsZen, ECachePolicy::Default | ECachePolicy::SkipData), RecievedRecordsSkipData, ECachePolicy::Default | ECachePolicy::SkipData); } #endif // UE_HTTPCACHESTORETEST_USE_ZEN } { TCbWriter<64> MetaWriter; MetaWriter.BeginObject(); MetaWriter.AddInteger(ANSITEXTVIEW("MetaKey"), 42); MetaWriter.EndObject(); FCbObject MetaObject = MetaWriter.Save().AsObject(); TArray PutRecords = CreateTestCacheRecords(TestBackend, RecordsInBatch, 1, MetaObject); TArray RecievedRecords = GetAndValidateRecordsAndChunks(TEXT("SimpleValueWithMeta"), PutRecords, ECachePolicy::Default); TArray RecievedRecordsSkipMeta = GetAndValidateRecordsAndChunks(TEXT("SimpleValueWithMetaSkipMeta"), PutRecords, ECachePolicy::Default | ECachePolicy::SkipMeta); TArray RecievedRecordsSkipData = GetAndValidateRecordsAndChunks(TEXT("SimpleValueWithMetaSkipData"), PutRecords, ECachePolicy::Default | ECachePolicy::SkipData); #if UE_HTTPCACHESTORETEST_USE_ZEN if (ZenIntermediaryBackend) { TArray PutRecordsZen = CreateTestCacheRecords(ZenIntermediaryBackend.Get(), RecordsInBatch, 1, MetaObject, "AutoTestDummyZen"); WaitForZenPushToUpstream(ZenIntermediaryBackend.Get(), PutRecordsZen); ValidateRecords(TEXT("SimpleValueWithMetaZenAndDirect"), GetAndValidateRecords(TEXT("SimpleValueWithMetaZen"), PutRecordsZen, ECachePolicy::Default), RecievedRecords, ECachePolicy::Default); ValidateRecords(TEXT("SimpleValueWithMetaSkipMetaZenAndDirect"), GetAndValidateRecords(TEXT("SimpleValueWithMetaSkipMetaZen"), PutRecordsZen, ECachePolicy::Default | ECachePolicy::SkipMeta), RecievedRecordsSkipMeta, ECachePolicy::Default | ECachePolicy::SkipMeta); ValidateRecords(TEXT("SimpleValueWithMetaSkipDataZenAndDirect"), GetAndValidateRecords(TEXT("SimpleValueWithMetaSkipDataZen"), PutRecordsZen, ECachePolicy::Default | ECachePolicy::SkipData), RecievedRecordsSkipData, ECachePolicy::Default | ECachePolicy::SkipData); } #endif // UE_HTTPCACHESTORETEST_USE_ZEN } { TArray PutRecords = CreateTestCacheRecords(TestBackend, RecordsInBatch, 5); TArray RecievedRecords = GetAndValidateRecordsAndChunks(TEXT("MultiValue"), PutRecords, ECachePolicy::Default); TArray RecievedRecordsSkipMeta = GetAndValidateRecordsAndChunks(TEXT("MultiValueSkipMeta"), PutRecords, ECachePolicy::Default | ECachePolicy::SkipMeta); TArray RecievedRecordsSkipData = GetAndValidateRecordsAndChunks(TEXT("MultiValueSkipData"), PutRecords, ECachePolicy::Default | ECachePolicy::SkipData); #if UE_HTTPCACHESTORETEST_USE_ZEN if (ZenIntermediaryBackend) { TArray PutRecordsZen = CreateTestCacheRecords(ZenIntermediaryBackend.Get(), RecordsInBatch, 5, FCbObject(), "AutoTestDummyZen"); WaitForZenPushToUpstream(ZenIntermediaryBackend.Get(), PutRecordsZen); ValidateRecords(TEXT("MultiValueZenAndDirect"), GetAndValidateRecords(TEXT("MultiValueZen"), PutRecordsZen, ECachePolicy::Default), RecievedRecords, ECachePolicy::Default); ValidateRecords(TEXT("MultiValueSkipMetaZenAndDirect"), GetAndValidateRecords(TEXT("MultiValueSkipMetaZen"), PutRecordsZen, ECachePolicy::Default | ECachePolicy::SkipMeta), RecievedRecordsSkipMeta, ECachePolicy::Default | ECachePolicy::SkipMeta); ValidateRecords(TEXT("MultiValueSkipDataZenAndDirect"), GetAndValidateRecords(TEXT("MultiValueSkipDataZen"), PutRecordsZen, ECachePolicy::Default | ECachePolicy::SkipData), RecievedRecordsSkipData, ECachePolicy::Default | ECachePolicy::SkipData); } #endif // UE_HTTPCACHESTORETEST_USE_ZEN } { TArray PutValues = CreateTestCacheValues(TestBackend, ValuesInBatch); TArray ReceivedValues = GetAndValidateValues(TEXT("SimpleValue"), PutValues, ECachePolicy::Default); TArray ReceivedValuesSkipData = GetAndValidateValues(TEXT("SimpleValueSkipData"), PutValues, ECachePolicy::Default | ECachePolicy::SkipData); #if UE_HTTPCACHESTORETEST_USE_ZEN if (ZenIntermediaryBackend) { TArray PutValuesZen = CreateTestCacheValues(ZenIntermediaryBackend.Get(), ValuesInBatch); WaitForZenPushValuesToUpstream(ZenIntermediaryBackend.Get(), PutValuesZen); ValidateValues(TEXT("SimpleValueZenAndDirect"), GetAndValidateValues(TEXT("SimpleValueZen"), PutValuesZen, ECachePolicy::Default), ReceivedValues, ECachePolicy::Default); ValidateValues(TEXT("SimpleValueSkipDataZenAndDirect"), GetAndValidateValues(TEXT("SimpleValueSkipDataZen"), PutValuesZen, ECachePolicy::Default | ECachePolicy::SkipData), ReceivedValuesSkipData, ECachePolicy::Default | ECachePolicy::SkipData); } // Disabled because we have no guarantee that the upstream server received the put in any particular timemframe. //if (ZenIntermediarySiblingBackend) //{ // GetAndValidateValues(TEXT("SimpleValueZen"), PutValues, ECachePolicy::Default, ZenIntermediarySiblingBackend.Get()); // GetAndValidateValues(TEXT("SimpleValueSkipDataZen"), PutValues, ECachePolicy::Default | ECachePolicy::SkipData, ZenIntermediarySiblingBackend.Get()); //} #endif // UE_HTTPCACHESTORETEST_USE_ZEN } return true; } static void PutTestRecords(ILegacyCacheStore* TestBackend, uint32 NumRecords, bool bSynchronous = false, TArray* OutKeys = nullptr) { FCacheBucket TestCacheBucket("AutoTestDummy"); const int64 RunUniqueId = FDateTime::Now().GetTicks(); TArray PutRequests; PutRequests.Reserve(NumRecords); TArray ValueBuffers; for (uint32 ValueIndex = 0; ValueIndex < NumRecords; ++ValueIndex) { TArray ValueContents; const int32 NumBytes = 12; ValueContents.AddUninitialized(NumBytes); *reinterpret_cast(ValueContents.GetData()) = RunUniqueId; *(reinterpret_cast(ValueContents.GetData()) + 2) = ValueIndex; ValueBuffers.Emplace(MakeSharedBufferFromArray(MoveTemp(ValueContents))); } if (OutKeys) { OutKeys->Reserve(OutKeys->Num() + NumRecords); } uint64 KeyIndex = 0; for (const FSharedBuffer& ValueBuffer : ValueBuffers) { FIoHash ValueHash(FIoHash::HashBuffer(ValueBuffer)); FCacheKey Key; Key.Bucket = TestCacheBucket; Key.Hash = ValueHash; if (OutKeys) { OutKeys->Add(Key); } FCacheRecordBuilder RecordBuilder(Key); RecordBuilder.AddValue(FValueId::FromHash(ValueHash), ValueBuffer); PutRequests.Add({ {TEXT("AutoTestStressPutRecord")}, RecordBuilder.Build(), ECachePolicy::Default, KeyIndex }); } FRequestOwner RequestOwner(bSynchronous ? EPriority::Blocking : EPriority::Normal); { FRequestBarrier RequestBarrier(RequestOwner); if (!bSynchronous) { RequestOwner.KeepAlive(); } TestBackend->Put(PutRequests, RequestOwner, [](FCachePutResponse&& Response) { check(Response.Status == EStatus::Ok); }); } if (bSynchronous) { RequestOwner.Wait(); } } // Stress test Put operations IMPLEMENT_HTTPDERIVEDDATA_AUTOMATION_TEST(FHttpCacheStoreStressPutTest, ".CacheStoreStressPut", EAutomationTestFlags::EditorContext | EAutomationTestFlags::StressFilter) bool FHttpCacheStoreStressPutTest::RunTest(const FString& Parameters) { using namespace UE::DerivedData; ILegacyCacheStore* TestBackend = GetTestBackend(); constexpr uint32 NumRecords = 1000; { FAutoScopedDurationTimer AutoTimer; PutTestRecords(TestBackend, NumRecords, true); UE_LOG(LogDerivedDataCache, Display, TEXT("Putting %u records (containing values of size 12 bytes): %.2f s"), NumRecords, AutoTimer.GetTime()); } return true; } } // UE::DerivedData #endif // #if WITH_DEV_AUTOMATION_TESTS && WITH_HTTP_DDC_BACKEND