// Copyright Epic Games, Inc. All Rights Reserved. #pragma once #include "VirtualFileCache.h" #include "CoreMinimal.h" #include "HAL/FileManager.h" #include "HAL/Runnable.h" #include "Logging/LogMacros.h" #include "Misc/Paths.h" #include "Misc/ScopeExit.h" #include "Misc/ScopeRWLock.h" #include "Async/MappedFileHandle.h" #include "Async/Future.h" #include "Containers/IntrusiveDoubleLinkedList.h" DECLARE_LOG_CATEGORY_EXTERN(LogVFC, Log, All); DECLARE_STATS_GROUP(TEXT("VFC"), STATGROUP_VFC, STATCAT_Advanced); DECLARE_DWORD_COUNTER_STAT_EXTERN(TEXT("Files Added"), STAT_FilesAdded, STATGROUP_VFC, ); DECLARE_DWORD_COUNTER_STAT_EXTERN(TEXT("Bytes Added"), STAT_BytesAdded, STATGROUP_VFC, ); DECLARE_DWORD_COUNTER_STAT_EXTERN(TEXT("Files Removed"), STAT_FilesRemoved, STATGROUP_VFC, ); DECLARE_DWORD_COUNTER_STAT_EXTERN(TEXT("Bytes Removed"), STAT_BytesRemoved, STATGROUP_VFC, ); DECLARE_DWORD_COUNTER_STAT_EXTERN(TEXT("Files Evicted"), STAT_FilesEvicted, STATGROUP_VFC, ); DECLARE_DWORD_COUNTER_STAT_EXTERN(TEXT("Bytes Evicted"), STAT_BytesEvicted, STATGROUP_VFC, ); // Fragmentation is defined as the amount of separation per file in the cache. // A value of 1 means that there is no fragmentation // A value of 2 means that the mean number of ranges per file is 2 constexpr double VFC_ALLOWED_FRAGMENTATION = 4.0f; constexpr uint64 VFC_BLOCK_SIZE_MIN = 1; constexpr uint64 VFC_BLOCK_SIZE_MAX = 1 << 24; using FMappedRange = TUniquePtr; enum class EVFCFileVersion { Invalid, Initial, Count, Current = Count - 1, }; struct FBlockRange { int32 StartIndex; int32 NumBlocks; }; struct FRangeId { int32 FileId; FBlockRange Range; }; struct FDataReference { TArray Ranges; uint32 TotalSize = 0; mutable int64 LastReferencedUnixTime = 0; // Mutable since it needs to update when the data is read void Touch(); }; struct FBlockFile { // Serialized: EVFCFileVersion FileVersion = EVFCFileVersion::Current; int32 FileId = 0; int32 BlockSize = 0; int32 NumBlocks = 0; TArray FreeRanges; TArray UsedRanges; // Not Serialized: TUniquePtr WriteHandle; TUniquePtr MapHandle; bool bWriteLocked = false; TUniquePtr FileHandleLock = MakeUnique(); int64 TotalSize() { return static_cast(NumBlocks) * BlockSize; } void Reset() { FileId = 0; BlockSize = 0; NumBlocks = 0; FreeRanges.Empty(); UsedRanges.Empty(); WriteHandle = nullptr; MapHandle = nullptr; bWriteLocked = false; FileHandleLock = MakeUnique(); } }; using FFileMap = TMap; struct FVirtualFileCache; struct IFileTableReader { virtual ~IFileTableReader() = default; virtual bool ValidateRanges() = 0; virtual const FDataReference* FindDataReference(const VFCKey& Id) const = 0; virtual TIoStatusOr> ReadData(VFCKey Id, int64 ReadOffset = 0, int64 ReadSizeOrZero = 0) = 0; virtual TIoStatusOr GetSizeForChunk(const VFCKey& Id) const = 0; virtual bool DoesChunkExist(const VFCKey& Id) const = 0; virtual double CurrentFragmentation() const = 0; virtual int64 GetUsedSize() const = 0; virtual int64 GetTotalSize() const = 0; }; struct FFileTable : IFileTableReader { FFileTable(FVirtualFileCache* InParent) : Parent(InParent) { } private: FVirtualFileCache* Parent; EVFCFileVersion FileVersion = EVFCFileVersion::Current; /* Files on disk */ TArray BlockFiles; /* Data stored in the chunks */ FFileMap FileMap; int64 TotalSize = -1; int64 UsedSize = -1; int32 LastBlockFileId = 0; int32 WriteLockCount = 0; public: void Initialize(const FVirtualFileCacheSettings& Settings); bool ReadTableFile(); int32 CreateBlockFile(int64 FileSize, int32 BlockSize); bool DeleteBlockFile(int32 BlockFileId); void WriteTableFile(); void Defragment(); void Empty(); bool CoalesceRanges(); void CalculateSizes(); FString GetBlockFilename(const FBlockFile& File) const; FBlockFile* GetFileForRange(FRangeId Id); FBlockFile* GetFileForId(int32 BlockFileId); bool BlockFileExistsOnDisk(const FBlockFile& BlockFile) const; IFileHandle* OpenBlockFileForWrite(FBlockFile& BlockFile); bool DeleteBlockFile(FFileTable* FileTable, int32 BlockFileId); bool RangeIsValid(FRangeId Block); bool EraseData(VFCKey Id); TIoStatusOr AllocateSingleRange(FBlockFile& File, int64 MaximumSize); TArray AllocateBlocksForSize(uint64 Size); int64 AllocationSize(FDataReference* DataRef); void FreeBlock(FRangeId RangeId); bool EnsureSizeFor(int64 RequiredBytes); int64 EvictOne(); bool EvictAmount(int64 NumBytesToEvict); IMappedFileHandle* MapBlockFile(FBlockFile& BlockFile); FMappedRange MapFileRange(FRangeId Range); bool ReadRange(FRangeId RangeId, void* Dest, int64 ReadSize); FIoStatus WriteData(VFCKey Id, const uint8* Data, uint64 DataSize); // IFileTableReader virtual bool ValidateRanges() override; virtual const FDataReference* FindDataReference(const VFCKey& Id) const override; virtual TIoStatusOr> ReadData(VFCKey Id, int64 ReadOffset = 0, int64 ReadSizeOrZero = 0) override; virtual TIoStatusOr GetSizeForChunk(const VFCKey& Id) const override; virtual bool DoesChunkExist(const VFCKey& Id) const override; virtual double CurrentFragmentation() const override; virtual int64 GetUsedSize() const override; virtual int64 GetTotalSize() const override; // \IFileTableReader friend FArchive& operator<<(FArchive& Ar, FFileTable& FileTable); }; struct FBlockRangeSortStartIndex { bool operator()(const FBlockRange& L, const FBlockRange& R) const { return L.StartIndex < R.StartIndex; } }; struct FBlockRangeSortSize { bool operator()(const FBlockRange& L, const FBlockRange& R) const { return L.NumBlocks > R.NumBlocks; } }; inline FString GetVFCDirectory() { return FPaths::ProjectPersistentDownloadDir() / "VFC"; } FArchive& operator<<(FArchive& Ar, FFileTable& FileTable); enum class ERWOp : int8 { Invalid = -1, Read, Write, Erase, }; struct FRWOp { ERWOp Op = ERWOp::Invalid; VFCKey Target; // Write TSharedPtr> DataToWrite; // Read TOptional>> ReadResult; int64 ReadOffset = 0; int64 ReadSize = 0; }; struct FLruCacheNode : public TIntrusiveDoubleLinkedListNode { // For reverse lookups VFCKey Key; TSharedPtr> Data; // Store the size here to ensure that the CurrentSize of the LruCache can rely on the Data size value changing. uint64 RecordedSize; }; class FLruCache { void EvictToBelowMaxSize(); void EvictOne(); bool FreeSpaceFor(int64 SizeToAdd); public: ~FLruCache() { LruList.Reset(); } FLruCacheNode* Find(VFCKey Key); const FLruCacheNode* Find(VFCKey Key) const; TSharedPtr> ReadLockAndFindData(VFCKey Key) const; void Insert(VFCKey Key, TSharedPtr> Data); void Remove(VFCKey Key); bool IsEnabled() const; void SetMaxSize(int64 NewMaxSize); public: mutable FRWLock Lock; private: TMap> NodeMap; TIntrusiveDoubleLinkedList LruList; int64 CurrentSize = 0; int64 MaxSize = 0; }; struct FVirtualFileCache; template struct FFileTableLockingReference { static_assert(LOCKTYPE == SLT_ReadOnly || LOCKTYPE == SLT_Write); FFileTableLockingReference(TTableType& InTable, FRWLock& InLock) : FileTable(&InTable) , Lock(&InLock) { if constexpr (LOCKTYPE == SLT_ReadOnly) { Lock->ReadLock(); } else if constexpr (LOCKTYPE == SLT_Write) { Lock->WriteLock(); } } ~FFileTableLockingReference() { if (Lock) { if constexpr (LOCKTYPE == SLT_ReadOnly) { Lock->ReadUnlock(); } else if constexpr (LOCKTYPE == SLT_Write) { Lock->WriteUnlock(); } } } FFileTableLockingReference(FFileTableLockingReference&& ToMove) : FileTable(ToMove.FileTable) , Lock(ToMove.Lock) { ToMove.Lock = nullptr; } FFileTableLockingReference(FFileTableLockingReference& ToMove) = delete; FFileTableLockingReference operator=(FFileTableLockingReference& ToMove) = delete; FFileTableLockingReference& operator=(FFileTableLockingReference&& ToMove) = delete; TTableType* operator->() { return FileTable; } TTableType* Get() { return FileTable; } private: TTableType* FileTable; FRWLock* Lock; }; using FFileTableReader = FFileTableLockingReference; using FFileTableMutator = FFileTableLockingReference; using FFileTableWriter = FFileTableLockingReference; struct FVirtualFileCacheThread : public FRunnable { FVirtualFileCacheThread(FVirtualFileCache* InParent); ~FVirtualFileCacheThread(); /* FRunnable */ virtual bool Init() override; virtual uint32 Run() override; virtual void Stop() override; virtual void Exit() override; /* FRunnable */ // Public Interface TFuture> RequestRead(VFCKey Target, int64 ReadOffset = 0, int64 ReadSizeOrZero = 0); void RequestWrite(VFCKey Target, TArrayView Data); void RequestErase(VFCKey Target); void Shutdown(); [[nodiscard]] FFileTableReader ReadFileTable() const; [[nodiscard]] FFileTableMutator MutateFileTable(); [[nodiscard]] FFileTableWriter ModifyFileTable(); public: void DeleteUnexpectedCacheFiles(TSet& ExpectedFiles); void FreeBlock(FRangeId RangeId); FString GetTableFilename() const; void EnqueueOrRunOp(TSharedPtr Op); TSharedPtr GetNextOp(); void DoOneOp(FRWOp* Op); void Touch(FDataReference& Id); void EraseTableFile(); void SetInMemoryCacheSize(int64 MaxSize); uint64 GetTotalMemCacheHits() const; uint64 GetTotalMemCacheMisses() const; public: FVirtualFileCache* Parent; FRunnableThread* Thread; FEvent* Event; FLruCache MemCache; FRWLock OperationQueueLock; TArray> OperationQueue; FString BasePath; std::atomic_bool bStopRequested; private: mutable FRWLock FileTableLock; FFileTable FileTableStorage; uint64 TotalMemCacheHits = 0; uint64 TotalMemCacheMisses = 0; }; struct FVirtualFileCache final : IVirtualFileCache { FVirtualFileCache() : Thread(this) {} virtual ~FVirtualFileCache() { Shutdown(); } FVirtualFileCache(const FVirtualFileCache&) = delete; FVirtualFileCache& operator=(const FVirtualFileCache&) = delete; public: void Shutdown(); virtual void Initialize(const FVirtualFileCacheSettings& Settings) override; virtual FIoStatus WriteData(VFCKey Id, const uint8* Data, uint64 DataSize) override; virtual TFuture> ReadData(VFCKey Id, int64 ReadOffset = 0, int64 ReadSizeOrZero = 0) override; virtual bool DoesChunkExist(const VFCKey& Id) const override; virtual void EraseData(VFCKey Id) override; virtual TIoStatusOr GetSizeForChunk(const VFCKey& Id) const override; virtual double CurrentFragmentation() const override; virtual void Defragment() override; virtual int64 GetTotalSize() const override; virtual int64 GetUsedSize() const override; virtual uint64 GetTotalMemCacheHits() const override; virtual uint64 GetTotalMemCacheMisses() const override; FString GetTableFilename() const { return Thread.GetTableFilename(); } public: FChunkEvictedDelegate OnDataEvicted; FVirtualFileCacheThread Thread; FString BasePath; private: FVirtualFileCacheSettings Settings; friend struct FVirtualFileCacheThread; };