Files
UnrealEngineUWP/Engine/Source/Developer/DerivedDataCache/Private/MemoryDerivedDataBackend.cpp
josh jensen bdc97a5f87 Fix a DDC issue where an early abort due to lack of memory cache space causes the disk not to be searched
This manifested itself in a game when textures ended up rebuilding every single run of the game but suddenly stopped building and coming from the DDC as they should have after the first run.

When a texture was being requested from the DDC, the in-memory DDC cache space ran out, and FMemoryDerivedDataBackend::CachedDataProbablyExists() returned true. FDerivedDataBackendAsyncPutWrapper::PutCachedData() assumes the data is already on its way, so it doesn't send it again and exits the function. Unfortunately, the data is not really on the disk, and FCachePutAsyncWorker never gets a chance to put it there.

Because of changing memory requirements from run to run, this game was eventually able to write all of the texture data to disk, but it took dozens of runs to do so, as it generally would only write a single mip from a mipchain in any given run. When all of the mips were finally written, the texture would be fully retrieved from the DDC, and no build would be necessary.

With this fix, no early abort is had, and all textures write themselves fully to the disk.

#rb Jack.Porter


#ROBOMERGE-SOURCE: CL 6345014 via CL 6346145

[CL 6346238 by josh jensen in Main branch]
2019-05-07 16:46:05 -04:00

286 lines
7.7 KiB
C++

// Copyright 1998-2019 Epic Games, Inc. All Rights Reserved.
#include "MemoryDerivedDataBackend.h"
#include "Templates/UniquePtr.h"
FMemoryDerivedDataBackend::FMemoryDerivedDataBackend(int64 InMaxCacheSize)
: MaxCacheSize(InMaxCacheSize)
, bDisabled( false )
, CurrentCacheSize( SerializationSpecificDataSize )
, bMaxSizeExceeded(false)
{
}
FMemoryDerivedDataBackend::~FMemoryDerivedDataBackend()
{
Disable();
}
bool FMemoryDerivedDataBackend::IsWritable()
{
FScopeLock ScopeLock(&SynchronizationObject);
return !bDisabled;
}
bool FMemoryDerivedDataBackend::CachedDataProbablyExists(const TCHAR* CacheKey)
{
COOK_STAT(auto Timer = UsageStats.TimeProbablyExists());
FScopeLock ScopeLock(&SynchronizationObject);
if (bDisabled)
{
return false;
}
bool Result = CacheItems.Contains(FString(CacheKey));
if (Result)
{
COOK_STAT(Timer.AddHit(0));
}
return Result;
}
bool FMemoryDerivedDataBackend::GetCachedData(const TCHAR* CacheKey, TArray<uint8>& OutData)
{
COOK_STAT(auto Timer = UsageStats.TimeGet());
FScopeLock ScopeLock(&SynchronizationObject);
if (!bDisabled)
{
FCacheValue* Item = CacheItems.FindRef(FString(CacheKey));
if (Item)
{
OutData = Item->Data;
Item->Age = 0;
check(OutData.Num());
COOK_STAT(Timer.AddHit(OutData.Num()));
return true;
}
}
OutData.Empty();
return false;
}
void FMemoryDerivedDataBackend::PutCachedData(const TCHAR* CacheKey, TArray<uint8>& InData, bool bPutEvenIfExists)
{
COOK_STAT(auto Timer = UsageStats.TimePut());
FScopeLock ScopeLock(&SynchronizationObject);
if (bDisabled || bMaxSizeExceeded)
{
return;
}
FString Key(CacheKey);
FCacheValue* Item = CacheItems.FindRef(FString(CacheKey));
if (Item)
{
//check(Item->Data == InData); // any second attempt to push data should be identical data
}
else
{
FCacheValue* Val = new FCacheValue(InData);
int32 CacheValueSize = CalcCacheValueSize(Key, *Val);
// check if we haven't exceeded the MaxCacheSize
if (MaxCacheSize > 0 && (CurrentCacheSize + CacheValueSize) > MaxCacheSize)
{
delete Val;
UE_LOG(LogDerivedDataCache, Display, TEXT("Failed to cache data. Maximum cache size reached. CurrentSize %d kb / MaxSize: %d kb"), CurrentCacheSize / 1024, MaxCacheSize / 1024);
bMaxSizeExceeded = true;
}
else
{
COOK_STAT(Timer.AddHit(InData.Num()));
CacheItems.Add(Key, Val);
CalcCacheValueSize(Key, *Val);
CurrentCacheSize += CacheValueSize;
}
}
}
void FMemoryDerivedDataBackend::RemoveCachedData(const TCHAR* CacheKey, bool bTransient)
{
FScopeLock ScopeLock(&SynchronizationObject);
if (bDisabled || bTransient)
{
return;
}
FString Key(CacheKey);
FCacheValue* Item = NULL;
if (CacheItems.RemoveAndCopyValue(Key, Item))
{
CurrentCacheSize -= CalcCacheValueSize(Key, *Item);
bMaxSizeExceeded = false;
check(Item);
delete Item;
}
else
{
check(!Item);
}
}
bool FMemoryDerivedDataBackend::SaveCache(const TCHAR* Filename)
{
double StartTime = FPlatformTime::Seconds();
TUniquePtr<FArchive> SaverArchive(IFileManager::Get().CreateFileWriter(Filename, FILEWRITE_EvenIfReadOnly));
if (!SaverArchive)
{
UE_LOG(LogDerivedDataCache, Error, TEXT("Could not save memory cache %s."), Filename);
return false;
}
FArchive& Saver = *SaverArchive;
uint32 Magic = MemCache_Magic64;
Saver << Magic;
const int64 DataStartOffset = Saver.Tell();
{
FScopeLock ScopeLock(&SynchronizationObject);
check(!bDisabled);
for (TMap<FString, FCacheValue*>::TIterator It(CacheItems); It; ++It )
{
Saver << It.Key();
Saver << It.Value()->Age;
Saver << It.Value()->Data;
}
}
const int64 DataSize = Saver.Tell(); // Everything except the footer
int64 Size = DataSize;
uint32 Crc = MemCache_Magic64; // Crc takes more time than I want to spend FCrc::MemCrc_DEPRECATED(&Buffer[0], Size);
Saver << Size;
Saver << Crc;
check(SerializationSpecificDataSize + DataSize <= MaxCacheSize || MaxCacheSize <= 0);
UE_LOG(LogDerivedDataCache, Log, TEXT("Saved boot cache %4.2fs %lldMB %s."), float(FPlatformTime::Seconds() - StartTime), DataSize / (1024 * 1024), Filename);
return true;
}
bool FMemoryDerivedDataBackend::LoadCache(const TCHAR* Filename)
{
double StartTime = FPlatformTime::Seconds();
const int64 FileSize = IFileManager::Get().FileSize(Filename);
if (FileSize < 0)
{
UE_LOG(LogDerivedDataCache, Warning, TEXT("Could not find memory cache %s."), Filename);
return false;
}
// We test 3 * uint32 which is the old format (< SerializationSpecificDataSize). We'll test
// against SerializationSpecificDataSize later when we read the magic number from the cache.
if (FileSize < sizeof(uint32) * 3)
{
UE_LOG(LogDerivedDataCache, Error, TEXT("Memory cache was corrputed (short) %s."), Filename);
return false;
}
if (FileSize > MaxCacheSize*2 && MaxCacheSize > 0)
{
UE_LOG(LogDerivedDataCache, Error, TEXT("Refusing to load DDC cache %s. Size exceeds doubled MaxCacheSize."), Filename);
return false;
}
TUniquePtr<FArchive> LoaderArchive(IFileManager::Get().CreateFileReader(Filename));
if (!LoaderArchive)
{
UE_LOG(LogDerivedDataCache, Warning, TEXT("Could not read memory cache %s."), Filename);
return false;
}
FArchive& Loader = *LoaderArchive;
uint32 Magic = 0;
Loader << Magic;
if (Magic != MemCache_Magic && Magic != MemCache_Magic64)
{
UE_LOG(LogDerivedDataCache, Error, TEXT("Memory cache was corrputed (magic) %s."), Filename);
return false;
}
// Check the file size again, this time against the correct minimum size.
if (Magic == MemCache_Magic64 && FileSize < SerializationSpecificDataSize)
{
UE_LOG(LogDerivedDataCache, Error, TEXT("Memory cache was corrputed (short) %s."), Filename);
return false;
}
// Calculate expected DataSize based on the magic number (footer size difference)
const int64 DataSize = FileSize - (Magic == MemCache_Magic64 ? (SerializationSpecificDataSize - sizeof(uint32)) : (sizeof(uint32) * 2));
Loader.Seek(DataSize);
int64 Size = 0;
uint32 Crc = 0;
if (Magic == MemCache_Magic64)
{
Loader << Size;
}
else
{
uint32 Size32 = 0;
Loader << Size32;
Size = (int64)Size32;
}
Loader << Crc;
if (Size != DataSize)
{
UE_LOG(LogDerivedDataCache, Error, TEXT("Memory cache was corrputed (size) %s."), Filename);
return false;
}
if ((Crc != MemCache_Magic && Crc != MemCache_Magic64) || Crc != Magic)
{
UE_LOG(LogDerivedDataCache, Warning, TEXT("Memory cache was corrputed (crc) %s."), Filename);
return false;
}
// Seek to data start offset (skip magic number)
Loader.Seek(sizeof(uint32));
{
TArray<uint8> Working;
FScopeLock ScopeLock(&SynchronizationObject);
check(!bDisabled);
while (Loader.Tell() < DataSize)
{
FString Key;
int32 Age;
Loader << Key;
Loader << Age;
Age++;
Loader << Working;
if (Age < MaxAge)
{
CacheItems.Add(Key, new FCacheValue(Working, Age));
}
Working.Reset();
}
// these are just a double check on ending correctly
if (Magic == MemCache_Magic64)
{
Loader << Size;
}
else
{
uint32 Size32 = 0;
Loader << Size32;
Size = (int64)Size32;
}
Loader << Crc;
}
CurrentCacheSize = FileSize;
CacheFilename = Filename;
UE_LOG(LogDerivedDataCache, Log, TEXT("Loaded boot cache %4.2fs %lldMB %s."), float(FPlatformTime::Seconds() - StartTime), DataSize / (1024 * 1024), Filename);
return true;
}
void FMemoryDerivedDataBackend::Disable()
{
FScopeLock ScopeLock(&SynchronizationObject);
bDisabled = true;
for (TMap<FString,FCacheValue*>::TIterator It(CacheItems); It; ++It )
{
delete It.Value();
}
CacheItems.Empty();
CurrentCacheSize = SerializationSpecificDataSize;
}
void FMemoryDerivedDataBackend::GatherUsageStats(TMap<FString, FDerivedDataCacheUsageStats>& UsageStatsMap, FString&& GraphPath)
{
COOK_STAT(UsageStatsMap.Add(FString::Printf(TEXT("%s: %s.%s"), *GraphPath, TEXT("MemoryBackend"), *CacheFilename), UsageStats));
}