Files
UnrealEngineUWP/Engine/Source/Runtime/PakFile/Private/SignedArchiveReader.h
T
paul chipchase 227893a8f9 Fix a potential deadlock when loading from packfiles
This was originally fixed in CL 11458603 but it did not fix the issue entirely and in any case I was not happy with the manual reset event approach.
The problem with the manual reset event was that there is no actual safe point at which we can reset the event without risking some requesting thread missing it and waiting forever, it is just a lot rarer than the original thread lock.

- Each FChunkRequest now can be given an event that is triggered, so the correct thread is always woken up if waiting.
- Since the signals are 1-1 we no longer risk having threads never being woken.
- We only wake the thread that has a request to process so don't risk flooding the cpu.
- Removed FChunkCacheWorker::SetupDecryptionKey as it was not implemented/used.
- Removed FChunkCacheWorker::ChunkRequestAvailable and its associated methods ::WaitForNextChunk/::FlushRemainingChunkCompletionEvents.

In testing this version runs slightly faster on loading tests (0.2seconds on average), although just removing the events entirely and running a busy loop with a yield ran slightly faster on machines with less cores (4 cores with hyperthreading or less) albiet with a much higher variation and so potentially could end up a lot worse.

#rb Graeme.Thornton
#jira UE-89165


#ushell-cherrypick of 11724338 by paul.chipchase

#ROBOMERGE-SOURCE: CL 11724753 in //UE4/Release-4.25/... via CL 11724760
#ROBOMERGE-BOT: RELEASE (Release-4.25Plus -> Main) (v656-11643781)

[CL 11724764 by paul chipchase in Main branch]
2020-02-28 05:04:42 -05:00

303 lines
7.7 KiB
C++

// Copyright Epic Games, Inc. All Rights Reserved.
#pragma once
#include "CoreMinimal.h"
#include "HAL/ThreadSafeCounter.h"
#include "Containers/LockFreeList.h"
#include "IPlatformFilePak.h"
#include "HAL/PlatformProcess.h"
#include "HAL/Runnable.h"
#include "Math/BigInt.h"
/**
* Chunk buffer.
* Buffers are locked and released only on the worker thread.
*/
struct FChunkBuffer
{
/** Chunk data */
uint8* Data;
/** Number of locks on this buffer */
int32 LockCount;
/** Index of the chunk */
int32 ChunkIndex;
/** Last time this buffer has been accessed */
double LastAccessTime;
FChunkBuffer()
: Data(NULL)
, LockCount(0)
, ChunkIndex(INDEX_NONE)
, LastAccessTime(0.0)
{
Data = (uint8*)FMemory::Malloc(FPakInfo::MaxChunkDataSize);
}
~FChunkBuffer()
{
FMemory::Free(Data);
Data = NULL;
}
};
/**
* Request to load a chunk. This is how the archive reader and the worker thread
* communicate. Requests can be locked by both threads.
*/
struct FChunkRequest
{
/** Chunk index */
int32 Index;
/** Chunk offset */
int64 Offset;
/** Chunk size */
int64 Size;
/** Buffer where the data is cached */
FChunkBuffer* Buffer;
/** Flag to indicate if the chunk has been verified */
FThreadSafeCounter IsTrusted;
/** Reference count */
FThreadSafeCounter RefCount;
/** Optional pointer to an FEvent that can be used to signal that the request is ready for use */
FEvent* Event;
/**
* Constructor
*/
FChunkRequest()
: Index(INDEX_NONE)
, Offset(0)
, Size(0)
, Buffer(nullptr)
, IsTrusted(0)
, RefCount(0)
, Event(nullptr)
{}
/**
* Waits until this chunk has been verified
*/
FORCEINLINE void WaitUntilReady() const
{
while (IsTrusted.GetValue() == 0)
{
FPlatformProcess::Sleep(0.0f);
}
}
/**
* Checks if this chunk has been verified.
*/
FORCEINLINE bool IsReady() const
{
return IsTrusted.GetValue() != 0;
}
};
/**
* Thread that loads and verifies signed chunks.
* One per pak file but can serve multiple FSignedArchiveReaders from multiple threads!
* Can process multiple chunks using a limited number of buffers.
*/
class FChunkCacheWorker : public FRunnable
{
enum
{
/** Buffer size */
MaxCachedChunks = 8
};
/** Reference hashes */
TSharedPtr<const FPakSignatureFile, ESPMode::ThreadSafe> Signatures;
/** Hash of the sig file data. Used to check that nothing was corrupted when a signature check fails */
TPakChunkHash OriginalSignatureFileHash;
/** Thread to run the worker FRunnable on */
FRunnableThread* Thread;
/** Archive reader */
FArchive* Reader;
/** Cached and verified chunks. */
FChunkBuffer CachedChunks[MaxCachedChunks];
/** Queue of chunks to cache */
TArray<FChunkRequest*> RequestQueue;
/** Lock for manipulating the queue */
FCriticalSection QueueLock;
/** Counter indicating how many pending queued request exist */
FThreadSafeCounter PendingQueueCounter;
/** Event used to signal there's work to be done */
FEvent* QueuedRequestsEvent;
/** List of active chunk requests */
TArray<FChunkRequest*> ActiveRequests;
/** Stops this thread */
FThreadSafeCounter StopTaskCounter;
/** Available chunk requests */
TLockFreePointerListUnordered<FChunkRequest, PLATFORM_CACHE_LINE_SIZE> FreeChunkRequests;
/**
* Process requested chunks
*
* @return Number of chunks processed this loop.
*/
int32 ProcessQueue();
/**
* Verifies chunk signature [*]
*/
bool CheckSignature(const FChunkRequest& ChunkInfo);
/**
* Tries to get a pre-cached chunk buffer
*/
FChunkBuffer* GetCachedChunkBuffer(int32 ChunkIndex);
/**
* Tries to get the least recent free buffer
*/
FChunkBuffer* GetFreeBuffer();
/**
* Decrements a ref count on a buffer for the specified chunk
*/
void ReleaseBuffer(int32 ChunkIndex);
/**
* Is this chunk cache worker running in a thread?
*/
FORCEINLINE bool IsMultithreaded() const
{
return Thread != nullptr;
}
public:
FChunkCacheWorker(FArchive* InReader, const TCHAR* Filename);
virtual ~FChunkCacheWorker();
//~ Begin FRunnable Interface.
virtual bool Init();
virtual uint32 Run();
virtual void Stop();
//~ End FRunnable Interface
/**
* Requests a chunk to be loaded and verified
*
* @param ChunkIndex Index of a chunk to load
* @param StartOffset Offset to the beginning of the chunk
* @param ChunkSize Chunk size
* @param Event Optional FEvent that will signal when the request is ready, nullptr is valid if the calling code does not want this signal
* @return Handle to the request.
*/
FChunkRequest& RequestChunk(int32 ChunkIndex, int64 StartOffset, int64 ChunkSize, FEvent* Event);
/**
* Releases the requested chunk buffer
*/
void ReleaseChunk(FChunkRequest& Chunk);
/**
* Indicates that this chunk worker is valid. If the signature file couldn't be loaded or if it failed
* the master table check, this will be false
*/
bool IsValid() const;
friend class FSignedArchiveReader;
};
/////////////////////////////////////////////////////////////////////////////////////////
/**
* FSignedArchiveReader - reads data from pre-cached and verified chunks.
*/
class FSignedArchiveReader : public FArchive
{
enum
{
/** Number of chunks to pre-cache.*/
PrecacheLength = 0
};
struct FReadInfo
{
FChunkRequest* Request;
FChunkBuffer* PreCachedChunk;
int64 SourceOffset;
int64 DestOffset;
int64 Size;
};
/** Number of chunks in the archive */
int32 ChunkCount;
/** Reader archive */
FArchive* PakReader;
/** Size of the archive on disk */
int64 SizeOnDisk;
/** Size of actual data (excluding signatures) */
int64 PakSize;
/** Current offet into data */
int64 PakOffset;
/** Worker thread - reads chunks from disk and verifies their signatures */
FChunkCacheWorker* SignatureChecker;
/** Last pre-cached buffer */
FChunkBuffer LastCachedChunk;
/**
* Calculate index of a chunk that contains the specified offset
*/
FORCEINLINE int32 CalculateChunkIndex(int64 ReadOffset) const
{
return (int32)(ReadOffset / FPakInfo::MaxChunkDataSize);
}
/**
* Calculate offset of a chunk in the archive
*/
FORCEINLINE int64 CalculateChunkOffsetFromIndex(int64 BufferIndex) const
{
return BufferIndex * FPakInfo::MaxChunkDataSize;
}
/**
* Calculate offset of a chunk in the archive and the offset to read from the archive
*
* @param ReadOffset Read request offset
* @param OutDataOffset Actuall offset to read from in the archive
* @return Offset where the chunk begins in the archive (signature offset)
*/
FORCEINLINE int64 CalculateChunkOffset(int64 ReadOffset, int64& OutDataOffset) const
{
const int32 ChunkIndex = CalculateChunkIndex(ReadOffset);
OutDataOffset = ReadOffset;
return CalculateChunkOffsetFromIndex(ChunkIndex);
}
/**
* Calculates chunk size based on its index (most chunks have the same size, except the last one
*/
int64 CalculateChunkSize(int64 ChunkIndex) const;
/**
* Queues chunks on the worker thread
* @param Chunks This array will contain info about each chunk created by the call.
* @param Length The length of data to precache
* @param Event Optional FEvent that will signal as each request becomes ready, nullptr is valid if the calling code does not want this signal
* @return Number of chunks in the output array which are actually required for the requested length. The rest are precache chunks
*/
int64 PrecacheChunks(TArray<FReadInfo>& Chunks, int64 Length, FEvent* Event);
public:
FSignedArchiveReader(FArchive* InPakReader, FChunkCacheWorker* InSignatureChecker);
virtual ~FSignedArchiveReader();
//~ Begin FArchive Interface
virtual void Serialize(void* Data, int64 Length) override;
virtual int64 Tell() override
{
return PakOffset;
}
virtual int64 TotalSize() override
{
return PakSize;
}
virtual void Seek(int64 InPos) override
{
PakOffset = InPos;
}
//~ End FArchive Interface
};