Files
UnrealEngineUWP/Engine/Source/Developer/DerivedDataCache/Private/Tests/HttpDerivedDataBackendTest.cpp
francis hurteau 86b3fd8de8 Refix crash in http request batch when the batch contains both a head and get request for the same cache keys
Added a test that could reliably reproduce the problem to validate it is properly fixed

#rb Danny.Couture, Zousar.Shaker
#preflight 610010b247c93a0001335319

#ROBOMERGE-OWNER: francis.hurteau
#ROBOMERGE-AUTHOR: francis.hurteau
#ROBOMERGE-SOURCE: CL 16968978 via CL 16972104
#ROBOMERGE-BOT: STARSHIP (Main -> Release-Engine-Test) (v838-16927207)

[CL 16972120 by francis hurteau in ue5-release-engine-test branch]
2021-07-27 15:25:39 -04:00

251 lines
7.5 KiB
C++

// Copyright Epic Games, Inc. All Rights Reserved.
#include "HttpDerivedDataBackend.h"
#include "Async/ParallelFor.h"
#include "DerivedDataBackendInterface.h"
#include "Misc/AutomationTest.h"
#include "Misc/SecureHash.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_LOG_CATEGORY_STATIC(LogHttpDerivedDataBackendTests, Log, All);
#define TEST_NAME_ROOT TEXT("System.DerivedDataCache.HttpDerivedDataBackend")
#define IMPLEMENT_HTTPDERIVEDDATA_AUTOMATION_TEST( TClass, PrettyName, TFlags ) \
IMPLEMENT_CUSTOM_COMPLEX_AUTOMATION_TEST(TClass, FHttpDerivedDataTestBase, TEST_NAME_ROOT PrettyName, TFlags) \
void TClass::GetTests(TArray<FString>& OutBeautifiedNames, TArray <FString>& OutTestCommands) const \
{ \
if (CheckPrequisites()) \
{ \
OutBeautifiedNames.Add(TEST_NAME_ROOT PrettyName); \
OutTestCommands.Add(FString()); \
} \
}
namespace HttpDerivedDataBackendTest
{
class FHttpDerivedDataTestBase : public FAutomationTestBase
{
public:
FHttpDerivedDataTestBase(const FString& InName, const bool bInComplexTask)
: FAutomationTestBase(InName, bInComplexTask)
{
}
bool CheckPrequisites() const
{
if (UE::DerivedData::Backends::FHttpDerivedDataBackend* Backend = GetTestBackend())
{
if (Backend->IsUsable())
{
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> TotalMS{ 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))
{
uint64 Before = FPlatformTime::Cycles64();
TestFunction();
uint64 Delta = FPlatformTime::Cycles64() - Before;
Requests++;
TotalMS += FPlatformTime::ToMilliseconds64(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: %llu, AvgLatency: %.02f ms, MaxLatency: %.02f s"), Requests.exchange(0), double(TotalMS) / TotalRequests, FPlatformTime::ToSeconds(MaxLatency));
}
else
{
UE_LOG(LogHttpDerivedDataBackendTests, Display, TEXT("RPS: %llu, AvgLatency: N/A, MaxLatency: %.02f s"), Requests.exchange(0), FPlatformTime::ToSeconds(MaxLatency));
}
}
LastEvent->Wait();
FPlatformProcess::ReturnSynchEventToPool(StartEvent);
FPlatformProcess::ReturnSynchEventToPool(LastEvent);
}
UE::DerivedData::Backends::FHttpDerivedDataBackend* GetTestBackend() const
{
static UE::DerivedData::Backends::FHttpDerivedDataBackend* CachedBackend = FetchTestBackend_Internal();
return CachedBackend;
}
private:
UE::DerivedData::Backends::FHttpDerivedDataBackend* FetchTestBackend_Internal() const
{
return UE::DerivedData::Backends::FHttpDerivedDataBackend::GetAny();
}
};
// Helper function to create a number of dummy cache keys for testing
TArray<FString> CreateTestCacheKeys(UE::DerivedData::Backends::FHttpDerivedDataBackend* InTestBackend, uint32 InNumKeys)
{
TArray<FString> Keys;
TArray<uint8> KeyContents;
KeyContents.Add(42);
FSHA1 HashState;
HashState.Update(KeyContents.GetData(), KeyContents.Num());
HashState.Final();
uint8 Hash[FSHA1::DigestSize];
HashState.GetHash(Hash);
const FString HashString = BytesToHex(Hash, FSHA1::DigestSize);
for (uint32 KeyIndex = 0; KeyIndex < InNumKeys; ++KeyIndex)
{
FString NewKey = FString::Printf(TEXT("__AutoTest_Dummy_%u__%s"), KeyIndex, *HashString);
Keys.Add(NewKey);
InTestBackend->PutCachedData(*NewKey, KeyContents, false);
}
return Keys;
}
IMPLEMENT_HTTPDERIVEDDATA_AUTOMATION_TEST(FConcurrentCachedDataProbablyExistsBatch, TEXT(".FConcurrentCachedDataProbablyExistsBatch"), EAutomationTestFlags::EditorContext | EAutomationTestFlags::ProductFilter)
bool FConcurrentCachedDataProbablyExistsBatch::RunTest(const FString& Parameters)
{
UE::DerivedData::Backends::FHttpDerivedDataBackend* TestBackend = GetTestBackend();
const int32 ThreadCount = 64;
const double Duration = 10;
const uint32 KeysInBatch = 4;
TArray<FString> Keys = CreateTestCacheKeys(TestBackend, KeysInBatch);
std::atomic<uint32> MismatchedResults = 0;
ConcurrentTestWithStats(
[&]()
{
TConstArrayView<FString> BatchView = MakeArrayView(Keys.GetData(), KeysInBatch);
TBitArray<> Result = TestBackend->CachedDataProbablyExistsBatch(BatchView);
if (Result.CountSetBits() != BatchView.Num())
{
MismatchedResults.fetch_add(BatchView.Num() - Result.CountSetBits(), std::memory_order_relaxed);
}
},
ThreadCount,
Duration
);
TestEqual(TEXT("Concurrent calls to CachedDataProbablyExistsBatch for a batch of keys that were put are not reliably found"), MismatchedResults, 0);
return true;
}
// This test validate that batch requests wont mismatch head and get request for the same keys in the same batch
IMPLEMENT_HTTPDERIVEDDATA_AUTOMATION_TEST(FConcurrentExistsAndGetForSameKeyBatch, TEXT(".FConcurrentExistsAndGetForSameKeyBatch"), EAutomationTestFlags::EditorContext | EAutomationTestFlags::ProductFilter)
bool FConcurrentExistsAndGetForSameKeyBatch::RunTest(const FString& Parameters)
{
UE::DerivedData::Backends::FHttpDerivedDataBackend* TestBackend = GetTestBackend();
const int32 ParallelTasks = 32;
const uint32 Iterations = 20;
const uint32 KeysInBatch = 4;
TArray<FString> Keys = CreateTestCacheKeys(TestBackend, KeysInBatch);
// Add some non valid keys by just using guids
for (int32 Index = 0; Index < KeysInBatch; ++Index)
{
Keys.Add(FGuid::NewGuid().ToString());
}
ParallelFor(ParallelTasks,
[&](int32 TaskIndex)
{
for (uint32 Iteration = 0; Iteration < Iterations; ++Iteration)
{
for (int32 KeyIndex = 0; KeyIndex < Keys.Num(); ++KeyIndex)
{
if ((Iteration % 2) ^ (KeyIndex % 2))
{
TestBackend->CachedDataProbablyExists(*Keys[KeyIndex]);
}
else
{
TArray<uint8> OutData;
TestBackend->GetCachedData(*Keys[KeyIndex], OutData);
}
}
}
}
);
return true;
}
}
#endif // #if WITH_DEV_AUTOMATION_TESTS && WITH_HTTP_DDC_BACKEND