// Copyright Epic Games, Inc. All Rights Reserved. #pragma once #include #include #include #include #include #include #include #include ZEN_THIRD_PARTY_INCLUDES_START #include ZEN_THIRD_PARTY_INCLUDES_END #include #include #include #include #define ZEN_USE_CACHE_TRACKER 0 namespace zen { class PathBuilderBase; class GcManager; class ZenCacheTracker; class ScrubContext; /****************************************************************************** /$$$$$$$$ /$$$$$$ /$$ |_____ $$ /$$__ $$ | $$ /$$/ /$$$$$$ /$$$$$$$ | $$ \__/ /$$$$$$ /$$$$$$| $$$$$$$ /$$$$$$ /$$/ /$$__ $| $$__ $$ | $$ |____ $$/$$_____| $$__ $$/$$__ $$ /$$/ | $$$$$$$| $$ \ $$ | $$ /$$$$$$| $$ | $$ \ $| $$$$$$$$ /$$/ | $$_____| $$ | $$ | $$ $$/$$__ $| $$ | $$ | $| $$_____/ /$$$$$$$| $$$$$$| $$ | $$ | $$$$$$| $$$$$$| $$$$$$| $$ | $| $$$$$$$ |________/\_______|__/ |__/ \______/ \_______/\_______|__/ |__/\_______/ Cache store for UE5. Restricts keys to "{bucket}/{hash}" pairs where the hash is 40 (hex) chars in size. Values may be opaque blobs or structured objects which can in turn contain references to other objects (or blobs). ******************************************************************************/ namespace access_tracking { struct KeyAccessTime { IoHash Key; GcClock::Tick LastAccess{}; }; struct AccessTimes { std::unordered_map> Buckets; }; }; // namespace access_tracking struct ZenCacheValue { IoBuffer Value; CbObject IndexData; }; ////////////////////////////////////////////////////////////////////////// #pragma pack(push) #pragma pack(1) struct DiskLocation { inline DiskLocation() = default; inline DiskLocation(uint64_t ValueSize, uint8_t Flags) : Flags(Flags | kStandaloneFile) { Location.StandaloneSize = ValueSize; } inline DiskLocation(const BlockStoreLocation& Location, uint64_t PayloadAlignment, uint8_t Flags) : Flags(Flags & ~kStandaloneFile) { this->Location.BlockLocation = BlockStoreDiskLocation(Location, PayloadAlignment); } inline BlockStoreLocation GetBlockLocation(uint64_t PayloadAlignment) const { ZEN_ASSERT(!(Flags & kStandaloneFile)); return Location.BlockLocation.Get(PayloadAlignment); } inline uint64_t Size() const { return (Flags & kStandaloneFile) ? Location.StandaloneSize : Location.BlockLocation.GetSize(); } inline uint8_t IsFlagSet(uint64_t Flag) const { return Flags & Flag; } inline uint8_t GetFlags() const { return Flags; } inline ZenContentType GetContentType() const { ZenContentType ContentType = ZenContentType::kBinary; if (IsFlagSet(kStructured)) { ContentType = ZenContentType::kCbObject; } if (IsFlagSet(kCompressed)) { ContentType = ZenContentType::kCompressedBinary; } return ContentType; } union { BlockStoreDiskLocation BlockLocation; // 10 bytes uint64_t StandaloneSize = 0; // 8 bytes } Location; static const uint8_t kStandaloneFile = 0x80u; // Stored as a separate file static const uint8_t kStructured = 0x40u; // Serialized as compact binary static const uint8_t kTombStone = 0x20u; // Represents a deleted key/value static const uint8_t kCompressed = 0x10u; // Stored in compressed buffer format uint8_t Flags = 0; uint8_t Reserved = 0; }; struct DiskIndexEntry { IoHash Key; // 20 bytes DiskLocation Location; // 12 bytes }; #pragma pack(pop) static_assert(sizeof(DiskIndexEntry) == 32); /** In-memory cache storage Intended for small values which are frequently accessed This should have a better memory management policy to maintain reasonable footprint. */ class ZenCacheMemoryLayer { public: ZenCacheMemoryLayer(); ~ZenCacheMemoryLayer(); bool Get(std::string_view Bucket, const IoHash& HashKey, ZenCacheValue& OutValue); void Put(std::string_view Bucket, const IoHash& HashKey, const ZenCacheValue& Value); void Drop(); bool DropBucket(std::string_view Bucket); void Scrub(ScrubContext& Ctx); void GatherAccessTimes(zen::access_tracking::AccessTimes& AccessTimes); void Reset(); uint64_t TotalSize() const; struct Configuration { uint64_t TargetFootprintBytes = 16 * 1024 * 1024; uint64_t ScavengeThreshold = 4 * 1024 * 1024; }; const Configuration& GetConfiguration() const { return m_Configuration; } void SetConfiguration(const Configuration& NewConfig) { m_Configuration = NewConfig; } private: struct CacheBucket { struct BucketValue { IoBuffer Payload; std::atomic_int64_t LastAccess; BucketValue() : Payload(), LastAccess() {} BucketValue(IoBuffer Value, const int64_t Timestamp) : Payload(Value), LastAccess(Timestamp) {} BucketValue(const BucketValue& V) : Payload(V.Payload), LastAccess(V.LastAccess.load(std::memory_order_relaxed)) {} BucketValue(BucketValue&& V) : Payload(std::move(V.Payload)), LastAccess(V.LastAccess.load(std::memory_order_relaxed)) {} BucketValue& operator=(const BucketValue& V) { return *this = BucketValue(V); } BucketValue& operator=(BucketValue&& V) { Payload = std::move(V.Payload); LastAccess.store(V.LastAccess.load(), std::memory_order_relaxed); return *this; } }; RwLock m_BucketLock; tsl::robin_map m_CacheMap; std::atomic_uint64_t m_TotalSize{}; bool Get(const IoHash& HashKey, ZenCacheValue& OutValue); void Put(const IoHash& HashKey, const ZenCacheValue& Value); void Drop(); void Scrub(ScrubContext& Ctx); void GatherAccessTimes(std::vector& AccessTimes); inline uint64_t TotalSize() const { return m_TotalSize; } }; mutable RwLock m_Lock; std::unordered_map> m_Buckets; std::vector> m_DroppedBuckets; Configuration m_Configuration; ZenCacheMemoryLayer(const ZenCacheMemoryLayer&) = delete; ZenCacheMemoryLayer& operator=(const ZenCacheMemoryLayer&) = delete; }; class ZenCacheDiskLayer { public: explicit ZenCacheDiskLayer(const std::filesystem::path& RootDir); ~ZenCacheDiskLayer(); bool Get(std::string_view Bucket, const IoHash& HashKey, ZenCacheValue& OutValue); void Put(std::string_view Bucket, const IoHash& HashKey, const ZenCacheValue& Value); bool Drop(); bool DropBucket(std::string_view Bucket); void Flush(); void Scrub(ScrubContext& Ctx); void GatherReferences(GcContext& GcCtx); void CollectGarbage(GcContext& GcCtx); void UpdateAccessTimes(const zen::access_tracking::AccessTimes& AccessTimes); void DiscoverBuckets(); uint64_t TotalSize() const; private: /** A cache bucket manages a single directory containing metadata and data for that bucket */ struct CacheBucket { CacheBucket(std::string BucketName); ~CacheBucket(); bool OpenOrCreate(std::filesystem::path BucketDir, bool AllowCreate = true); bool Get(const IoHash& HashKey, ZenCacheValue& OutValue); void Put(const IoHash& HashKey, const ZenCacheValue& Value); bool Drop(); void Flush(); void Scrub(ScrubContext& Ctx); void GatherReferences(GcContext& GcCtx); void CollectGarbage(GcContext& GcCtx); void UpdateAccessTimes(const std::vector& AccessTimes); inline uint64_t TotalSize() const { return m_TotalSize.load(std::memory_order::relaxed); } private: const uint64_t MaxBlockSize = 1ull << 30; uint64_t m_PayloadAlignment = 1ull << 4; std::string m_BucketName; std::filesystem::path m_BucketDir; std::filesystem::path m_BlocksBasePath; BlockStore m_BlockStore; Oid m_BucketId; uint64_t m_LargeObjectThreshold = 128 * 1024; // These files are used to manage storage of small objects for this bucket TCasLogFile m_SlogFile; struct IndexEntry { DiskLocation Location; std::atomic_int64_t LastAccess; IndexEntry() : Location(), LastAccess() {} IndexEntry(const DiskLocation& Loc, const int64_t Timestamp) : Location(Loc), LastAccess(Timestamp) {} IndexEntry(const IndexEntry& E) : Location(E.Location), LastAccess(E.LastAccess.load(std::memory_order_relaxed)) {} IndexEntry(IndexEntry&& E) noexcept : Location(std::move(E.Location)), LastAccess(E.LastAccess.load(std::memory_order_relaxed)) { } IndexEntry& operator=(const IndexEntry& E) { return *this = IndexEntry(E); } IndexEntry& operator=(IndexEntry&& E) noexcept { Location = std::move(E.Location); LastAccess.store(E.LastAccess.load(), std::memory_order_relaxed); return *this; } }; using IndexMap = tsl::robin_map; RwLock m_IndexLock; IndexMap m_Index; std::atomic_uint64_t m_TotalSize{}; void BuildPath(PathBuilderBase& Path, const IoHash& HashKey); void PutStandaloneCacheValue(const IoHash& HashKey, const ZenCacheValue& Value); bool GetStandaloneCacheValue(const DiskLocation& Loc, const IoHash& HashKey, ZenCacheValue& OutValue); void PutInlineCacheValue(const IoHash& HashKey, const ZenCacheValue& Value); bool GetInlineCacheValue(const DiskLocation& Loc, ZenCacheValue& OutValue); void MakeIndexSnapshot(); uint64_t ReadIndexFile(); uint64_t ReadLog(uint64_t LogPosition); void OpenLog(const std::filesystem::path& BucketDir, const bool IsNew); void SaveManifest(); // These locks are here to avoid contention on file creation, therefore it's sufficient // that we take the same lock for the same hash // // These locks are small and should really be spaced out so they don't share cache lines, // but we don't currently access them at particularly high frequency so it should not be // an issue in practice RwLock m_ShardedLocks[256]; inline RwLock& LockForHash(const IoHash& Hash) { return m_ShardedLocks[Hash.Hash[19]]; } }; std::filesystem::path m_RootDir; mutable RwLock m_Lock; std::unordered_map> m_Buckets; // TODO: make this case insensitive std::vector> m_DroppedBuckets; ZenCacheDiskLayer(const ZenCacheDiskLayer&) = delete; ZenCacheDiskLayer& operator=(const ZenCacheDiskLayer&) = delete; }; class ZenCacheNamespace final : public RefCounted, public GcStorage, public GcContributor { public: ZenCacheNamespace(GcManager& Gc, const std::filesystem::path& RootDir); ~ZenCacheNamespace(); bool Get(std::string_view Bucket, const IoHash& HashKey, ZenCacheValue& OutValue); void Put(std::string_view Bucket, const IoHash& HashKey, const ZenCacheValue& Value); bool Drop(); bool DropBucket(std::string_view Bucket); void Flush(); void Scrub(ScrubContext& Ctx); uint64_t DiskLayerThreshold() const { return m_DiskLayerSizeThreshold; } virtual void GatherReferences(GcContext& GcCtx) override; virtual void CollectGarbage(GcContext& GcCtx) override; virtual GcStorageSize StorageSize() const override; private: std::filesystem::path m_RootDir; ZenCacheMemoryLayer m_MemLayer; ZenCacheDiskLayer m_DiskLayer; uint64_t m_DiskLayerSizeThreshold = 1 * 1024; uint64_t m_LastScrubTime = 0; #if ZEN_USE_CACHE_TRACKER std::unique_ptr m_AccessTracker; #endif ZenCacheNamespace(const ZenCacheNamespace&) = delete; ZenCacheNamespace& operator=(const ZenCacheNamespace&) = delete; }; class ZenCacheStore final : public GcStorage, public GcContributor { public: static constexpr std::string_view DefaultNamespace = "!default!"; // This is intentionally not a valid namespace name and will only be used for mapping when no namespace is given static constexpr std::string_view NamespaceDiskPrefix = "ns_"; struct Configuration { std::filesystem::path BasePath; bool AllowAutomaticCreationOfNamespaces = true; }; ZenCacheStore(GcManager& Gc, const Configuration& Configuration); ~ZenCacheStore(); bool Get(std::string_view Namespace, std::string_view Bucket, const IoHash& HashKey, ZenCacheValue& OutValue); void Put(std::string_view Namespace, std::string_view Bucket, const IoHash& HashKey, const ZenCacheValue& Value); bool DropBucket(std::string_view Namespace, std::string_view Bucket); bool DropNamespace(std::string_view Namespace); void Flush(); void Scrub(ScrubContext& Ctx); virtual void GatherReferences(GcContext& GcCtx) override; virtual void CollectGarbage(GcContext& GcCtx) override; virtual GcStorageSize StorageSize() const override; private: ZenCacheNamespace* GetNamespace(std::string_view Namespace); void IterateNamespaces(const std::function& Callback) const; typedef std::unordered_map> NamespaceMap; mutable RwLock m_NamespacesLock; NamespaceMap m_Namespaces; std::vector> m_DroppedNamespaces; GcManager& m_Gc; Configuration m_Configuration; }; void z$_forcelink(); } // namespace zen