Files
UnrealEngine/Engine/Source/Developer/DerivedDataCache/Private/Tests/HttpCacheStoreTest.cpp
2025-05-18 13:04:45 +08:00

861 lines
34 KiB
C++

// 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<FString>& OutBeautifiedNames, TArray <FString>& 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<void()> TestFunction, int32 ThreadCount, double Duration)
{
std::atomic<uint64> Requests{ 0 };
std::atomic<uint64> MaxLatency{ 0 };
std::atomic<uint64> TotalCycles{ 0 };
std::atomic<uint64> TotalRequests{ 0 };
FEvent* StartEvent = FPlatformProcess::GetSynchEventFromPool(true);
FEvent* LastEvent = FPlatformProcess::GetSynchEventFromPool(true);
std::atomic<double> StopTime{ 0.0 };
std::atomic<uint64> 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<FCacheRecord> Records, FCacheRecordPolicy Policy, TArray<FCacheRecord>& OutRecords)
{
using namespace UE::DerivedData;
ICacheStore* TestBackend = GetTestBackend();
TArray<FCacheGetRequest> 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<uint64>(RecordIndex) });
}
struct FGetOutput
{
FCacheRecord Record;
EStatus Status = EStatus::Error;
};
TArray<TOptional<FGetOutput>> 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<FValue> Values, ECachePolicy Policy, TArray<FValue>& OutValues, const char* BucketName = nullptr, ICacheStore* CacheStore = GetTestBackend())
{
using namespace UE::DerivedData;
ICacheStore* TestBackend = CacheStore;
FCacheBucket TestCacheBucket(BucketName ? BucketName : "AutoTestDummy");
TArray<FCacheGetValueRequest> 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<uint64>(ValueIndex) });
}
struct FGetValueOutput
{
FValue Value;
EStatus Status = EStatus::Error;
};
TArray<TOptional<FGetValueOutput>> 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<FCacheRecord> Records, FCacheRecordPolicy Policy, uint64 Offset, uint64 Size, TArray<FSharedBuffer>& OutChunks)
{
using namespace UE::DerivedData;
ICacheStore* TestBackend = GetTestBackend();
TArray<FCacheGetChunkRequest> Requests;
int32 OverallIndex = 0;
for (int32 RecordIndex = 0; RecordIndex < Records.Num(); ++RecordIndex)
{
const FCacheRecord& Record = Records[RecordIndex];
TConstArrayView<FValueWithId> 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<uint64>(OverallIndex) });
++OverallIndex;
}
}
struct FGetChunksOutput
{
FSharedBuffer Chunk;
EStatus Status = EStatus::Error;
};
TArray<TOptional<FGetChunksOutput>> 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<FCacheRecord> RecordsToTest, TConstArrayView<FCacheRecord> 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<FValueWithId> ExpectedValues = ExpectedRecord.GetValues();
const TConstArrayView<FValueWithId> 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<FValue> ValuesToTest, TConstArrayView<FValue> 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<FSharedBuffer> RecordChunksToTest, TConstArrayView<FCacheRecord> 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<FValueWithId> 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<FValueWithId> 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<FCacheRecord> GetAndValidateRecords(const TCHAR* Name, TConstArrayView<FCacheRecord> Records, FCacheRecordPolicy Policy)
{
using namespace UE::DerivedData;
TArray<FCacheRecord> ReceivedRecords;
bool bGetSuccessful = GetRecords(Records, Policy, ReceivedRecords);
TestTrue(FString::Printf(TEXT("%s::Get status"), Name), bGetSuccessful);
if (!bGetSuccessful)
{
return TArray<FCacheRecord>();
}
ValidateRecords(Name, ReceivedRecords, Records, Policy);
return ReceivedRecords;
}
TArray<FValue> GetAndValidateValues(const TCHAR* Name, TConstArrayView<FValue> Values, ECachePolicy Policy, ICacheStore* CacheStore = GetTestBackend())
{
using namespace UE::DerivedData;
TArray<FValue> ReceivedValues;
bool bGetSuccessful = GetValues(Values, Policy, ReceivedValues, nullptr, CacheStore);
TestTrue(FString::Printf(TEXT("%s::Get status"), Name), bGetSuccessful);
if (!bGetSuccessful)
{
return TArray<FValue>();
}
ValidateValues(Name, ReceivedValues, Values, Policy);
return ReceivedValues;
}
TArray<FSharedBuffer> GetAndValidateRecordChunks(const TCHAR* Name, TConstArrayView<FCacheRecord> Records, FCacheRecordPolicy Policy, uint64 Offset, uint64 Size)
{
using namespace UE::DerivedData;
TArray<FSharedBuffer> ReceivedChunks;
bool bGetSuccessful = GetRecordChunks(Records, Policy, Offset, Size, ReceivedChunks);
TestTrue(FString::Printf(TEXT("%s::GetChunks status"), Name), bGetSuccessful);
if (!bGetSuccessful)
{
return TArray<FSharedBuffer>();
}
ValidateRecordChunks(Name, ReceivedChunks, Records, Policy, Offset, Size);
return ReceivedChunks;
}
TArray<FCacheRecord> GetAndValidateRecordsAndChunks(const TCHAR* Name, TConstArrayView<FCacheRecord> 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<FCacheRecord> 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<FCacheRecord> CacheRecords;
TArray<FCachePutRequest> PutRequests;
PutRequests.Reserve(InNumKeys);
const uint32 KeySalt = 1;
for (uint32 KeyIndex = 0; KeyIndex < InNumKeys; ++KeyIndex)
{
FIoHashBuilder HashBuilder;
HashBuilder.Update(&KeySalt,sizeof(KeySalt));
TArray<FSharedBuffer> Values;
for (uint32 ValueIndex = 0; ValueIndex < InNumValues; ++ValueIndex)
{
TArray<uint8> 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<FValue> CreateTestCacheValues(ICacheStore* InTestBackend, uint32 InNumValues, const char* BucketName = nullptr, uint8 Salt = 1)
{
using namespace UE::DerivedData;
FCacheBucket TestCacheBucket(BucketName ? BucketName : "AutoTestDummy");
TArray<FCachePutValueRequest> PutValueRequests;
PutValueRequests.Reserve(InNumValues);
TArray<FSharedBuffer> ValueBuffers;
for (uint32 ValueIndex = 0; ValueIndex < InNumValues; ++ValueIndex)
{
TArray<uint8> 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<FValue> 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<FServiceAutoLaunchSettings>();
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<FServiceAutoLaunchSettings>();
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<FServiceAutoLaunchSettings>();
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<FServiceAutoLaunchSettings>();
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<ILegacyCacheStore> ZenIntermediarySiblingBackend(CreateZenCacheStore(TEXT("TestSibling"),
*FString::Printf(TEXT("Host=%s, StructuredNamespace=%s"), *ScopeZenSiblingService.GetInstance().GetEndpoint().GetURL(), *TestNamespace), nullptr));
FScopeZenService ScopeZenService(MoveTemp(ZenTestServiceSettings));
TUniquePtr<ILegacyCacheStore> ZenIntermediaryBackend(CreateZenCacheStore(TEXT("Test"),
*FString::Printf(TEXT("Host=%s, StructuredNamespace=%s"), *ScopeZenService.GetInstance().GetEndpoint().GetURL(), *TestNamespace), nullptr));
auto WaitForZenPushToUpstream = [](ILegacyCacheStore* ZenBackend, TConstArrayView<FCacheRecord> 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<FValue> 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<FCacheRecord> PutRecords = CreateTestCacheRecords(TestBackend, RecordsInBatch, 1);
TArray<FCacheRecord> RecievedRecords = GetAndValidateRecordsAndChunks(TEXT("SimpleValue"), PutRecords, ECachePolicy::Default);
TArray<FCacheRecord> RecievedRecordsSkipMeta = GetAndValidateRecordsAndChunks(TEXT("SimpleValueSkipMeta"), PutRecords, ECachePolicy::Default | ECachePolicy::SkipMeta);
TArray<FCacheRecord> RecievedRecordsSkipData = GetAndValidateRecordsAndChunks(TEXT("SimpleValueSkipData"), PutRecords, ECachePolicy::Default | ECachePolicy::SkipData);
#if UE_HTTPCACHESTORETEST_USE_ZEN
if (ZenIntermediaryBackend)
{
TArray<FCacheRecord> 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<FCacheRecord> PutRecords = CreateTestCacheRecords(TestBackend, RecordsInBatch, 1, MetaObject);
TArray<FCacheRecord> RecievedRecords = GetAndValidateRecordsAndChunks(TEXT("SimpleValueWithMeta"), PutRecords, ECachePolicy::Default);
TArray<FCacheRecord> RecievedRecordsSkipMeta = GetAndValidateRecordsAndChunks(TEXT("SimpleValueWithMetaSkipMeta"), PutRecords, ECachePolicy::Default | ECachePolicy::SkipMeta);
TArray<FCacheRecord> RecievedRecordsSkipData = GetAndValidateRecordsAndChunks(TEXT("SimpleValueWithMetaSkipData"), PutRecords, ECachePolicy::Default | ECachePolicy::SkipData);
#if UE_HTTPCACHESTORETEST_USE_ZEN
if (ZenIntermediaryBackend)
{
TArray<FCacheRecord> 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<FCacheRecord> PutRecords = CreateTestCacheRecords(TestBackend, RecordsInBatch, 5);
TArray<FCacheRecord> RecievedRecords = GetAndValidateRecordsAndChunks(TEXT("MultiValue"), PutRecords, ECachePolicy::Default);
TArray<FCacheRecord> RecievedRecordsSkipMeta = GetAndValidateRecordsAndChunks(TEXT("MultiValueSkipMeta"), PutRecords, ECachePolicy::Default | ECachePolicy::SkipMeta);
TArray<FCacheRecord> RecievedRecordsSkipData = GetAndValidateRecordsAndChunks(TEXT("MultiValueSkipData"), PutRecords, ECachePolicy::Default | ECachePolicy::SkipData);
#if UE_HTTPCACHESTORETEST_USE_ZEN
if (ZenIntermediaryBackend)
{
TArray<FCacheRecord> 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<FValue> PutValues = CreateTestCacheValues(TestBackend, ValuesInBatch);
TArray<FValue> ReceivedValues = GetAndValidateValues(TEXT("SimpleValue"), PutValues, ECachePolicy::Default);
TArray<FValue> ReceivedValuesSkipData = GetAndValidateValues(TEXT("SimpleValueSkipData"), PutValues, ECachePolicy::Default | ECachePolicy::SkipData);
#if UE_HTTPCACHESTORETEST_USE_ZEN
if (ZenIntermediaryBackend)
{
TArray<FValue> 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<FCacheKey>* OutKeys = nullptr)
{
FCacheBucket TestCacheBucket("AutoTestDummy");
const int64 RunUniqueId = FDateTime::Now().GetTicks();
TArray<FCachePutRequest> PutRequests;
PutRequests.Reserve(NumRecords);
TArray<FSharedBuffer> ValueBuffers;
for (uint32 ValueIndex = 0; ValueIndex < NumRecords; ++ValueIndex)
{
TArray<uint8> ValueContents;
const int32 NumBytes = 12;
ValueContents.AddUninitialized(NumBytes);
*reinterpret_cast<int64*>(ValueContents.GetData()) = RunUniqueId;
*(reinterpret_cast<uint32*>(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