// Copyright Epic Games, Inc. All Rights Reserved. #include #include #include #include #include #include #include ZEN_THIRD_PARTY_INCLUDES_START #include ZEN_THIRD_PARTY_INCLUDES_END #if ZEN_WITH_TESTS # include # include #endif // ZEN_WITH_TESTS namespace zen { using namespace std::literals; namespace chunkblock_impl { struct RangeDescriptor { uint64_t RangeStart = 0; uint64_t RangeLength = 0; uint32_t ChunkBlockIndexStart = 0; uint32_t ChunkBlockIndexCount = 0; }; void MergeCheapestRange(std::vector& InOutRanges) { ZEN_ASSERT(InOutRanges.size() > 1); size_t BestRangeIndexToCollapse = SIZE_MAX; uint64_t BestGap = (uint64_t)-1; for (size_t RangeIndex = 0; RangeIndex < InOutRanges.size() - 1; RangeIndex++) { const RangeDescriptor& Range = InOutRanges[RangeIndex]; const RangeDescriptor& NextRange = InOutRanges[RangeIndex + 1]; uint64_t Gap = NextRange.RangeStart - (Range.RangeStart + Range.RangeLength); if (Gap < BestGap) { BestRangeIndexToCollapse = RangeIndex; BestGap = Gap; } else if (Gap == BestGap) { const RangeDescriptor& BestRange = InOutRanges[BestRangeIndexToCollapse]; const RangeDescriptor& BestNextRange = InOutRanges[BestRangeIndexToCollapse + 1]; uint64_t BestMergedSize = (BestNextRange.RangeStart + BestNextRange.RangeLength) - BestRange.RangeStart; uint64_t MergedSize = (NextRange.RangeStart + NextRange.RangeLength) - Range.RangeStart; if (MergedSize < BestMergedSize) { BestRangeIndexToCollapse = RangeIndex; } } } ZEN_ASSERT(BestRangeIndexToCollapse != SIZE_MAX); ZEN_ASSERT(BestRangeIndexToCollapse < InOutRanges.size() - 1); ZEN_ASSERT(BestGap != (uint64_t)-1); RangeDescriptor& BestRange = InOutRanges[BestRangeIndexToCollapse]; const RangeDescriptor& BestNextRange = InOutRanges[BestRangeIndexToCollapse + 1]; BestRange.RangeLength = BestNextRange.RangeStart - BestRange.RangeStart + BestNextRange.RangeLength; BestRange.ChunkBlockIndexCount = BestNextRange.ChunkBlockIndexStart - BestRange.ChunkBlockIndexStart + BestNextRange.ChunkBlockIndexCount; InOutRanges.erase(InOutRanges.begin() + BestRangeIndexToCollapse + 1); } std::vector GetBlockRanges(const ChunkBlockDescription& BlockDescription, const uint64_t ChunkStartOffsetInBlock, std::span BlockChunkIndexNeeded) { ZEN_TRACE_CPU("GetBlockRanges"); std::vector BlockRanges; { uint64_t CurrentOffset = ChunkStartOffsetInBlock; uint32_t ChunkBlockIndex = 0; uint32_t NeedBlockChunkIndexOffset = 0; RangeDescriptor NextRange; while (NeedBlockChunkIndexOffset < BlockChunkIndexNeeded.size() && ChunkBlockIndex < BlockDescription.ChunkRawHashes.size()) { const uint32_t ChunkCompressedLength = BlockDescription.ChunkCompressedLengths[ChunkBlockIndex]; if (ChunkBlockIndex < BlockChunkIndexNeeded[NeedBlockChunkIndexOffset]) { if (NextRange.RangeLength > 0) { BlockRanges.push_back(NextRange); NextRange = {}; } ChunkBlockIndex++; CurrentOffset += ChunkCompressedLength; } else if (ChunkBlockIndex == BlockChunkIndexNeeded[NeedBlockChunkIndexOffset]) { if (NextRange.RangeLength == 0) { NextRange.RangeStart = CurrentOffset; NextRange.ChunkBlockIndexStart = ChunkBlockIndex; } NextRange.RangeLength += ChunkCompressedLength; NextRange.ChunkBlockIndexCount++; ChunkBlockIndex++; CurrentOffset += ChunkCompressedLength; NeedBlockChunkIndexOffset++; } else { ZEN_ASSERT(false); } } if (NextRange.RangeLength > 0) { BlockRanges.push_back(NextRange); } } ZEN_ASSERT(!BlockRanges.empty()); return BlockRanges; } std::vector OptimizeRanges(uint64_t TotalBlockSize, std::span ExactRanges, double LatencySec, uint64_t SpeedBytesPerSec, uint64_t MaxRangeCountPerRequest, uint64_t MaxRangesPerBlock) { ZEN_TRACE_CPU("OptimizeRanges"); ZEN_ASSERT(MaxRangesPerBlock > 0); std::vector Ranges(ExactRanges.begin(), ExactRanges.end()); while (Ranges.size() > MaxRangesPerBlock) { MergeCheapestRange(Ranges); } while (true) { const std::uint64_t RangeTotalSize = std::accumulate(Ranges.begin(), Ranges.end(), uint64_t(0u), [](uint64_t Current, const RangeDescriptor& Value) { return Current + Value.RangeLength; }); const size_t RangeCount = Ranges.size(); const uint64_t RequestCount = MaxRangeCountPerRequest == (uint64_t)-1 ? 1 : (RangeCount + MaxRangeCountPerRequest - 1) / MaxRangeCountPerRequest; uint64_t RequestTimeAsBytes = uint64_t(SpeedBytesPerSec * RequestCount * LatencySec); if (RangeCount == 1) { // Does fetching the full block add less time than the time it takes to complete a single request? if (TotalBlockSize - RangeTotalSize < SpeedBytesPerSec * LatencySec) { const std::uint64_t InitialRangeTotalSize = std::accumulate(ExactRanges.begin(), ExactRanges.end(), uint64_t(0u), [](uint64_t Current, const RangeDescriptor& Value) { return Current + Value.RangeLength; }); ZEN_DEBUG( "Latency round trip takes as long as receiving the extra redundant bytes - go full block, dropping {} of slack, " "adding {} of bytes to fetch, for block of size {}", NiceBytes(TotalBlockSize - RangeTotalSize), NiceBytes(TotalBlockSize - InitialRangeTotalSize), NiceBytes(TotalBlockSize)); return {}; } else { return Ranges; } } if (RequestTimeAsBytes < (TotalBlockSize - RangeTotalSize)) { return Ranges; } if (RangeCount == 2) { // Merge to single range Ranges.front().RangeLength = Ranges.back().RangeStart - Ranges.front().RangeStart + Ranges.back().RangeLength; Ranges.front().ChunkBlockIndexCount = Ranges.back().ChunkBlockIndexStart - Ranges.front().ChunkBlockIndexStart + Ranges.back().ChunkBlockIndexCount; Ranges.pop_back(); } else { MergeCheapestRange(Ranges); } } } } // namespace chunkblock_impl ChunkBlockDescription ParseChunkBlockDescription(const CbObjectView& BlockObject) { ChunkBlockDescription Result; Result.BlockHash = BlockObject["rawHash"sv].AsHash(); if (Result.BlockHash != IoHash::Zero) { Result.HeaderSize = BlockObject["headerSize"sv].AsUInt64(); CbArrayView ChunksArray = BlockObject["rawHashes"sv].AsArrayView(); Result.ChunkRawHashes.reserve(ChunksArray.Num()); for (CbFieldView ChunkView : ChunksArray) { Result.ChunkRawHashes.push_back(ChunkView.AsHash()); } CbArrayView ChunkRawLengthsArray = BlockObject["chunkRawLengths"sv].AsArrayView(); Result.ChunkRawLengths.reserve(ChunkRawLengthsArray.Num()); for (CbFieldView ChunkView : ChunkRawLengthsArray) { Result.ChunkRawLengths.push_back(ChunkView.AsUInt32()); } CbArrayView ChunkCompressedLengthsArray = BlockObject["chunkCompressedLengths"sv].AsArrayView(); Result.ChunkCompressedLengths.reserve(ChunkCompressedLengthsArray.Num()); for (CbFieldView ChunkView : ChunkCompressedLengthsArray) { Result.ChunkCompressedLengths.push_back(ChunkView.AsUInt32()); } } return Result; } std::vector ParseChunkBlockDescriptionList(const CbObjectView& BlocksObject) { if (!BlocksObject) { return {}; } std::vector Result; CbArrayView Blocks = BlocksObject["blocks"sv].AsArrayView(); Result.reserve(Blocks.Num()); for (CbFieldView BlockView : Blocks) { CbObjectView BlockObject = BlockView.AsObjectView(); Result.emplace_back(ParseChunkBlockDescription(BlockObject)); } return Result; } CbObject BuildChunkBlockDescription(const ChunkBlockDescription& Block, CbObjectView MetaData) { ZEN_ASSERT(Block.BlockHash != IoHash::Zero); ZEN_ASSERT(Block.HeaderSize > 0); ZEN_ASSERT(Block.ChunkRawLengths.size() == Block.ChunkRawHashes.size()); ZEN_ASSERT(Block.ChunkCompressedLengths.size() == Block.ChunkRawHashes.size()); CbObjectWriter Writer; Writer.AddHash("rawHash"sv, Block.BlockHash); Writer.AddInteger("headerSize"sv, Block.HeaderSize); Writer.BeginArray("rawHashes"sv); { for (const IoHash& ChunkHash : Block.ChunkRawHashes) { Writer.AddHash(ChunkHash); } } Writer.EndArray(); Writer.BeginArray("chunkRawLengths"); { for (uint32_t ChunkSize : Block.ChunkRawLengths) { Writer.AddInteger(ChunkSize); } } Writer.EndArray(); Writer.BeginArray("chunkCompressedLengths"); { for (uint32_t ChunkSize : Block.ChunkCompressedLengths) { Writer.AddInteger(ChunkSize); } } Writer.EndArray(); Writer.AddObject("metadata", MetaData); return Writer.Save(); } ChunkBlockDescription GetChunkBlockDescription(const SharedBuffer& BlockPayload, const IoHash& RawHash) { ChunkBlockDescription BlockDescription = {{.BlockHash = IoHash::HashBuffer(BlockPayload)}}; if (BlockDescription.BlockHash != RawHash) { throw std::runtime_error(fmt::format("Block {} content hash {} does not match block hash", RawHash, BlockDescription.BlockHash)); } if (IterateChunkBlock( BlockPayload, [&BlockDescription, RawHash](CompressedBuffer&& Chunk, const IoHash& AttachmentHash) { if (CompositeBuffer Decompressed = Chunk.DecompressToComposite(); Decompressed) { IoHash ChunkHash = IoHash::HashBuffer(Decompressed.Flatten()); if (ChunkHash != AttachmentHash) { throw std::runtime_error( fmt::format("Chunk {} in block {} content hash {} does not match chunk", AttachmentHash, RawHash, ChunkHash)); } BlockDescription.ChunkRawHashes.push_back(AttachmentHash); BlockDescription.ChunkRawLengths.push_back(gsl::narrow(Decompressed.GetSize())); BlockDescription.ChunkCompressedLengths.push_back(gsl::narrow(Chunk.GetCompressedSize())); } else { throw std::runtime_error(fmt::format("Chunk {} in block {} is not a compressed buffer", AttachmentHash, RawHash)); } }, BlockDescription.HeaderSize)) { return BlockDescription; } else { throw std::runtime_error(fmt::format("Block {} is malformed", RawHash)); } } CompressedBuffer GenerateChunkBlock(std::vector>&& FetchChunks, ChunkBlockDescription& OutBlock) { const size_t ChunkCount = FetchChunks.size(); std::vector ChunkSegments; ChunkSegments.resize(1); ChunkSegments.reserve(1 + ChunkCount); OutBlock.ChunkRawHashes.reserve(ChunkCount); OutBlock.ChunkRawLengths.reserve(ChunkCount); OutBlock.ChunkCompressedLengths.reserve(ChunkCount); { IoBuffer TempBuffer(ChunkCount * 9); MutableMemoryView View = TempBuffer.GetMutableView(); uint8_t* BufferStartPtr = reinterpret_cast(View.GetData()); uint8_t* BufferEndPtr = BufferStartPtr; BufferEndPtr += WriteVarUInt(gsl::narrow(ChunkCount), BufferEndPtr); for (const auto& It : FetchChunks) { std::pair Chunk = It.second(It.first); uint64_t ChunkSize = 0; std::span Segments = Chunk.second.GetSegments(); for (const SharedBuffer& Segment : Segments) { ZEN_ASSERT(Segment.IsOwned()); ChunkSize += Segment.GetSize(); ChunkSegments.push_back(Segment); } BufferEndPtr += WriteVarUInt(ChunkSize, BufferEndPtr); OutBlock.ChunkRawHashes.push_back(It.first); OutBlock.ChunkRawLengths.push_back(gsl::narrow(Chunk.first)); OutBlock.ChunkCompressedLengths.push_back(gsl::narrow(ChunkSize)); } ZEN_ASSERT(BufferEndPtr <= View.GetDataEnd()); ptrdiff_t TempBufferLength = std::distance(BufferStartPtr, BufferEndPtr); ChunkSegments[0] = SharedBuffer(IoBuffer(TempBuffer, 0, gsl::narrow(TempBufferLength))); OutBlock.HeaderSize = TempBufferLength; } CompressedBuffer CompressedBlock = CompressedBuffer::Compress(CompositeBuffer(std::move(ChunkSegments)), OodleCompressor::Mermaid, OodleCompressionLevel::None); OutBlock.BlockHash = CompressedBlock.DecodeRawHash(); return CompressedBlock; } std::vector ReadChunkBlockHeader(const MemoryView BlockView, uint64_t& OutHeaderSize) { const uint8_t* ReadPtr = reinterpret_cast(BlockView.GetData()); uint32_t NumberSize; uint64_t ChunkCount = ReadVarUInt(ReadPtr, NumberSize); ReadPtr += NumberSize; std::vector ChunkSizes; ChunkSizes.reserve(ChunkCount); while (ChunkCount--) { if (ReadPtr >= BlockView.GetDataEnd()) { throw std::runtime_error("Invalid block header, block data ended unexpectedly"); } uint64_t ChunkSize = ReadVarUInt(ReadPtr, NumberSize); if (ChunkSize > std::numeric_limits::max()) { throw std::runtime_error("Invalid block header, header data is corrupt"); } if (ChunkSize < 1) { throw std::runtime_error("Invalid block header, header data is corrupt"); } ChunkSizes.push_back(gsl::narrow(ChunkSize)); ReadPtr += NumberSize; } uint64_t Offset = std::distance((const uint8_t*)BlockView.GetData(), ReadPtr); OutHeaderSize = Offset; return ChunkSizes; } bool IterateChunkBlock(const SharedBuffer& BlockPayload, std::function Visitor, uint64_t& OutHeaderSize) { ZEN_ASSERT(BlockPayload); if (BlockPayload.GetSize() < 1) { return false; } MemoryView BlockView = BlockPayload.GetView(); std::vector ChunkSizes = ReadChunkBlockHeader(BlockView, OutHeaderSize); uint64_t Offset = OutHeaderSize; OutHeaderSize = Offset; for (uint64_t ChunkSize : ChunkSizes) { IoBuffer Chunk(BlockPayload.AsIoBuffer(), Offset, ChunkSize); IoHash AttachmentRawHash; uint64_t AttachmentRawSize; CompressedBuffer CompressedChunk = CompressedBuffer::FromCompressed(SharedBuffer(Chunk), AttachmentRawHash, AttachmentRawSize); ZEN_ASSERT_SLOW(IoHash::HashBuffer(CompressedChunk.DecompressToComposite()) == AttachmentRawHash); if (!CompressedChunk) { ZEN_ERROR("Invalid chunk in block"); return false; } Visitor(std::move(CompressedChunk), AttachmentRawHash); Offset += ChunkSize; ZEN_ASSERT(Offset <= BlockView.GetSize()); } return true; }; std::vector FindReuseBlocks(LoggerRef InLog, const uint8_t BlockReuseMinPercentLimit, const bool IsVerbose, ReuseBlocksStatistics& Stats, const std::vector& KnownBlocks, std::span ChunkHashes, std::span ChunkIndexes, std::vector& OutUnusedChunkIndexes) { ZEN_TRACE_CPU("FindReuseBlocks"); ZEN_SCOPED_LOG(InLog); // Find all blocks with a usage level higher than MinPercentLimit // Pick out the blocks with usage higher or equal to MinPercentLimit // Sort them with highest size usage - most usage first // Make a list of all chunks and mark them as not found // For each block, recalculate the block has usage percent based on the chunks marked as not found // If the block still reaches MinPercentLimit, keep it and remove the matching chunks from the not found list // Repeat for following all remaining block that initially matched MinPercentLimit std::vector FilteredReuseBlockIndexes; uint32_t ChunkCount = gsl::narrow(ChunkHashes.size()); std::vector ChunkFound(ChunkCount, false); if (ChunkCount > 0) { size_t AcceptedChunkCount = 0; if (!KnownBlocks.empty()) { Stopwatch ReuseTimer; tsl::robin_map ChunkHashToChunkIndex; ChunkHashToChunkIndex.reserve(ChunkIndexes.size()); for (uint32_t ChunkIndex : ChunkIndexes) { ChunkHashToChunkIndex.insert_or_assign(ChunkHashes[ChunkIndex], ChunkIndex); } std::vector BlockSizes(KnownBlocks.size(), 0); std::vector BlockUseSize(KnownBlocks.size(), 0); std::vector ReuseBlockIndexes; for (size_t KnownBlockIndex = 0; KnownBlockIndex < KnownBlocks.size(); KnownBlockIndex++) { const ChunkBlockDescription& KnownBlock = KnownBlocks[KnownBlockIndex]; if (KnownBlock.BlockHash != IoHash::Zero && KnownBlock.ChunkRawHashes.size() == KnownBlock.ChunkCompressedLengths.size()) { size_t BlockAttachmentCount = KnownBlock.ChunkRawHashes.size(); if (BlockAttachmentCount == 0) { continue; } size_t ReuseSize = 0; size_t BlockSize = 0; size_t FoundAttachmentCount = 0; size_t BlockChunkCount = KnownBlock.ChunkRawHashes.size(); for (size_t BlockChunkIndex = 0; BlockChunkIndex < BlockChunkCount; BlockChunkIndex++) { const IoHash& BlockChunkHash = KnownBlock.ChunkRawHashes[BlockChunkIndex]; const uint32_t BlockChunkSize = KnownBlock.ChunkCompressedLengths[BlockChunkIndex]; BlockSize += BlockChunkSize; if (ChunkHashToChunkIndex.contains(BlockChunkHash)) { ReuseSize += BlockChunkSize; FoundAttachmentCount++; } } size_t ReusePercent = (ReuseSize * 100) / BlockSize; if (ReusePercent >= BlockReuseMinPercentLimit) { if (IsVerbose) { ZEN_INFO("Reusing block {}. {} attachments found, usage level: {}%", KnownBlock.BlockHash, FoundAttachmentCount, ReusePercent); } ReuseBlockIndexes.push_back(KnownBlockIndex); BlockSizes[KnownBlockIndex] = BlockSize; BlockUseSize[KnownBlockIndex] = ReuseSize; } else if (FoundAttachmentCount > 0) { if (IsVerbose) { ZEN_INFO("Skipping block {}. {} attachments found, usage level: {}%", KnownBlock.BlockHash, FoundAttachmentCount, ReusePercent); } Stats.RejectedBlockCount++; Stats.RejectedChunkCount += FoundAttachmentCount; Stats.RejectedByteCount += ReuseSize; } } } if (!ReuseBlockIndexes.empty()) { std::sort(ReuseBlockIndexes.begin(), ReuseBlockIndexes.end(), [&](size_t Lhs, size_t Rhs) { return BlockUseSize[Lhs] > BlockUseSize[Rhs]; }); for (size_t KnownBlockIndex : ReuseBlockIndexes) { std::vector FoundChunkIndexes; size_t BlockSize = 0; size_t AdjustedReuseSize = 0; size_t AdjustedRawReuseSize = 0; const ChunkBlockDescription& KnownBlock = KnownBlocks[KnownBlockIndex]; for (size_t BlockChunkIndex = 0; BlockChunkIndex < KnownBlock.ChunkRawHashes.size(); BlockChunkIndex++) { const IoHash& BlockChunkHash = KnownBlock.ChunkRawHashes[BlockChunkIndex]; const uint32_t BlockChunkSize = KnownBlock.ChunkCompressedLengths[BlockChunkIndex]; BlockSize += BlockChunkSize; if (auto It = ChunkHashToChunkIndex.find(BlockChunkHash); It != ChunkHashToChunkIndex.end()) { const uint32_t ChunkIndex = It->second; if (!ChunkFound[ChunkIndex]) { FoundChunkIndexes.push_back(ChunkIndex); AdjustedReuseSize += KnownBlock.ChunkCompressedLengths[BlockChunkIndex]; AdjustedRawReuseSize += KnownBlock.ChunkRawLengths[BlockChunkIndex]; } } } size_t ReusePercent = (AdjustedReuseSize * 100) / BlockSize; if (ReusePercent >= BlockReuseMinPercentLimit) { if (IsVerbose) { ZEN_INFO("Reusing block {}. {} attachments found, usage level: {}%", KnownBlock.BlockHash, FoundChunkIndexes.size(), ReusePercent); } FilteredReuseBlockIndexes.push_back(KnownBlockIndex); for (uint32_t ChunkIndex : FoundChunkIndexes) { ChunkFound[ChunkIndex] = true; } AcceptedChunkCount += FoundChunkIndexes.size(); Stats.AcceptedChunkCount += FoundChunkIndexes.size(); Stats.AcceptedByteCount += AdjustedReuseSize; Stats.AcceptedRawByteCount += AdjustedRawReuseSize; Stats.AcceptedReduntantChunkCount += KnownBlock.ChunkRawHashes.size() - FoundChunkIndexes.size(); Stats.AcceptedReduntantByteCount += BlockSize - AdjustedReuseSize; } else { if (IsVerbose) { ZEN_INFO("Skipping block {}. filtered usage level: {}%", KnownBlock.BlockHash, ReusePercent); } Stats.RejectedBlockCount++; Stats.RejectedChunkCount += FoundChunkIndexes.size(); Stats.RejectedByteCount += AdjustedReuseSize; } } } } OutUnusedChunkIndexes.reserve(ChunkIndexes.size() - AcceptedChunkCount); for (uint32_t ChunkIndex : ChunkIndexes) { if (!ChunkFound[ChunkIndex]) { OutUnusedChunkIndexes.push_back(ChunkIndex); } } } return FilteredReuseBlockIndexes; } ChunkBlockAnalyser::ChunkBlockAnalyser(LoggerRef Log, std::span BlockDescriptions, const Options& Options) : m_Log(Log) , m_BlockDescriptions(BlockDescriptions) , m_Options(Options) { } std::vector ChunkBlockAnalyser::GetNeeded(const tsl::robin_map& ChunkHashToChunkIndex, std::function&& NeedsBlockChunk) { ZEN_TRACE_CPU("ChunkBlockAnalyser::GetNeeded"); std::vector Result; std::vector ChunkIsNeeded(ChunkHashToChunkIndex.size()); for (uint32_t ChunkIndex = 0; ChunkIndex < ChunkHashToChunkIndex.size(); ChunkIndex++) { ChunkIsNeeded[ChunkIndex] = NeedsBlockChunk(ChunkIndex); } std::vector BlockSlack(m_BlockDescriptions.size(), 0u); for (uint32_t BlockIndex = 0; BlockIndex < m_BlockDescriptions.size(); BlockIndex++) { const ChunkBlockDescription& BlockDescription = m_BlockDescriptions[BlockIndex]; uint64_t BlockUsedSize = 0; uint64_t BlockSize = 0; for (uint32_t ChunkBlockIndex = 0; ChunkBlockIndex < BlockDescription.ChunkRawHashes.size(); ChunkBlockIndex++) { const IoHash& ChunkHash = BlockDescription.ChunkRawHashes[ChunkBlockIndex]; if (auto It = ChunkHashToChunkIndex.find(ChunkHash); It != ChunkHashToChunkIndex.end()) { const uint32_t RemoteChunkIndex = It->second; if (ChunkIsNeeded[RemoteChunkIndex]) { BlockUsedSize += BlockDescription.ChunkCompressedLengths[ChunkBlockIndex]; } } BlockSize += BlockDescription.ChunkCompressedLengths[ChunkBlockIndex]; } BlockSlack[BlockIndex] = BlockSize - BlockUsedSize; } std::vector BlockOrder(m_BlockDescriptions.size()); std::iota(BlockOrder.begin(), BlockOrder.end(), 0); std::sort(BlockOrder.begin(), BlockOrder.end(), [&BlockSlack](uint32_t Lhs, uint32_t Rhs) { return BlockSlack[Lhs] < BlockSlack[Rhs]; }); std::vector ChunkIsPickedUp(ChunkHashToChunkIndex.size(), false); for (uint32_t BlockIndex : BlockOrder) { const ChunkBlockDescription& BlockDescription = m_BlockDescriptions[BlockIndex]; std::vector BlockChunkIndexNeeded; for (uint32_t ChunkBlockIndex = 0; ChunkBlockIndex < BlockDescription.ChunkRawHashes.size(); ChunkBlockIndex++) { const IoHash& ChunkHash = BlockDescription.ChunkRawHashes[ChunkBlockIndex]; if (auto It = ChunkHashToChunkIndex.find(ChunkHash); It != ChunkHashToChunkIndex.end()) { const uint32_t RemoteChunkIndex = It->second; if (ChunkIsNeeded[RemoteChunkIndex]) { if (!ChunkIsPickedUp[RemoteChunkIndex]) { ChunkIsPickedUp[RemoteChunkIndex] = true; BlockChunkIndexNeeded.push_back(ChunkBlockIndex); } } } else { ZEN_DEBUG("Chunk {} not found in block {}", ChunkHash, BlockDescription.BlockHash); } } if (!BlockChunkIndexNeeded.empty()) { Result.push_back(NeededBlock{.BlockIndex = BlockIndex, .ChunkIndexes = std::move(BlockChunkIndexNeeded)}); } } return Result; } ChunkBlockAnalyser::BlockResult ChunkBlockAnalyser::CalculatePartialBlockDownloads(std::span NeededBlocks, std::span BlockPartialDownloadModes) { ZEN_TRACE_CPU("ChunkBlockAnalyser::CalculatePartialBlockDownloads"); Stopwatch PartialAnalisysTimer; ChunkBlockAnalyser::BlockResult Result; { uint64_t MinRequestCount = 0; uint64_t RequestCount = 0; uint64_t RangeCount = 0; uint64_t IdealDownloadTotalSize = 0; uint64_t ActualDownloadTotalSize = 0; uint64_t FullDownloadTotalSize = 0; for (const NeededBlock& NeededBlock : NeededBlocks) { const ChunkBlockDescription& BlockDescription = m_BlockDescriptions[NeededBlock.BlockIndex]; std::span BlockChunkIndexNeeded(NeededBlock.ChunkIndexes); const uint32_t ChunkStartOffsetInBlock = gsl::narrow(CompressedBuffer::GetHeaderSizeForNoneEncoder() + BlockDescription.HeaderSize); uint64_t TotalBlockSize = std::accumulate(BlockDescription.ChunkCompressedLengths.begin(), BlockDescription.ChunkCompressedLengths.end(), uint64_t(ChunkStartOffsetInBlock)); uint64_t ExactRangesSize = 0; uint64_t DownloadRangesSize = 0; uint64_t FullDownloadSize = 0; bool CanDoPartialBlockDownload = (BlockDescription.HeaderSize > 0) && (BlockDescription.ChunkCompressedLengths.size() == BlockDescription.ChunkRawHashes.size()); if (NeededBlock.ChunkIndexes.size() == BlockDescription.ChunkRawHashes.size() || !CanDoPartialBlockDownload) { // Full block ExactRangesSize = TotalBlockSize; DownloadRangesSize = TotalBlockSize; FullDownloadSize = TotalBlockSize; MinRequestCount++; RequestCount++; RangeCount++; Result.FullBlockIndexes.push_back(NeededBlock.BlockIndex); } else if (NeededBlock.ChunkIndexes.empty()) { // Not needed } else { FullDownloadSize = TotalBlockSize; std::vector Ranges = chunkblock_impl::GetBlockRanges(BlockDescription, ChunkStartOffsetInBlock, BlockChunkIndexNeeded); ExactRangesSize = std::accumulate( Ranges.begin(), Ranges.end(), uint64_t(0), [](uint64_t Current, const chunkblock_impl::RangeDescriptor& Range) { return Current + Range.RangeLength; }); EPartialBlockDownloadMode PartialBlockDownloadMode = BlockPartialDownloadModes[NeededBlock.BlockIndex]; if (PartialBlockDownloadMode == EPartialBlockDownloadMode::Off) { // Use full block MinRequestCount++; RangeCount++; RequestCount++; Result.FullBlockIndexes.push_back(NeededBlock.BlockIndex); DownloadRangesSize = TotalBlockSize; } else { const bool IsHighSpeed = (PartialBlockDownloadMode == EPartialBlockDownloadMode::MultiRangeHighSpeed); uint64_t MaxRangeCountPerRequest = IsHighSpeed ? m_Options.HostHighSpeedMaxRangeCountPerRequest : m_Options.HostMaxRangeCountPerRequest; ZEN_ASSERT(MaxRangeCountPerRequest != 0); if (PartialBlockDownloadMode == EPartialBlockDownloadMode::Exact) { // Use exact ranges for (const chunkblock_impl::RangeDescriptor& Range : Ranges) { Result.BlockRanges.push_back(BlockRangeDescriptor{.BlockIndex = NeededBlock.BlockIndex, .RangeStart = Range.RangeStart, .RangeLength = Range.RangeLength, .ChunkBlockIndexStart = Range.ChunkBlockIndexStart, .ChunkBlockIndexCount = Range.ChunkBlockIndexCount}); } MinRequestCount++; RangeCount += Ranges.size(); RequestCount += MaxRangeCountPerRequest == (uint64_t)-1 ? 1 : (Ranges.size() + MaxRangeCountPerRequest - 1) / MaxRangeCountPerRequest; DownloadRangesSize = ExactRangesSize; } else { if (PartialBlockDownloadMode == EPartialBlockDownloadMode::SingleRange) { // Use single range if (Ranges.size() > 1) { Ranges = {chunkblock_impl::RangeDescriptor{ .RangeStart = Ranges.front().RangeStart, .RangeLength = Ranges.back().RangeStart + Ranges.back().RangeLength - Ranges.front().RangeStart, .ChunkBlockIndexStart = Ranges.front().ChunkBlockIndexStart, .ChunkBlockIndexCount = Ranges.back().ChunkBlockIndexStart + Ranges.back().ChunkBlockIndexCount - Ranges.front().ChunkBlockIndexStart}}; } // We still do the optimize pass to see if it is more effective to use a full block } double LatencySec = IsHighSpeed ? m_Options.HostHighSpeedLatencySec : m_Options.HostLatencySec; uint64_t SpeedBytesPerSec = IsHighSpeed ? m_Options.HostHighSpeedBytesPerSec : m_Options.HostSpeedBytesPerSec; if (LatencySec > 0.0 && SpeedBytesPerSec > 0u) { Ranges = chunkblock_impl::OptimizeRanges(TotalBlockSize, Ranges, LatencySec, SpeedBytesPerSec, MaxRangeCountPerRequest, m_Options.MaxRangesPerBlock); } MinRequestCount++; if (Ranges.empty()) { Result.FullBlockIndexes.push_back(NeededBlock.BlockIndex); RequestCount++; RangeCount++; DownloadRangesSize = TotalBlockSize; } else { for (const chunkblock_impl::RangeDescriptor& Range : Ranges) { Result.BlockRanges.push_back(BlockRangeDescriptor{.BlockIndex = NeededBlock.BlockIndex, .RangeStart = Range.RangeStart, .RangeLength = Range.RangeLength, .ChunkBlockIndexStart = Range.ChunkBlockIndexStart, .ChunkBlockIndexCount = Range.ChunkBlockIndexCount}); } RangeCount += Ranges.size(); RequestCount += MaxRangeCountPerRequest == (uint64_t)-1 ? 1 : (Ranges.size() + MaxRangeCountPerRequest - 1) / MaxRangeCountPerRequest; } DownloadRangesSize = Ranges.empty() ? TotalBlockSize : std::accumulate(Ranges.begin(), Ranges.end(), uint64_t(0), [](uint64_t Current, const chunkblock_impl::RangeDescriptor& Range) { return Current + Range.RangeLength; }); } } } IdealDownloadTotalSize += ExactRangesSize; ActualDownloadTotalSize += DownloadRangesSize; FullDownloadTotalSize += FullDownloadSize; if (ExactRangesSize < FullDownloadSize) { ZEN_DEBUG("Block {}: Full: {}, Ideal: {}, Actual: {}, Saves: {}", NeededBlock.BlockIndex, NiceBytes(FullDownloadSize), NiceBytes(ExactRangesSize), NiceBytes(DownloadRangesSize), NiceBytes(FullDownloadSize - DownloadRangesSize)); } } uint64_t Actual = FullDownloadTotalSize - ActualDownloadTotalSize; uint64_t Ideal = FullDownloadTotalSize - IdealDownloadTotalSize; if (Ideal < FullDownloadTotalSize && !m_Options.IsQuiet) { const double AchievedPercent = Ideal == 0 ? 100.0 : (100.0 * Actual) / Ideal; ZEN_INFO( "Block Partial Analysis: Blocks: {}, Full: {}, Ideal: {}, Actual: {}. Skipping {} ({:.1f}%) out of " "possible {} using {} extra ranges " "via {} extra requests. Completed in {}", NeededBlocks.size(), NiceBytes(FullDownloadTotalSize), NiceBytes(IdealDownloadTotalSize), NiceBytes(ActualDownloadTotalSize), NiceBytes(FullDownloadTotalSize - ActualDownloadTotalSize), AchievedPercent, NiceBytes(Ideal), RangeCount - MinRequestCount, RequestCount - MinRequestCount, NiceTimeSpanMs(PartialAnalisysTimer.GetElapsedTimeMs())); } } return Result; } #if ZEN_WITH_TESTS namespace chunkblock_testutils { static std::vector> CreateAttachments( const std::span& Sizes, OodleCompressionLevel CompressionLevel = OodleCompressionLevel::VeryFast, uint64_t BlockSize = 0) { std::vector> Result; Result.reserve(Sizes.size()); for (size_t Size : Sizes) { CompressedBuffer Compressed = CompressedBuffer::Compress(SharedBuffer(CreateSemiRandomBlob(Size)), OodleCompressor::Mermaid, CompressionLevel, BlockSize); Result.emplace_back(std::pair(Oid::NewOid(), Compressed)); } return Result; } } // namespace chunkblock_testutils TEST_SUITE_BEGIN("remotestore.chunkblock"); TEST_CASE("chunkblock.block") { using namespace std::literals; using namespace chunkblock_testutils; std::vector AttachmentSizes({7633, 6825, 5738, 8031, 7225, 566, 3656, 6006, 24, 3466, 1093, 4269, 2257, 3685, 3489, 7194, 6151, 5482, 6217, 3511, 6738, 5061, 7537, 2759, 1916, 8210, 2235, 4024, 1582, 5251, 491, 5464, 4607, 8135, 3767, 4045, 4415, 5007, 8876, 6761, 3359, 8526, 4097, 4855, 8225}); std::vector> AttachmentsWithId = CreateAttachments(AttachmentSizes); std::vector> Chunks; Chunks.reserve(AttachmentSizes.size()); for (const auto& It : AttachmentsWithId) { Chunks.push_back( std::make_pair(It.second.DecodeRawHash(), [Buffer = It.second](const IoHash&) -> std::pair { return {Buffer.DecodeRawSize(), Buffer.GetCompressed()}; })); } ChunkBlockDescription Block; CompressedBuffer BlockBuffer = GenerateChunkBlock(std::move(Chunks), Block); uint64_t HeaderSize; CHECK(IterateChunkBlock( BlockBuffer.Decompress(), [](CompressedBuffer&&, const IoHash&) {}, HeaderSize)); } TEST_CASE("chunkblock.reuseblocks") { using namespace std::literals; using namespace chunkblock_testutils; std::vector> BlockAttachmentSizes( {std::vector{7633, 6825, 5738, 8031, 7225, 566, 3656, 6006, 24, 3466, 1093, 4269, 2257, 3685, 3489, 7194, 6151, 5482, 6217, 3511, 6738, 5061, 7537, 2759, 1916, 8210, 2235, 4024, 1582, 5251, 491, 5464, 4607, 8135, 3767, 4045, 4415, 5007, 8876, 6761, 3359, 8526, 4097, 4855, 8225}, {17633, 16825, 15738, 18031, 17225, 11566, 13656, 16006, 11124, 13466, 11093, 14269, 12257, 13685, 13489, 17194, 16151, 15482, 16217, 13511, 16738, 15061, 17537, 12759, 11916, 18210, 12235, 14024, 11582, 15251, 11491, 15464, 14607, 18135, 13767, 14045, 14415, 15007, 18876, 16761, 13359, 18526, 14097, 14855, 18225}}); std::vector BlockDescriptions; for (auto& AttachmentSizes : BlockAttachmentSizes) { std::vector> AttachmentsWithId = CreateAttachments(AttachmentSizes); std::vector> Chunks; Chunks.reserve(AttachmentSizes.size()); for (const auto& It : AttachmentsWithId) { Chunks.push_back( std::make_pair(It.second.DecodeRawHash(), [Buffer = It.second](const IoHash&) -> std::pair { return {Buffer.DecodeRawSize(), Buffer.GetCompressed()}; })); } ChunkBlockDescription Block; CompressedBuffer BlockBuffer = GenerateChunkBlock(std::move(Chunks), Block); BlockDescriptions.emplace_back(std::move(Block)); } LoggerRef LogRef = Log(); { // We use just about all the chunks - should result in use of both blocks ReuseBlocksStatistics ReuseBlocksStats; std::vector ManyChunkHashes; ManyChunkHashes.insert(ManyChunkHashes.end(), BlockDescriptions[0].ChunkRawHashes.begin(), BlockDescriptions[0].ChunkRawHashes.end() - 1); ManyChunkHashes.insert(ManyChunkHashes.end(), BlockDescriptions[1].ChunkRawHashes.begin() + 1, BlockDescriptions[1].ChunkRawHashes.end()); std::vector ManyChunkIndexes; ManyChunkIndexes.resize(ManyChunkHashes.size()); std::iota(ManyChunkIndexes.begin(), ManyChunkIndexes.end(), 0); std::vector UnusedChunkIndexes; std::vector ReusedBlocks = FindReuseBlocks(LogRef, 80, false, ReuseBlocksStats, BlockDescriptions, ManyChunkHashes, ManyChunkIndexes, UnusedChunkIndexes); CHECK_EQ(2u, ReusedBlocks.size()); CHECK_EQ(0u, UnusedChunkIndexes.size()); } { // We now only about one of the blocks ReuseBlocksStatistics ReuseBlocksStats; std::vector ManyChunkHashes; ManyChunkHashes.insert(ManyChunkHashes.end(), BlockDescriptions[0].ChunkRawHashes.begin(), BlockDescriptions[0].ChunkRawHashes.end() - 1); ManyChunkHashes.insert(ManyChunkHashes.end(), BlockDescriptions[1].ChunkRawHashes.begin() + 1, BlockDescriptions[1].ChunkRawHashes.end()); std::vector ManyChunkIndexes; ManyChunkIndexes.resize(ManyChunkHashes.size()); std::iota(ManyChunkIndexes.begin(), ManyChunkIndexes.end(), 0); std::vector UnusedChunkIndexes; std::vector ReusedBlocks = FindReuseBlocks(LogRef, 80, false, ReuseBlocksStats, std::vector{BlockDescriptions[0]}, ManyChunkHashes, ManyChunkIndexes, UnusedChunkIndexes); CHECK_EQ(1u, ReusedBlocks.size()); CHECK_EQ(BlockDescriptions[1].ChunkRawHashes.size() - 1, UnusedChunkIndexes.size()); } { std::vector ManyChunkHashes; ManyChunkHashes.insert(ManyChunkHashes.end(), BlockDescriptions[0].ChunkRawHashes.begin(), BlockDescriptions[0].ChunkRawHashes.end() - BlockDescriptions[0].ChunkRawHashes.size() / 2); ManyChunkHashes.insert(ManyChunkHashes.end(), BlockDescriptions[1].ChunkRawHashes.begin() + BlockDescriptions[1].ChunkRawHashes.size() / 2, BlockDescriptions[1].ChunkRawHashes.end()); std::vector ManyChunkIndexes; ManyChunkIndexes.resize(ManyChunkHashes.size()); std::iota(ManyChunkIndexes.begin(), ManyChunkIndexes.end(), 0); { // We use half the chunks - should result in no use of blocks due to 80% limit std::vector UnusedChunkIndexes80Percent; ReuseBlocksStatistics ReuseBlocksStats; std::vector ReusedBlocks80Percent = FindReuseBlocks(LogRef, 80, false, ReuseBlocksStats, BlockDescriptions, ManyChunkHashes, ManyChunkIndexes, UnusedChunkIndexes80Percent); CHECK_EQ(0u, ReusedBlocks80Percent.size()); CHECK_EQ(ManyChunkHashes.size(), UnusedChunkIndexes80Percent.size()); } { // We use half the chunks - should result in use of both blocks due to 40% limit std::vector UnusedChunkIndexes40Percent; ReuseBlocksStatistics ReuseBlocksStats; std::vector ReusedBlocks40Percent = FindReuseBlocks(LogRef, 40, false, ReuseBlocksStats, BlockDescriptions, ManyChunkHashes, ManyChunkIndexes, UnusedChunkIndexes40Percent); CHECK_EQ(2u, ReusedBlocks40Percent.size()); CHECK_EQ(0u, UnusedChunkIndexes40Percent.size()); } } { std::vector ManyChunkHashes; ManyChunkHashes.insert(ManyChunkHashes.end(), BlockDescriptions[0].ChunkRawHashes.begin(), BlockDescriptions[0].ChunkRawHashes.end() - BlockDescriptions[0].ChunkRawHashes.size() / 2); ManyChunkHashes.insert(ManyChunkHashes.end(), BlockDescriptions[1].ChunkRawHashes.begin() + 1, BlockDescriptions[1].ChunkRawHashes.end()); std::vector ManyChunkIndexes; ManyChunkIndexes.resize(ManyChunkHashes.size()); std::iota(ManyChunkIndexes.begin(), ManyChunkIndexes.end(), 0); { // We use half the chunks for first block - should result in use of one blocks due to 80% limit ReuseBlocksStatistics ReuseBlocksStats; std::vector UnusedChunkIndexes80Percent; std::vector ReusedBlocks80Percent = FindReuseBlocks(LogRef, 80, false, ReuseBlocksStats, BlockDescriptions, ManyChunkHashes, ManyChunkIndexes, UnusedChunkIndexes80Percent); CHECK_EQ(1u, ReusedBlocks80Percent.size()); CHECK_EQ(BlockDescriptions[0].ChunkRawHashes.size() - BlockDescriptions[0].ChunkRawHashes.size() / 2, UnusedChunkIndexes80Percent.size()); } { // We use half the chunks - should result in use of both blocks due to 40% limit ReuseBlocksStatistics ReuseBlocksStats; std::vector UnusedChunkIndexes40Percent; std::vector ReusedBlocks40Percent = FindReuseBlocks(LogRef, 40, false, ReuseBlocksStats, BlockDescriptions, ManyChunkHashes, ManyChunkIndexes, UnusedChunkIndexes40Percent); CHECK_EQ(2u, ReusedBlocks40Percent.size()); CHECK_EQ(0u, UnusedChunkIndexes40Percent.size()); } } { // Test simulate ThinkChunkBlockDescriptions for (ChunkBlockDescription& BlockDescription : BlockDescriptions) { BlockDescription.HeaderSize = 0; BlockDescription.ChunkRawLengths = std::vector(BlockDescription.ChunkRawHashes.size(), 1); BlockDescription.ChunkCompressedLengths = std::vector(BlockDescription.ChunkRawHashes.size(), 1); } std::vector ManyChunkHashes; ManyChunkHashes.insert(ManyChunkHashes.end(), BlockDescriptions[0].ChunkRawHashes.begin(), BlockDescriptions[0].ChunkRawHashes.end() - BlockDescriptions[0].ChunkRawHashes.size() / 2); ManyChunkHashes.insert(ManyChunkHashes.end(), BlockDescriptions[1].ChunkRawHashes.begin() + 1, BlockDescriptions[1].ChunkRawHashes.end()); std::vector ManyChunkIndexes; ManyChunkIndexes.resize(ManyChunkHashes.size()); std::iota(ManyChunkIndexes.begin(), ManyChunkIndexes.end(), 0); { // We use half the chunks for first block - should result in use of one blocks due to 80% limit ReuseBlocksStatistics ReuseBlocksStats; std::vector UnusedChunkIndexes80Percent; std::vector ReusedBlocks80Percent = FindReuseBlocks(LogRef, 80, false, ReuseBlocksStats, BlockDescriptions, ManyChunkHashes, ManyChunkIndexes, UnusedChunkIndexes80Percent); CHECK_EQ(1u, ReusedBlocks80Percent.size()); CHECK_EQ(BlockDescriptions[0].ChunkRawHashes.size() - BlockDescriptions[0].ChunkRawHashes.size() / 2, UnusedChunkIndexes80Percent.size()); } { // We use half the chunks - should result in use of both blocks due to 40% limit ReuseBlocksStatistics ReuseBlocksStats; std::vector UnusedChunkIndexes40Percent; std::vector ReusedBlocks40Percent = FindReuseBlocks(LogRef, 40, false, ReuseBlocksStats, BlockDescriptions, ManyChunkHashes, ManyChunkIndexes, UnusedChunkIndexes40Percent); CHECK_EQ(2u, ReusedBlocks40Percent.size()); CHECK_EQ(0u, UnusedChunkIndexes40Percent.size()); } } } namespace chunkblock_analyser_testutils { // Build a ChunkBlockDescription without any real payload. // Hashes are derived deterministically from (BlockSeed XOR ChunkIndex) so that the same // seed produces the same hashes - useful for deduplication tests. static ChunkBlockDescription MakeBlockDesc(uint64_t HeaderSize, std::initializer_list CompressedLengths, uint32_t BlockSeed = 0) { ChunkBlockDescription Desc; Desc.HeaderSize = HeaderSize; uint32_t ChunkIndex = 0; for (uint32_t Length : CompressedLengths) { uint64_t HashInput = uint64_t(BlockSeed ^ ChunkIndex); Desc.ChunkRawHashes.push_back(IoHash::HashBuffer(MemoryView(&HashInput, sizeof(HashInput)))); Desc.ChunkRawLengths.push_back(Length); Desc.ChunkCompressedLengths.push_back(Length); ChunkIndex++; } return Desc; } // Build the robin_map needed by GetNeeded from a flat list of blocks. // First occurrence of each hash wins; index is assigned sequentially across all blocks. [[maybe_unused]] static tsl::robin_map MakeHashMap(const std::vector& Blocks) { tsl::robin_map Result; uint32_t Index = 0; for (const ChunkBlockDescription& Block : Blocks) { for (const IoHash& Hash : Block.ChunkRawHashes) { if (!Result.contains(Hash)) { Result.emplace(Hash, Index++); } } } return Result; } } // namespace chunkblock_analyser_testutils TEST_CASE("chunkblock.mergecheapestrange.picks_smallest_gap") { using RD = chunkblock_impl::RangeDescriptor; // Gap between ranges 0-1 is 50, gap between 1-2 is 150 -> pair 0-1 gets merged std::vector Ranges = { {.RangeStart = 0, .RangeLength = 100, .ChunkBlockIndexStart = 0, .ChunkBlockIndexCount = 1}, {.RangeStart = 150, .RangeLength = 100, .ChunkBlockIndexStart = 1, .ChunkBlockIndexCount = 1}, {.RangeStart = 400, .RangeLength = 100, .ChunkBlockIndexStart = 2, .ChunkBlockIndexCount = 1}, }; chunkblock_impl::MergeCheapestRange(Ranges); REQUIRE_EQ(2u, Ranges.size()); CHECK_EQ(0u, Ranges[0].RangeStart); CHECK_EQ(250u, Ranges[0].RangeLength); // 150+100 CHECK_EQ(0u, Ranges[0].ChunkBlockIndexStart); CHECK_EQ(2u, Ranges[0].ChunkBlockIndexCount); CHECK_EQ(400u, Ranges[1].RangeStart); CHECK_EQ(100u, Ranges[1].RangeLength); CHECK_EQ(2u, Ranges[1].ChunkBlockIndexStart); CHECK_EQ(1u, Ranges[1].ChunkBlockIndexCount); } TEST_CASE("chunkblock.mergecheapestrange.tiebreak_smaller_merged") { using RD = chunkblock_impl::RangeDescriptor; // Gap 0-1 == gap 1-2 == 100; merged size 0-1 (250) < merged size 1-2 (350) -> pair 0-1 wins std::vector Ranges = { {.RangeStart = 0, .RangeLength = 100, .ChunkBlockIndexStart = 0, .ChunkBlockIndexCount = 1}, {.RangeStart = 200, .RangeLength = 50, .ChunkBlockIndexStart = 1, .ChunkBlockIndexCount = 1}, {.RangeStart = 350, .RangeLength = 200, .ChunkBlockIndexStart = 2, .ChunkBlockIndexCount = 1}, }; chunkblock_impl::MergeCheapestRange(Ranges); REQUIRE_EQ(2u, Ranges.size()); // Pair 0-1 merged: start=0, length = (200+50)-0 = 250 CHECK_EQ(0u, Ranges[0].RangeStart); CHECK_EQ(250u, Ranges[0].RangeLength); CHECK_EQ(0u, Ranges[0].ChunkBlockIndexStart); CHECK_EQ(2u, Ranges[0].ChunkBlockIndexCount); // Pair 1 unchanged (was index 2) CHECK_EQ(350u, Ranges[1].RangeStart); CHECK_EQ(200u, Ranges[1].RangeLength); CHECK_EQ(2u, Ranges[1].ChunkBlockIndexStart); CHECK_EQ(1u, Ranges[1].ChunkBlockIndexCount); } TEST_CASE("chunkblock.optimizeranges.preserves_ranges_low_latency") { using RD = chunkblock_impl::RangeDescriptor; // With MaxRangeCountPerRequest unlimited, RequestCount=1 // RequestTimeAsBytes = 100000 * 1 * 0.001 = 100 << slack=7000 -> all ranges preserved std::vector ExactRanges = { {.RangeStart = 0, .RangeLength = 1000, .ChunkBlockIndexStart = 0, .ChunkBlockIndexCount = 1}, {.RangeStart = 2000, .RangeLength = 1000, .ChunkBlockIndexStart = 1, .ChunkBlockIndexCount = 1}, {.RangeStart = 4000, .RangeLength = 1000, .ChunkBlockIndexStart = 2, .ChunkBlockIndexCount = 1}, }; uint64_t TotalBlockSize = 10000; double LatencySec = 0.001; uint64_t SpeedBytesPerSec = 100000; uint64_t MaxRangeCountPerReq = (uint64_t)-1; uint64_t MaxRangesPerBlock = 1024; auto Result = chunkblock_impl::OptimizeRanges(TotalBlockSize, ExactRanges, LatencySec, SpeedBytesPerSec, MaxRangeCountPerReq, MaxRangesPerBlock); REQUIRE_EQ(3u, Result.size()); } TEST_CASE("chunkblock.optimizeranges.falls_back_to_full_block") { using RD = chunkblock_impl::RangeDescriptor; // 1 range already; slack=100 < SpeedBytesPerSec*LatencySec=200 -> full block (empty result) std::vector ExactRanges = { {.RangeStart = 100, .RangeLength = 900, .ChunkBlockIndexStart = 0, .ChunkBlockIndexCount = 3}, }; uint64_t TotalBlockSize = 1000; double LatencySec = 0.01; uint64_t SpeedBytesPerSec = 20000; uint64_t MaxRangeCountPerReq = (uint64_t)-1; uint64_t MaxRangesPerBlock = 1024; auto Result = chunkblock_impl::OptimizeRanges(TotalBlockSize, ExactRanges, LatencySec, SpeedBytesPerSec, MaxRangeCountPerReq, MaxRangesPerBlock); CHECK(Result.empty()); } TEST_CASE("chunkblock.optimizeranges.maxrangesperblock_clamp") { using RD = chunkblock_impl::RangeDescriptor; // 5 input ranges; MaxRangesPerBlock=2 clamps to <=2 before the cost model runs std::vector ExactRanges = { {.RangeStart = 0, .RangeLength = 100, .ChunkBlockIndexStart = 0, .ChunkBlockIndexCount = 1}, {.RangeStart = 300, .RangeLength = 100, .ChunkBlockIndexStart = 1, .ChunkBlockIndexCount = 1}, {.RangeStart = 600, .RangeLength = 100, .ChunkBlockIndexStart = 2, .ChunkBlockIndexCount = 1}, {.RangeStart = 900, .RangeLength = 100, .ChunkBlockIndexStart = 3, .ChunkBlockIndexCount = 1}, {.RangeStart = 1200, .RangeLength = 100, .ChunkBlockIndexStart = 4, .ChunkBlockIndexCount = 1}, }; uint64_t TotalBlockSize = 5000; double LatencySec = 0.001; uint64_t SpeedBytesPerSec = 100000; uint64_t MaxRangeCountPerReq = (uint64_t)-1; uint64_t MaxRangesPerBlock = 2; auto Result = chunkblock_impl::OptimizeRanges(TotalBlockSize, ExactRanges, LatencySec, SpeedBytesPerSec, MaxRangeCountPerReq, MaxRangesPerBlock); CHECK(Result.size() <= 2u); CHECK(!Result.empty()); } TEST_CASE("chunkblock.optimizeranges.low_maxrangecountperrequest_drives_merge") { using RD = chunkblock_impl::RangeDescriptor; // MaxRangeCountPerRequest=1 means RequestCount==RangeCount; high latency drives merging // With MaxRangeCountPerRequest=-1 the same 3 ranges would be preserved (verified by comment below) std::vector ExactRanges = { {.RangeStart = 100, .RangeLength = 100, .ChunkBlockIndexStart = 0, .ChunkBlockIndexCount = 1}, {.RangeStart = 250, .RangeLength = 100, .ChunkBlockIndexStart = 1, .ChunkBlockIndexCount = 1}, {.RangeStart = 400, .RangeLength = 100, .ChunkBlockIndexStart = 2, .ChunkBlockIndexCount = 1}, }; uint64_t TotalBlockSize = 1000; double LatencySec = 1.0; uint64_t SpeedBytesPerSec = 500; // With MaxRangeCountPerRequest=-1: RequestCount=1, RequestTimeAsBytes=500 < slack=700 -> preserved // With MaxRangeCountPerRequest=1: RequestCount=3, RequestTimeAsBytes=1500 > slack=700 -> merged uint64_t MaxRangesPerBlock = 1024; auto Unlimited = chunkblock_impl::OptimizeRanges(TotalBlockSize, ExactRanges, LatencySec, SpeedBytesPerSec, (uint64_t)-1, MaxRangesPerBlock); CHECK_EQ(3u, Unlimited.size()); auto Limited = chunkblock_impl::OptimizeRanges(TotalBlockSize, ExactRanges, LatencySec, SpeedBytesPerSec, uint64_t(1), MaxRangesPerBlock); CHECK(Limited.size() < 3u); } TEST_CASE("chunkblock.optimizeranges.unlimited_rangecountperrequest_no_extra_cost") { using RD = chunkblock_impl::RangeDescriptor; // MaxRangeCountPerRequest=-1 -> RequestCount always 1, even with many ranges and high latency std::vector ExactRanges = { {.RangeStart = 0, .RangeLength = 50, .ChunkBlockIndexStart = 0, .ChunkBlockIndexCount = 1}, {.RangeStart = 200, .RangeLength = 50, .ChunkBlockIndexStart = 1, .ChunkBlockIndexCount = 1}, {.RangeStart = 400, .RangeLength = 50, .ChunkBlockIndexStart = 2, .ChunkBlockIndexCount = 1}, {.RangeStart = 600, .RangeLength = 50, .ChunkBlockIndexStart = 3, .ChunkBlockIndexCount = 1}, {.RangeStart = 800, .RangeLength = 50, .ChunkBlockIndexStart = 4, .ChunkBlockIndexCount = 1}, }; uint64_t TotalBlockSize = 5000; double LatencySec = 0.1; uint64_t SpeedBytesPerSec = 10000; // RequestTimeAsBytes=1000 << slack=4750 uint64_t MaxRangeCountPerReq = (uint64_t)-1; uint64_t MaxRangesPerBlock = 1024; auto Result = chunkblock_impl::OptimizeRanges(TotalBlockSize, ExactRanges, LatencySec, SpeedBytesPerSec, MaxRangeCountPerReq, MaxRangesPerBlock); CHECK_EQ(5u, Result.size()); } TEST_CASE("chunkblock.optimizeranges.two_range_direct_merge_path") { using RD = chunkblock_impl::RangeDescriptor; // Exactly 2 ranges; cost model demands merge; exercises the RangeCount==2 direct-merge branch // After direct merge -> 1 range with small slack -> full block (empty) std::vector ExactRanges = { {.RangeStart = 0, .RangeLength = 100, .ChunkBlockIndexStart = 0, .ChunkBlockIndexCount = 2}, {.RangeStart = 400, .RangeLength = 100, .ChunkBlockIndexStart = 2, .ChunkBlockIndexCount = 2}, }; uint64_t TotalBlockSize = 600; double LatencySec = 0.1; uint64_t SpeedBytesPerSec = 5000; // RequestTimeAsBytes=500 > slack=400 on first iter uint64_t MaxRangeCountPerReq = (uint64_t)-1; uint64_t MaxRangesPerBlock = 1024; // Iteration 1: RangeCount=2, RequestCount=1, RequestTimeAsBytes=500 > slack=400 -> direct merge // After merge: 1 range [{0,500,0,4}], slack=100 < Speed*Lat=500 -> full block auto Result = chunkblock_impl::OptimizeRanges(TotalBlockSize, ExactRanges, LatencySec, SpeedBytesPerSec, MaxRangeCountPerReq, MaxRangesPerBlock); CHECK(Result.empty()); } TEST_CASE("chunkblock.getneeded.all_chunks") { using namespace chunkblock_analyser_testutils; LoggerRef LogRef = Log(); auto Block = MakeBlockDesc(50, {100, 100, 100, 100}); ChunkBlockAnalyser::Options Options; ChunkBlockAnalyser Analyser(LogRef, std::span(&Block, 1), Options); auto HashMap = MakeHashMap({Block}); auto NeededBlocks = Analyser.GetNeeded(HashMap, [](uint32_t) { return true; }); REQUIRE_EQ(1u, NeededBlocks.size()); CHECK_EQ(0u, NeededBlocks[0].BlockIndex); REQUIRE_EQ(4u, NeededBlocks[0].ChunkIndexes.size()); CHECK_EQ(0u, NeededBlocks[0].ChunkIndexes[0]); CHECK_EQ(1u, NeededBlocks[0].ChunkIndexes[1]); CHECK_EQ(2u, NeededBlocks[0].ChunkIndexes[2]); CHECK_EQ(3u, NeededBlocks[0].ChunkIndexes[3]); } TEST_CASE("chunkblock.getneeded.no_chunks") { using namespace chunkblock_analyser_testutils; LoggerRef LogRef = Log(); auto Block = MakeBlockDesc(50, {100, 100, 100, 100}); ChunkBlockAnalyser::Options Options; ChunkBlockAnalyser Analyser(LogRef, std::span(&Block, 1), Options); auto HashMap = MakeHashMap({Block}); auto NeededBlocks = Analyser.GetNeeded(HashMap, [](uint32_t) { return false; }); CHECK(NeededBlocks.empty()); } TEST_CASE("chunkblock.getneeded.subset_within_block") { using namespace chunkblock_analyser_testutils; LoggerRef LogRef = Log(); auto Block = MakeBlockDesc(50, {100, 100, 100, 100}); ChunkBlockAnalyser::Options Options; ChunkBlockAnalyser Analyser(LogRef, std::span(&Block, 1), Options); auto HashMap = MakeHashMap({Block}); // Indices 0 and 2 are needed; 1 and 3 are not auto NeededBlocks = Analyser.GetNeeded(HashMap, [](uint32_t ChunkIndex) { return ChunkIndex == 0 || ChunkIndex == 2; }); REQUIRE_EQ(1u, NeededBlocks.size()); CHECK_EQ(0u, NeededBlocks[0].BlockIndex); REQUIRE_EQ(2u, NeededBlocks[0].ChunkIndexes.size()); CHECK_EQ(0u, NeededBlocks[0].ChunkIndexes[0]); CHECK_EQ(2u, NeededBlocks[0].ChunkIndexes[1]); } TEST_CASE("chunkblock.getneeded.dedup_low_slack_wins") { using namespace chunkblock_analyser_testutils; LoggerRef LogRef = Log(); // Block 0: {H0, H1, SharedH, H3} - 3 of 4 needed (H3 not needed); slack = 100 // Block 1: {H4, H5, SharedH, H6} - only SharedH needed; slack = 300 // Block 0 has less slack -> processed first -> SharedH assigned to block 0 IoHash SharedH = IoHash::HashBuffer(MemoryView("shared_chunk_dedup", 18)); IoHash H0 = IoHash::HashBuffer(MemoryView("block0_chunk0", 13)); IoHash H1 = IoHash::HashBuffer(MemoryView("block0_chunk1", 13)); IoHash H3 = IoHash::HashBuffer(MemoryView("block0_chunk3", 13)); IoHash H4 = IoHash::HashBuffer(MemoryView("block1_chunk0", 13)); IoHash H5 = IoHash::HashBuffer(MemoryView("block1_chunk1", 13)); IoHash H6 = IoHash::HashBuffer(MemoryView("block1_chunk3", 13)); ChunkBlockDescription Block0; Block0.HeaderSize = 50; Block0.ChunkRawHashes = {H0, H1, SharedH, H3}; Block0.ChunkRawLengths = {100, 100, 100, 100}; Block0.ChunkCompressedLengths = {100, 100, 100, 100}; ChunkBlockDescription Block1; Block1.HeaderSize = 50; Block1.ChunkRawHashes = {H4, H5, SharedH, H6}; Block1.ChunkRawLengths = {100, 100, 100, 100}; Block1.ChunkCompressedLengths = {100, 100, 100, 100}; std::vector Blocks = {Block0, Block1}; ChunkBlockAnalyser::Options Options; ChunkBlockAnalyser Analyser(LogRef, Blocks, Options); // Map: H0->0, H1->1, SharedH->2, H3->3, H4->4, H5->5, H6->6 auto HashMap = MakeHashMap(Blocks); // Need H0(0), H1(1), SharedH(2) from block 0; SharedH from block 1 (already index 2) // H3(3) not needed; H4,H5,H6 not needed auto NeededBlocks = Analyser.GetNeeded(HashMap, [](uint32_t ChunkIndex) { return ChunkIndex <= 2; }); // Block 0 slack=100 (H3 unused), block 1 slack=300 (H4,H5,H6 unused) // Block 0 processed first; picks up H0, H1, SharedH // Block 1 tries SharedH but it's already picked up -> empty -> not added REQUIRE_EQ(1u, NeededBlocks.size()); CHECK_EQ(0u, NeededBlocks[0].BlockIndex); REQUIRE_EQ(3u, NeededBlocks[0].ChunkIndexes.size()); CHECK_EQ(0u, NeededBlocks[0].ChunkIndexes[0]); CHECK_EQ(1u, NeededBlocks[0].ChunkIndexes[1]); CHECK_EQ(2u, NeededBlocks[0].ChunkIndexes[2]); } TEST_CASE("chunkblock.getneeded.dedup_no_double_pickup") { using namespace chunkblock_analyser_testutils; LoggerRef LogRef = Log(); // SharedH appears in both blocks; should appear in the result exactly once IoHash SharedH = IoHash::HashBuffer(MemoryView("shared_chunk_nodup", 18)); IoHash H0 = IoHash::HashBuffer(MemoryView("unique_chunk_b0", 15)); IoHash H1 = IoHash::HashBuffer(MemoryView("unique_chunk_b1a", 16)); IoHash H2 = IoHash::HashBuffer(MemoryView("unique_chunk_b1b", 16)); IoHash H3 = IoHash::HashBuffer(MemoryView("unique_chunk_b1c", 16)); ChunkBlockDescription Block0; Block0.HeaderSize = 50; Block0.ChunkRawHashes = {SharedH, H0}; Block0.ChunkRawLengths = {100, 100}; Block0.ChunkCompressedLengths = {100, 100}; ChunkBlockDescription Block1; Block1.HeaderSize = 50; Block1.ChunkRawHashes = {H1, H2, H3, SharedH}; Block1.ChunkRawLengths = {100, 100, 100, 100}; Block1.ChunkCompressedLengths = {100, 100, 100, 100}; std::vector Blocks = {Block0, Block1}; ChunkBlockAnalyser::Options Options; ChunkBlockAnalyser Analyser(LogRef, Blocks, Options); // Map: SharedH->0, H0->1, H1->2, H2->3, H3->4 // Only SharedH (index 0) needed; no other chunks auto HashMap = MakeHashMap(Blocks); auto NeededBlocks = Analyser.GetNeeded(HashMap, [](uint32_t ChunkIndex) { return ChunkIndex == 0; }); // Block 0: SharedH needed, H0 not needed -> slack=100 // Block 1: SharedH needed, H1/H2/H3 not needed -> slack=300 // Block 0 processed first -> picks up SharedH; Block 1 skips it // Count total occurrences of SharedH across all NeededBlocks uint32_t SharedOccurrences = 0; for (const auto& NB : NeededBlocks) { for (uint32_t Idx : NB.ChunkIndexes) { // SharedH is at block-local index 0 in Block0 and index 3 in Block1 (void)Idx; SharedOccurrences++; } } CHECK_EQ(1u, SharedOccurrences); REQUIRE_EQ(1u, NeededBlocks.size()); CHECK_EQ(0u, NeededBlocks[0].BlockIndex); } TEST_CASE("chunkblock.getneeded.skips_unrequested_chunks") { using namespace chunkblock_analyser_testutils; LoggerRef LogRef = Log(); // Block has 4 chunks but only 2 appear in the hash map -> ChunkIndexes has exactly those 2 auto Block = MakeBlockDesc(50, {100, 100, 100, 100}); ChunkBlockAnalyser::Options Options; ChunkBlockAnalyser Analyser(LogRef, std::span(&Block, 1), Options); // Only put chunks at positions 0 and 2 in the map tsl::robin_map HashMap; HashMap.emplace(Block.ChunkRawHashes[0], 0u); HashMap.emplace(Block.ChunkRawHashes[2], 1u); auto NeededBlocks = Analyser.GetNeeded(HashMap, [](uint32_t) { return true; }); REQUIRE_EQ(1u, NeededBlocks.size()); CHECK_EQ(0u, NeededBlocks[0].BlockIndex); REQUIRE_EQ(2u, NeededBlocks[0].ChunkIndexes.size()); CHECK_EQ(0u, NeededBlocks[0].ChunkIndexes[0]); CHECK_EQ(2u, NeededBlocks[0].ChunkIndexes[1]); } TEST_CASE("chunkblock.getneeded.two_blocks_both_contribute") { using namespace chunkblock_analyser_testutils; LoggerRef LogRef = Log(); // Block 0: all 4 needed (slack=0); block 1: 3 of 4 needed (slack=100) // Both blocks contribute chunks -> 2 NeededBlocks in result auto Block0 = MakeBlockDesc(50, {100, 100, 100, 100}, /*BlockSeed=*/0); auto Block1 = MakeBlockDesc(50, {100, 100, 100, 100}, /*BlockSeed=*/200); std::vector Blocks = {Block0, Block1}; ChunkBlockAnalyser::Options Options; ChunkBlockAnalyser Analyser(LogRef, Blocks, Options); // HashMap: Block0 hashes -> indices 0-3, Block1 hashes -> indices 4-7 auto HashMap = MakeHashMap(Blocks); // Need all Block0 chunks (0-3) and Block1 chunks 0-2 (indices 4-6); not chunk index 7 (Block1 chunk 3) auto NeededBlocks = Analyser.GetNeeded(HashMap, [](uint32_t ChunkIndex) { return ChunkIndex <= 6; }); CHECK_EQ(2u, NeededBlocks.size()); // Block 0 has slack=0 (all 4 needed), Block 1 has slack=100 (1 not needed) // Block 0 comes first in result CHECK_EQ(0u, NeededBlocks[0].BlockIndex); CHECK_EQ(4u, NeededBlocks[0].ChunkIndexes.size()); CHECK_EQ(1u, NeededBlocks[1].BlockIndex); CHECK_EQ(3u, NeededBlocks[1].ChunkIndexes.size()); } TEST_CASE("chunkblock.calc.off_mode") { using namespace chunkblock_analyser_testutils; using Mode = ChunkBlockAnalyser::EPartialBlockDownloadMode; LoggerRef LogRef = Log(); // HeaderSize > 0, chunks size matches -> CanDoPartialBlockDownload = true // But mode Off forces full block regardless auto Block = MakeBlockDesc(50, {100, 200, 300, 400}); ChunkBlockAnalyser::Options Options; Options.IsQuiet = true; ChunkBlockAnalyser Analyser(LogRef, std::span(&Block, 1), Options); std::vector NeededBlocks = {{.BlockIndex = 0, .ChunkIndexes = {0, 2}}}; std::vector Modes = {Mode::Off}; auto Result = Analyser.CalculatePartialBlockDownloads(NeededBlocks, Modes); REQUIRE_EQ(1u, Result.FullBlockIndexes.size()); CHECK_EQ(0u, Result.FullBlockIndexes[0]); CHECK(Result.BlockRanges.empty()); } TEST_CASE("chunkblock.calc.exact_mode") { using namespace chunkblock_analyser_testutils; using Mode = ChunkBlockAnalyser::EPartialBlockDownloadMode; LoggerRef LogRef = Log(); auto Block = MakeBlockDesc(50, {100, 200, 300, 400}); ChunkBlockAnalyser::Options Options; Options.IsQuiet = true; ChunkBlockAnalyser Analyser(LogRef, std::span(&Block, 1), Options); uint64_t ChunkStartOffset = CompressedBuffer::GetHeaderSizeForNoneEncoder() + Block.HeaderSize; // Need chunks 0 and 2 -> 2 non-contiguous ranges; Exact mode passes them straight through std::vector NeededBlocks = {{.BlockIndex = 0, .ChunkIndexes = {0, 2}}}; std::vector Modes = {Mode::Exact}; auto Result = Analyser.CalculatePartialBlockDownloads(NeededBlocks, Modes); CHECK(Result.FullBlockIndexes.empty()); REQUIRE_EQ(2u, Result.BlockRanges.size()); CHECK_EQ(0u, Result.BlockRanges[0].BlockIndex); CHECK_EQ(ChunkStartOffset, Result.BlockRanges[0].RangeStart); CHECK_EQ(100u, Result.BlockRanges[0].RangeLength); CHECK_EQ(0u, Result.BlockRanges[0].ChunkBlockIndexStart); CHECK_EQ(1u, Result.BlockRanges[0].ChunkBlockIndexCount); CHECK_EQ(0u, Result.BlockRanges[1].BlockIndex); CHECK_EQ(ChunkStartOffset + 300u, Result.BlockRanges[1].RangeStart); // 100+200 before chunk 2 CHECK_EQ(300u, Result.BlockRanges[1].RangeLength); CHECK_EQ(2u, Result.BlockRanges[1].ChunkBlockIndexStart); CHECK_EQ(1u, Result.BlockRanges[1].ChunkBlockIndexCount); } TEST_CASE("chunkblock.calc.singlerange_mode") { using namespace chunkblock_analyser_testutils; using Mode = ChunkBlockAnalyser::EPartialBlockDownloadMode; LoggerRef LogRef = Log(); auto Block = MakeBlockDesc(50, {100, 200, 300, 400}); // Default HostLatencySec=-1 -> OptimizeRanges not called after SingleRange collapse ChunkBlockAnalyser::Options Options; Options.IsQuiet = true; ChunkBlockAnalyser Analyser(LogRef, std::span(&Block, 1), Options); uint64_t ChunkStartOffset = CompressedBuffer::GetHeaderSizeForNoneEncoder() + Block.HeaderSize; // Need chunks 0 and 2 -> 2 ranges that get collapsed to 1 std::vector NeededBlocks = {{.BlockIndex = 0, .ChunkIndexes = {0, 2}}}; std::vector Modes = {Mode::SingleRange}; auto Result = Analyser.CalculatePartialBlockDownloads(NeededBlocks, Modes); CHECK(Result.FullBlockIndexes.empty()); REQUIRE_EQ(1u, Result.BlockRanges.size()); CHECK_EQ(0u, Result.BlockRanges[0].BlockIndex); CHECK_EQ(ChunkStartOffset, Result.BlockRanges[0].RangeStart); // Spans from chunk 0 start to chunk 2 end: 100+200+300=600 CHECK_EQ(600u, Result.BlockRanges[0].RangeLength); CHECK_EQ(0u, Result.BlockRanges[0].ChunkBlockIndexStart); // ChunkBlockIndexCount = (2+1) - 0 = 3 CHECK_EQ(3u, Result.BlockRanges[0].ChunkBlockIndexCount); } TEST_CASE("chunkblock.calc.multirange_mode") { using namespace chunkblock_analyser_testutils; using Mode = ChunkBlockAnalyser::EPartialBlockDownloadMode; LoggerRef LogRef = Log(); auto Block = MakeBlockDesc(50, {100, 200, 300, 400}); // Low latency: RequestTimeAsBytes=100 << slack -> OptimizeRanges preserves ranges ChunkBlockAnalyser::Options Options; Options.IsQuiet = true; Options.HostLatencySec = 0.001; Options.HostSpeedBytesPerSec = 100000; ChunkBlockAnalyser Analyser(LogRef, std::span(&Block, 1), Options); uint64_t ChunkStartOffset = CompressedBuffer::GetHeaderSizeForNoneEncoder() + Block.HeaderSize; std::vector NeededBlocks = {{.BlockIndex = 0, .ChunkIndexes = {0, 2}}}; std::vector Modes = {Mode::MultiRange}; auto Result = Analyser.CalculatePartialBlockDownloads(NeededBlocks, Modes); CHECK(Result.FullBlockIndexes.empty()); REQUIRE_EQ(2u, Result.BlockRanges.size()); CHECK_EQ(ChunkStartOffset, Result.BlockRanges[0].RangeStart); CHECK_EQ(100u, Result.BlockRanges[0].RangeLength); CHECK_EQ(ChunkStartOffset + 300u, Result.BlockRanges[1].RangeStart); CHECK_EQ(300u, Result.BlockRanges[1].RangeLength); } TEST_CASE("chunkblock.calc.multirangehighspeed_mode") { using namespace chunkblock_analyser_testutils; using Mode = ChunkBlockAnalyser::EPartialBlockDownloadMode; LoggerRef LogRef = Log(); auto Block = MakeBlockDesc(50, {100, 200, 300, 400}); // Block slack ~= 714 bytes (TotalBlockSize~=1114, RangeTotalSize=400 for chunks 0+2) // RequestTimeAsBytes = 400000 * 1 * 0.001 = 400 < 714 -> ranges preserved ChunkBlockAnalyser::Options Options; Options.IsQuiet = true; Options.HostHighSpeedLatencySec = 0.001; Options.HostHighSpeedBytesPerSec = 400000; ChunkBlockAnalyser Analyser(LogRef, std::span(&Block, 1), Options); uint64_t ChunkStartOffset = CompressedBuffer::GetHeaderSizeForNoneEncoder() + Block.HeaderSize; std::vector NeededBlocks = {{.BlockIndex = 0, .ChunkIndexes = {0, 2}}}; std::vector Modes = {Mode::MultiRangeHighSpeed}; auto Result = Analyser.CalculatePartialBlockDownloads(NeededBlocks, Modes); CHECK(Result.FullBlockIndexes.empty()); REQUIRE_EQ(2u, Result.BlockRanges.size()); CHECK_EQ(ChunkStartOffset, Result.BlockRanges[0].RangeStart); CHECK_EQ(100u, Result.BlockRanges[0].RangeLength); CHECK_EQ(ChunkStartOffset + 300u, Result.BlockRanges[1].RangeStart); CHECK_EQ(300u, Result.BlockRanges[1].RangeLength); } TEST_CASE("chunkblock.calc.all_chunks_needed_full_block") { using namespace chunkblock_analyser_testutils; using Mode = ChunkBlockAnalyser::EPartialBlockDownloadMode; LoggerRef LogRef = Log(); auto Block = MakeBlockDesc(50, {100, 200, 300, 400}); ChunkBlockAnalyser::Options Options; Options.IsQuiet = true; Options.HostLatencySec = 0.001; Options.HostSpeedBytesPerSec = 100000; ChunkBlockAnalyser Analyser(LogRef, std::span(&Block, 1), Options); // All 4 chunks needed -> short-circuit to full block regardless of mode std::vector NeededBlocks = {{.BlockIndex = 0, .ChunkIndexes = {0, 1, 2, 3}}}; std::vector Modes = {Mode::Exact}; auto Result = Analyser.CalculatePartialBlockDownloads(NeededBlocks, Modes); REQUIRE_EQ(1u, Result.FullBlockIndexes.size()); CHECK_EQ(0u, Result.FullBlockIndexes[0]); CHECK(Result.BlockRanges.empty()); } TEST_CASE("chunkblock.calc.headersize_zero_forces_full_block") { using namespace chunkblock_analyser_testutils; using Mode = ChunkBlockAnalyser::EPartialBlockDownloadMode; LoggerRef LogRef = Log(); // HeaderSize=0 -> CanDoPartialBlockDownload=false -> full block even in Exact mode auto Block = MakeBlockDesc(0, {100, 200, 300, 400}); ChunkBlockAnalyser::Options Options; Options.IsQuiet = true; ChunkBlockAnalyser Analyser(LogRef, std::span(&Block, 1), Options); std::vector NeededBlocks = {{.BlockIndex = 0, .ChunkIndexes = {0, 2}}}; std::vector Modes = {Mode::Exact}; auto Result = Analyser.CalculatePartialBlockDownloads(NeededBlocks, Modes); REQUIRE_EQ(1u, Result.FullBlockIndexes.size()); CHECK_EQ(0u, Result.FullBlockIndexes[0]); CHECK(Result.BlockRanges.empty()); } TEST_CASE("chunkblock.calc.low_maxrangecountperrequest") { using namespace chunkblock_analyser_testutils; using Mode = ChunkBlockAnalyser::EPartialBlockDownloadMode; LoggerRef LogRef = Log(); // 5 chunks of 100 bytes each; need chunks 0, 2, 4 -> 3 non-contiguous ranges // With MaxRangeCountPerRequest=1 and high latency, cost model merges aggressively -> full block auto Block = MakeBlockDesc(10, {100, 100, 100, 100, 100}); ChunkBlockAnalyser::Options Options; Options.IsQuiet = true; Options.HostLatencySec = 0.1; Options.HostSpeedBytesPerSec = 1000; Options.HostMaxRangeCountPerRequest = 1; ChunkBlockAnalyser Analyser(LogRef, std::span(&Block, 1), Options); std::vector NeededBlocks = {{.BlockIndex = 0, .ChunkIndexes = {0, 2, 4}}}; std::vector Modes = {Mode::MultiRange}; auto Result = Analyser.CalculatePartialBlockDownloads(NeededBlocks, Modes); // Cost model drives merging: 3 requests x 1000 x 0.1 = 300 > slack ~= 210+headersize // After merges converges to full block REQUIRE_EQ(1u, Result.FullBlockIndexes.size()); CHECK_EQ(0u, Result.FullBlockIndexes[0]); CHECK(Result.BlockRanges.empty()); } TEST_CASE("chunkblock.calc.no_latency_skips_optimize") { using namespace chunkblock_analyser_testutils; using Mode = ChunkBlockAnalyser::EPartialBlockDownloadMode; LoggerRef LogRef = Log(); auto Block = MakeBlockDesc(50, {100, 200, 300, 400}); // Default HostLatencySec=-1 -> OptimizeRanges not called; raw GetBlockRanges result used ChunkBlockAnalyser::Options Options; Options.IsQuiet = true; ChunkBlockAnalyser Analyser(LogRef, std::span(&Block, 1), Options); uint64_t ChunkStartOffset = CompressedBuffer::GetHeaderSizeForNoneEncoder() + Block.HeaderSize; std::vector NeededBlocks = {{.BlockIndex = 0, .ChunkIndexes = {0, 2}}}; std::vector Modes = {Mode::MultiRange}; auto Result = Analyser.CalculatePartialBlockDownloads(NeededBlocks, Modes); // No optimize pass -> exact ranges from GetBlockRanges CHECK(Result.FullBlockIndexes.empty()); REQUIRE_EQ(2u, Result.BlockRanges.size()); CHECK_EQ(ChunkStartOffset, Result.BlockRanges[0].RangeStart); CHECK_EQ(100u, Result.BlockRanges[0].RangeLength); CHECK_EQ(ChunkStartOffset + 300u, Result.BlockRanges[1].RangeStart); CHECK_EQ(300u, Result.BlockRanges[1].RangeLength); } TEST_CASE("chunkblock.calc.multiple_blocks_different_modes") { using namespace chunkblock_analyser_testutils; using Mode = ChunkBlockAnalyser::EPartialBlockDownloadMode; LoggerRef LogRef = Log(); // 3 blocks with different modes: Off, Exact, MultiRange auto Block0 = MakeBlockDesc(50, {100, 200, 300, 400}, /*BlockSeed=*/0); auto Block1 = MakeBlockDesc(50, {100, 200, 300, 400}, /*BlockSeed=*/10); auto Block2 = MakeBlockDesc(50, {100, 200, 300, 400}, /*BlockSeed=*/20); ChunkBlockAnalyser::Options Options; Options.IsQuiet = true; Options.HostLatencySec = 0.001; Options.HostSpeedBytesPerSec = 100000; std::vector Blocks = {Block0, Block1, Block2}; ChunkBlockAnalyser Analyser(LogRef, Blocks, Options); uint64_t ChunkStartOffset = CompressedBuffer::GetHeaderSizeForNoneEncoder() + 50; std::vector NeededBlocks = { {.BlockIndex = 0, .ChunkIndexes = {0, 2}}, {.BlockIndex = 1, .ChunkIndexes = {0, 2}}, {.BlockIndex = 2, .ChunkIndexes = {0, 2}}, }; std::vector Modes = {Mode::Off, Mode::Exact, Mode::MultiRange}; auto Result = Analyser.CalculatePartialBlockDownloads(NeededBlocks, Modes); // Block 0: Off -> FullBlockIndexes REQUIRE_EQ(1u, Result.FullBlockIndexes.size()); CHECK_EQ(0u, Result.FullBlockIndexes[0]); // Block 1: Exact -> 2 ranges; Block 2: MultiRange (low latency) -> 2 ranges // Total: 4 ranges REQUIRE_EQ(4u, Result.BlockRanges.size()); // First 2 ranges belong to Block 1 (Exact) CHECK_EQ(1u, Result.BlockRanges[0].BlockIndex); CHECK_EQ(ChunkStartOffset, Result.BlockRanges[0].RangeStart); CHECK_EQ(100u, Result.BlockRanges[0].RangeLength); CHECK_EQ(1u, Result.BlockRanges[1].BlockIndex); CHECK_EQ(ChunkStartOffset + 300u, Result.BlockRanges[1].RangeStart); CHECK_EQ(300u, Result.BlockRanges[1].RangeLength); // Last 2 ranges belong to Block 2 (MultiRange preserved) CHECK_EQ(2u, Result.BlockRanges[2].BlockIndex); CHECK_EQ(ChunkStartOffset, Result.BlockRanges[2].RangeStart); CHECK_EQ(100u, Result.BlockRanges[2].RangeLength); CHECK_EQ(2u, Result.BlockRanges[3].BlockIndex); CHECK_EQ(ChunkStartOffset + 300u, Result.BlockRanges[3].RangeStart); CHECK_EQ(300u, Result.BlockRanges[3].RangeLength); } TEST_CASE("chunkblock.getblockranges.first_chunk_only") { using namespace chunkblock_analyser_testutils; auto Block = MakeBlockDesc(50, {100, 200, 300, 400}); uint64_t ChunkStartOffset = CompressedBuffer::GetHeaderSizeForNoneEncoder() + Block.HeaderSize; std::vector Needed = {0}; auto Ranges = chunkblock_impl::GetBlockRanges(Block, ChunkStartOffset, Needed); REQUIRE_EQ(1u, Ranges.size()); CHECK_EQ(ChunkStartOffset, Ranges[0].RangeStart); CHECK_EQ(100u, Ranges[0].RangeLength); CHECK_EQ(0u, Ranges[0].ChunkBlockIndexStart); CHECK_EQ(1u, Ranges[0].ChunkBlockIndexCount); } TEST_CASE("chunkblock.getblockranges.last_chunk_only") { using namespace chunkblock_analyser_testutils; auto Block = MakeBlockDesc(50, {100, 200, 300, 400}); uint64_t ChunkStartOffset = CompressedBuffer::GetHeaderSizeForNoneEncoder() + Block.HeaderSize; std::vector Needed = {3}; auto Ranges = chunkblock_impl::GetBlockRanges(Block, ChunkStartOffset, Needed); REQUIRE_EQ(1u, Ranges.size()); CHECK_EQ(ChunkStartOffset + 600u, Ranges[0].RangeStart); // 100+200+300 before chunk 3 CHECK_EQ(400u, Ranges[0].RangeLength); CHECK_EQ(3u, Ranges[0].ChunkBlockIndexStart); CHECK_EQ(1u, Ranges[0].ChunkBlockIndexCount); } TEST_CASE("chunkblock.getblockranges.middle_chunk_only") { using namespace chunkblock_analyser_testutils; auto Block = MakeBlockDesc(50, {100, 200, 300, 400}); uint64_t ChunkStartOffset = CompressedBuffer::GetHeaderSizeForNoneEncoder() + Block.HeaderSize; std::vector Needed = {1}; auto Ranges = chunkblock_impl::GetBlockRanges(Block, ChunkStartOffset, Needed); REQUIRE_EQ(1u, Ranges.size()); CHECK_EQ(ChunkStartOffset + 100u, Ranges[0].RangeStart); // 100 before chunk 1 CHECK_EQ(200u, Ranges[0].RangeLength); CHECK_EQ(1u, Ranges[0].ChunkBlockIndexStart); CHECK_EQ(1u, Ranges[0].ChunkBlockIndexCount); } TEST_CASE("chunkblock.getblockranges.all_chunks") { using namespace chunkblock_analyser_testutils; auto Block = MakeBlockDesc(50, {100, 200, 300, 400}); uint64_t ChunkStartOffset = CompressedBuffer::GetHeaderSizeForNoneEncoder() + Block.HeaderSize; std::vector Needed = {0, 1, 2, 3}; auto Ranges = chunkblock_impl::GetBlockRanges(Block, ChunkStartOffset, Needed); REQUIRE_EQ(1u, Ranges.size()); CHECK_EQ(ChunkStartOffset, Ranges[0].RangeStart); CHECK_EQ(1000u, Ranges[0].RangeLength); // 100+200+300+400 CHECK_EQ(0u, Ranges[0].ChunkBlockIndexStart); CHECK_EQ(4u, Ranges[0].ChunkBlockIndexCount); } TEST_CASE("chunkblock.getblockranges.non_contiguous") { using namespace chunkblock_analyser_testutils; // Chunks 0 and 2 needed, chunk 1 skipped -> two separate ranges auto Block = MakeBlockDesc(50, {100, 200, 300}); uint64_t ChunkStartOffset = CompressedBuffer::GetHeaderSizeForNoneEncoder() + Block.HeaderSize; std::vector Needed = {0, 2}; auto Ranges = chunkblock_impl::GetBlockRanges(Block, ChunkStartOffset, Needed); REQUIRE_EQ(2u, Ranges.size()); CHECK_EQ(ChunkStartOffset, Ranges[0].RangeStart); CHECK_EQ(100u, Ranges[0].RangeLength); CHECK_EQ(0u, Ranges[0].ChunkBlockIndexStart); CHECK_EQ(1u, Ranges[0].ChunkBlockIndexCount); CHECK_EQ(ChunkStartOffset + 300u, Ranges[1].RangeStart); // 100+200 before chunk 2 CHECK_EQ(300u, Ranges[1].RangeLength); CHECK_EQ(2u, Ranges[1].ChunkBlockIndexStart); CHECK_EQ(1u, Ranges[1].ChunkBlockIndexCount); } TEST_CASE("chunkblock.getblockranges.contiguous_run") { using namespace chunkblock_analyser_testutils; // Chunks 1, 2, 3 needed (consecutive) -> one merged range auto Block = MakeBlockDesc(50, {50, 100, 150, 200, 250}); uint64_t ChunkStartOffset = CompressedBuffer::GetHeaderSizeForNoneEncoder() + Block.HeaderSize; std::vector Needed = {1, 2, 3}; auto Ranges = chunkblock_impl::GetBlockRanges(Block, ChunkStartOffset, Needed); REQUIRE_EQ(1u, Ranges.size()); CHECK_EQ(ChunkStartOffset + 50u, Ranges[0].RangeStart); // 50 before chunk 1 CHECK_EQ(450u, Ranges[0].RangeLength); // 100+150+200 CHECK_EQ(1u, Ranges[0].ChunkBlockIndexStart); CHECK_EQ(3u, Ranges[0].ChunkBlockIndexCount); } TEST_SUITE_END(); void chunkblock_forcelink() { } #endif // ZEN_WITH_TESTS } // namespace zen