diff options
| author | Dan Engelbrecht <[email protected]> | 2024-03-20 15:13:03 +0100 |
|---|---|---|
| committer | GitHub Enterprise <[email protected]> | 2024-03-20 15:13:03 +0100 |
| commit | d6071e029b7cb9eec6abfa612b16abc16c84e6a3 (patch) | |
| tree | 21745ab3bb73594a56b2fc548022d900df8ea62f /src | |
| parent | remove hv tags on actions since they are no longer useful (diff) | |
| download | zen-d6071e029b7cb9eec6abfa612b16abc16c84e6a3.tar.xz zen-d6071e029b7cb9eec6abfa612b16abc16c84e6a3.zip | |
non memory copy compressed range (#13)
* Add CompressedBuffer::GetRange that references source data rather than make a memory copy
* Use Compressed.CopyRange in project store GetChunkRange
* docs for CompressedBuffer::CopyRange and CompressedBuffer::GetRange
Diffstat (limited to 'src')
| -rw-r--r-- | src/zencore/compress.cpp | 192 | ||||
| -rw-r--r-- | src/zencore/include/zencore/compress.h | 32 | ||||
| -rw-r--r-- | src/zenserver/projectstore/httpprojectstore.cpp | 9 | ||||
| -rw-r--r-- | src/zenserver/projectstore/projectstore.cpp | 56 | ||||
| -rw-r--r-- | src/zenserver/projectstore/projectstore.h | 6 | ||||
| -rw-r--r-- | src/zenserver/vfs/vfsimpl.cpp | 26 |
6 files changed, 277 insertions, 44 deletions
diff --git a/src/zencore/compress.cpp b/src/zencore/compress.cpp index a8e8a79f4..58be65f13 100644 --- a/src/zencore/compress.cpp +++ b/src/zencore/compress.cpp @@ -1097,6 +1097,88 @@ ValidBufferOrEmpty(BufferType&& CompressedData, IoHash& OutRawHash, uint64_t& Ou } CompositeBuffer +GetCompressedRange(const BufferHeader& Header, const CompositeBuffer& CompressedData, uint64_t RawOffset, uint64_t RawSize) +{ + if (Header.TotalRawSize < RawOffset + RawSize) + { + return CompositeBuffer(); + } + if (Header.Method == CompressionMethod::None) + { + BufferHeader NewHeader = Header; + NewHeader.Crc32 = 0; + NewHeader.TotalRawSize = RawSize; + NewHeader.TotalCompressedSize = NewHeader.TotalRawSize + sizeof(BufferHeader); + NewHeader.RawHash = BLAKE3(); + + UniqueBuffer HeaderData = UniqueBuffer::Alloc(sizeof(BufferHeader)); + NewHeader.Write(HeaderData); + + return CompositeBuffer(HeaderData.MoveToShared(), CompressedData.Mid(sizeof(BufferHeader) + RawOffset, RawSize).MakeOwned()); + } + else + { + UniqueBuffer BlockSizeBuffer; + MemoryView BlockSizeView = + CompressedData.ViewOrCopyRange(sizeof(BufferHeader), Header.BlockCount * sizeof(uint32_t), BlockSizeBuffer); + std::span<uint32_t const> CompressedBlockSizes(reinterpret_cast<const uint32_t*>(BlockSizeView.GetData()), Header.BlockCount); + + const uint64_t BlockSize = uint64_t(1) << Header.BlockSizeExponent; + const uint64_t LastBlockSize = BlockSize - ((Header.BlockCount * BlockSize) - Header.TotalRawSize); + const size_t FirstBlock = uint64_t(RawOffset / BlockSize); + const size_t LastBlock = uint64_t((RawOffset + RawSize - 1) / BlockSize); + uint64_t CompressedOffset = sizeof(BufferHeader) + uint64_t(Header.BlockCount) * sizeof(uint32_t); + + const uint64_t NewBlockCount = LastBlock - FirstBlock + 1; + const uint64_t NewMetaSize = NewBlockCount * sizeof(uint32_t); + uint64_t NewCompressedSize = 0; + uint64_t NewTotalRawSize = 0; + std::vector<uint32_t> NewCompressedBlockSizes; + + NewCompressedBlockSizes.reserve(NewBlockCount); + for (size_t BlockIndex = FirstBlock; BlockIndex <= LastBlock; ++BlockIndex) + { + const uint64_t UncompressedBlockSize = (BlockIndex == Header.BlockCount - 1) ? LastBlockSize : BlockSize; + NewTotalRawSize += UncompressedBlockSize; + + const uint32_t CompressedBlockSize = CompressedBlockSizes[BlockIndex]; + NewCompressedBlockSizes.push_back(CompressedBlockSize); + NewCompressedSize += ByteSwap(CompressedBlockSize); + } + + const uint64_t NewTotalCompressedSize = sizeof(BufferHeader) + NewBlockCount * sizeof(uint32_t) + NewCompressedSize; + const uint64_t NewCompressedHeaderSize = sizeof(BufferHeader) + NewBlockCount * sizeof(uint32_t); + UniqueBuffer NewCompressedHeaderData = UniqueBuffer::Alloc(NewCompressedHeaderSize); + + // Seek to first compressed block + for (size_t BlockIndex = 0; BlockIndex < FirstBlock; ++BlockIndex) + { + const uint64_t CompressedBlockSize = ByteSwap(CompressedBlockSizes[BlockIndex]); + CompressedOffset += CompressedBlockSize; + } + + CompositeBuffer NewCompressedData = CompressedData.Mid(CompressedOffset, NewCompressedSize).MakeOwned(); + + // Copy block sizes + NewCompressedHeaderData.GetMutableView().Mid(sizeof(BufferHeader), NewMetaSize).CopyFrom(MakeMemoryView(NewCompressedBlockSizes)); + + BufferHeader NewHeader; + NewHeader.Crc32 = 0; + NewHeader.Method = Header.Method; + NewHeader.Compressor = Header.Compressor; + NewHeader.CompressionLevel = Header.CompressionLevel; + NewHeader.BlockSizeExponent = Header.BlockSizeExponent; + NewHeader.BlockCount = static_cast<uint32_t>(NewBlockCount); + NewHeader.TotalRawSize = NewTotalRawSize; + NewHeader.TotalCompressedSize = NewTotalCompressedSize; + NewHeader.RawHash = BLAKE3(); + NewHeader.Write(NewCompressedHeaderData.GetMutableView().Left(sizeof(BufferHeader) + NewMetaSize)); + + return CompositeBuffer(NewCompressedHeaderData.MoveToShared(), NewCompressedData); + } +} + +CompositeBuffer CopyCompressedRange(const BufferHeader& Header, const CompositeBuffer& CompressedData, uint64_t RawOffset, uint64_t RawSize) { if (Header.TotalRawSize < RawOffset + RawSize) @@ -1338,6 +1420,19 @@ CompressedBuffer::CopyRange(uint64_t RawOffset, uint64_t RawSize) const return Range; } +CompressedBuffer +CompressedBuffer::GetRange(uint64_t RawOffset, uint64_t RawSize) const +{ + using namespace detail; + const BufferHeader Header = BufferHeader::Read(CompressedData); + const uint64_t TotalRawSize = RawSize < ~uint64_t(0) ? RawSize : Header.TotalRawSize - RawOffset; + + CompressedBuffer Range; + Range.CompressedData = GetCompressedRange(Header, CompressedData, RawOffset, TotalRawSize); + + return Range; +} + bool CompressedBuffer::TryDecompressTo(MutableMemoryView RawView, uint64_t RawOffset) const { @@ -1920,6 +2015,66 @@ TEST_CASE("CompressedBuffer") } } + SUBCASE("get range") + { + const uint64_t BlockSize = 64 * sizeof(uint64_t); + const uint64_t N = 1000; + std::vector<uint64_t> ExpectedValues = GenerateData(N); + + CompressedBuffer Compressed = CompressedBuffer::Compress(SharedBuffer::MakeView(MakeMemoryView(ExpectedValues)), + OodleCompressor::Mermaid, + OodleCompressionLevel::Optimal4, + BlockSize); + + { + const uint64_t OffsetCount = 0; + const uint64_t Count = N; + SharedBuffer Uncompressed = Compressed.GetRange(OffsetCount * sizeof(uint64_t), Count * sizeof(uint64_t)).Decompress(); + std::span<uint64_t const> Values((const uint64_t*)Uncompressed.GetData(), Uncompressed.GetSize() / sizeof(uint64_t)); + CHECK(Values.size() == Count); + ValidateData(Values, ExpectedValues, OffsetCount); + } + + { + const uint64_t OffsetCount = 64; + const uint64_t Count = N - 64; + SharedBuffer Uncompressed = Compressed.GetRange(OffsetCount * sizeof(uint64_t), Count * sizeof(uint64_t)).Decompress(); + std::span<uint64_t const> Values((const uint64_t*)Uncompressed.GetData(), Uncompressed.GetSize() / sizeof(uint64_t)); + CHECK(Values.size() == Count); + ValidateData(Values, ExpectedValues, OffsetCount); + } + + { + const uint64_t OffsetCount = 64 * 2 + 32; + const uint64_t Count = N - OffsetCount; + const uint64_t RawOffset = OffsetCount * sizeof(uint64_t); + const uint64_t RawSize = Count * sizeof(uint64_t); + uint64_t FirstBlockOffset = RawOffset % BlockSize; + + SharedBuffer Uncompressed = Compressed.GetRange(RawOffset, RawSize).Decompress(); + std::span<uint64_t const> AllValues((const uint64_t*)Uncompressed.GetData(), RawSize / sizeof(uint64_t)); + std::span<uint64_t const> Values((const uint64_t*)(((const uint8_t*)(Uncompressed.GetData()) + FirstBlockOffset)), + RawSize / sizeof(uint64_t)); + CHECK(Values.size() == Count); + ValidateData(Values, ExpectedValues, OffsetCount); + } + + { + const uint64_t OffsetCount = 64 * 2 + 63; + const uint64_t Count = N - OffsetCount - 5; + const uint64_t RawOffset = OffsetCount * sizeof(uint64_t); + const uint64_t RawSize = Count * sizeof(uint64_t); + uint64_t FirstBlockOffset = RawOffset % BlockSize; + + SharedBuffer Uncompressed = Compressed.GetRange(RawOffset, RawSize).Decompress(); + std::span<uint64_t const> AllValues((const uint64_t*)Uncompressed.GetData(), RawSize / sizeof(uint64_t)); + std::span<uint64_t const> Values((const uint64_t*)(((const uint8_t*)(Uncompressed.GetData()) + FirstBlockOffset)), + RawSize / sizeof(uint64_t)); + CHECK(Values.size() == Count); + ValidateData(Values, ExpectedValues, OffsetCount); + } + } + SUBCASE("copy uncompressed range") { const uint64_t N = 1000; @@ -1956,6 +2111,43 @@ TEST_CASE("CompressedBuffer") ValidateData(Values, ExpectedValues, OffsetCount); } } + + SUBCASE("get uncompressed range") + { + const uint64_t N = 1000; + std::vector<uint64_t> ExpectedValues = GenerateData(N); + + CompressedBuffer Compressed = CompressedBuffer::Compress(SharedBuffer::MakeView(MakeMemoryView(ExpectedValues)), + OodleCompressor::NotSet, + OodleCompressionLevel::None); + + { + const uint64_t OffsetCount = 0; + const uint64_t Count = N; + SharedBuffer Uncompressed = Compressed.GetRange(OffsetCount * sizeof(uint64_t), Count * sizeof(uint64_t)).Decompress(); + std::span<uint64_t const> Values((const uint64_t*)Uncompressed.GetData(), Uncompressed.GetSize() / sizeof(uint64_t)); + CHECK(Values.size() == Count); + ValidateData(Values, ExpectedValues, OffsetCount); + } + + { + const uint64_t OffsetCount = 1; + const uint64_t Count = N - OffsetCount; + SharedBuffer Uncompressed = Compressed.GetRange(OffsetCount * sizeof(uint64_t), Count * sizeof(uint64_t)).Decompress(); + std::span<uint64_t const> Values((const uint64_t*)Uncompressed.GetData(), Uncompressed.GetSize() / sizeof(uint64_t)); + CHECK(Values.size() == Count); + ValidateData(Values, ExpectedValues, OffsetCount); + } + + { + const uint64_t OffsetCount = 42; + const uint64_t Count = 100; + SharedBuffer Uncompressed = Compressed.GetRange(OffsetCount * sizeof(uint64_t), Count * sizeof(uint64_t)).Decompress(); + std::span<uint64_t const> Values((const uint64_t*)Uncompressed.GetData(), Uncompressed.GetSize() / sizeof(uint64_t)); + CHECK(Values.size() == Count); + ValidateData(Values, ExpectedValues, OffsetCount); + } + } } TEST_CASE("CompressedBufferReader") diff --git a/src/zencore/include/zencore/compress.h b/src/zencore/include/zencore/compress.h index c51b5407f..5e761ceef 100644 --- a/src/zencore/include/zencore/compress.h +++ b/src/zencore/include/zencore/compress.h @@ -130,9 +130,41 @@ public: /** Returns the hash of the raw data. Zero on error or if this is null. */ [[nodiscard]] ZENCORE_API IoHash DecodeRawHash() const; + /** + * Returns a block aligned range of a compressed buffer. + * + * This extracts a sub-range from the compressed buffer, if the buffer is block-compressed + * it will align start and end to end up on block boundaries. + * + * The resulting segments in the CompressedBuffer will are allocated and the data is copied + * from the source buffers. + * + * A new header will be allocated and generated. + * + * The RawHash field of the header will be zero as we do not calculate the raw hash for the sub-range + * + * @return A sub-range from the compressed buffer that encompasses RawOffset and RawSize + */ [[nodiscard]] ZENCORE_API CompressedBuffer CopyRange(uint64_t RawOffset, uint64_t RawSize = ~uint64_t(0)) const; /** + * Returns a block aligned range of a compressed buffer. + * + * This extracts a sub-range from the compressed buffer, if the buffer is block-compressed + * it will align start and end to end up on block boundaries. + * + * The resulting segments in the CompressedBuffer will reference the source buffers so it won't + * allocate memory and copy data for the compressed data blocks. + * + * A new header will be allocated and generated. + * + * The RawHash field of the header will be zero as we do not calculate the raw hash for the sub-range + * + * @return A sub-range from the compressed buffer that encompasses RawOffset and RawSize + */ + [[nodiscard]] ZENCORE_API CompressedBuffer GetRange(uint64_t RawOffset, uint64_t RawSize = ~uint64_t(0)) const; + + /** * Returns the compressor and compression level used by this buffer. * * The compressor and compression level may differ from those specified when creating the buffer diff --git a/src/zenserver/projectstore/httpprojectstore.cpp b/src/zenserver/projectstore/httpprojectstore.cpp index 0ba49cf8a..bc71e2fa0 100644 --- a/src/zenserver/projectstore/httpprojectstore.cpp +++ b/src/zenserver/projectstore/httpprojectstore.cpp @@ -768,14 +768,15 @@ HttpProjectService::HandleChunkByIdRequest(HttpRouterRequest& Req) HttpContentType AcceptType = HttpReq.AcceptContentType(); - IoBuffer Chunk; + CompositeBuffer Chunk; + HttpContentType ContentType; std::pair<HttpResponseCode, std::string> Result = - m_ProjectStore->GetChunkRange(ProjectId, OplogId, ChunkId, Offset, Size, AcceptType, Chunk); + m_ProjectStore->GetChunkRange(ProjectId, OplogId, ChunkId, Offset, Size, AcceptType, Chunk, ContentType); if (Result.first == HttpResponseCode::OK) { m_ProjectStats.ChunkHitCount++; - ZEN_DEBUG("chunk - '{}/{}/{}' '{}'", ProjectId, OplogId, ChunkId, ToString(Chunk.GetContentType())); - return HttpReq.WriteResponse(HttpResponseCode::OK, Chunk.GetContentType(), Chunk); + ZEN_DEBUG("chunk - '{}/{}/{}' '{}'", ProjectId, OplogId, ChunkId, ToString(ContentType)); + return HttpReq.WriteResponse(HttpResponseCode::OK, ContentType, Chunk); } else if (Result.first == HttpResponseCode::NotFound) { diff --git a/src/zenserver/projectstore/projectstore.cpp b/src/zenserver/projectstore/projectstore.cpp index cfa53c080..e4a39e55f 100644 --- a/src/zenserver/projectstore/projectstore.cpp +++ b/src/zenserver/projectstore/projectstore.cpp @@ -2647,7 +2647,8 @@ ProjectStore::GetChunkRange(const std::string_view ProjectId, uint64_t Offset, uint64_t Size, ZenContentType AcceptType, - IoBuffer& OutChunk) + CompositeBuffer& OutChunk, + ZenContentType& OutContentType) { if (ChunkId.size() != 2 * sizeof(Oid::OidBits)) { @@ -2656,7 +2657,7 @@ ProjectStore::GetChunkRange(const std::string_view ProjectId, const Oid Obj = Oid::FromHexString(ChunkId); - return GetChunkRange(ProjectId, OplogId, Obj, Offset, Size, AcceptType, OutChunk); + return GetChunkRange(ProjectId, OplogId, Obj, Offset, Size, AcceptType, OutChunk, OutContentType); } std::pair<HttpResponseCode, std::string> @@ -2666,7 +2667,8 @@ ProjectStore::GetChunkRange(const std::string_view ProjectId, uint64_t Offset, uint64_t Size, ZenContentType AcceptType, - IoBuffer& OutChunk) + CompositeBuffer& OutChunk, + ZenContentType& OutContentType) { bool IsOffset = Offset != 0 || Size != ~(0ull); @@ -2690,10 +2692,9 @@ ProjectStore::GetChunkRange(const std::string_view ProjectId, return {HttpResponseCode::NotFound, {}}; } - OutChunk = Chunk; - HttpContentType ContentType = Chunk.GetContentType(); + OutContentType = Chunk.GetContentType(); - if (Chunk.GetContentType() == HttpContentType::kCompressedBinary) + if (OutContentType == ZenContentType::kCompressedBinary) { IoHash RawHash; uint64_t RawSize; @@ -2702,46 +2703,47 @@ ProjectStore::GetChunkRange(const std::string_view ProjectId, if (IsOffset) { - if ((Offset + Size) > RawSize) + if (Size == ~(0ull) || (Offset + Size) > RawSize) { Size = RawSize - Offset; } - if (AcceptType == HttpContentType::kBinary) + if (AcceptType == ZenContentType::kBinary) { - OutChunk = Compressed.Decompress(Offset, Size).AsIoBuffer(); - OutChunk.SetContentType(HttpContentType::kBinary); + OutChunk = CompositeBuffer(Compressed.Decompress(Offset, Size)); + OutContentType = ZenContentType::kBinary; } else { // Value will be a range of compressed blocks that covers the requested range // The client will have to compensate for any offsets that do not land on an even block size multiple - OutChunk = Compressed.CopyRange(Offset, Size).GetCompressed().Flatten().AsIoBuffer(); - OutChunk.SetContentType(HttpContentType::kCompressedBinary); + OutChunk = Compressed.GetRange(Offset, Size).GetCompressed(); } } else { - if (AcceptType == HttpContentType::kBinary) + if (AcceptType == ZenContentType::kBinary) { - OutChunk = Compressed.Decompress().AsIoBuffer(); - OutChunk.SetContentType(HttpContentType::kBinary); + OutChunk = Compressed.DecompressToComposite(); } else { - OutChunk = Compressed.GetCompressed().Flatten().AsIoBuffer(); - OutChunk.SetContentType(HttpContentType::kCompressedBinary); + OutChunk = Compressed.GetCompressed(); + OutContentType = ZenContentType::kCompressedBinary; } } } else if (IsOffset) { - if ((Offset + Size) > Chunk.GetSize()) + if (Size == ~(0ull) || (Offset + Size) > Chunk.GetSize()) { Size = Chunk.GetSize() - Offset; } - OutChunk = IoBuffer(std::move(Chunk), Offset, Size); - OutChunk.SetContentType(ContentType); + OutChunk = CompositeBuffer(SharedBuffer(IoBuffer(std::move(Chunk), Offset, Size))); + } + else + { + OutChunk = CompositeBuffer(SharedBuffer(std::move(Chunk))); } return {HttpResponseCode::OK, {}}; @@ -4428,7 +4430,8 @@ TEST_CASE("project.store.partial.read") CHECK(RawSize == Attachments[OpIds[1]][0].second.DecodeRawSize()); } - IoBuffer ChunkResult; + CompositeBuffer ChunkResult; + HttpContentType ContentType; CHECK(ProjectStore .GetChunkRange("proj1"sv, "oplog1"sv, @@ -4436,13 +4439,14 @@ TEST_CASE("project.store.partial.read") 0, ~0ull, HttpContentType::kCompressedBinary, - ChunkResult) + ChunkResult, + ContentType) .first == HttpResponseCode::OK); CHECK(ChunkResult); CHECK(CompressedBuffer::FromCompressedNoValidate(std::move(ChunkResult)).DecodeRawSize() == Attachments[OpIds[2]][1].second.DecodeRawSize()); - IoBuffer PartialChunkResult; + CompositeBuffer PartialChunkResult; CHECK(ProjectStore .GetChunkRange("proj1"sv, "oplog1"sv, @@ -4450,13 +4454,13 @@ TEST_CASE("project.store.partial.read") 5, 1773, HttpContentType::kCompressedBinary, - PartialChunkResult) + PartialChunkResult, + ContentType) .first == HttpResponseCode::OK); CHECK(PartialChunkResult); IoHash PartialRawHash; uint64_t PartialRawSize; - CompressedBuffer PartialCompressedResult = - CompressedBuffer::FromCompressed(SharedBuffer(PartialChunkResult), PartialRawHash, PartialRawSize); + CompressedBuffer PartialCompressedResult = CompressedBuffer::FromCompressed(PartialChunkResult, PartialRawHash, PartialRawSize); CHECK(PartialRawSize >= 1773); uint64_t RawOffsetInPartialCompressed = GetCompressedOffset(PartialCompressedResult, 5); diff --git a/src/zenserver/projectstore/projectstore.h b/src/zenserver/projectstore/projectstore.h index d8c053649..eda336150 100644 --- a/src/zenserver/projectstore/projectstore.h +++ b/src/zenserver/projectstore/projectstore.h @@ -333,14 +333,16 @@ public: uint64_t Offset, uint64_t Size, ZenContentType AcceptType, - IoBuffer& OutChunk); + CompositeBuffer& OutChunk, + ZenContentType& OutContentType); std::pair<HttpResponseCode, std::string> GetChunkRange(const std::string_view ProjectId, const std::string_view OplogId, const std::string_view ChunkId, uint64_t Offset, uint64_t Size, ZenContentType AcceptType, - IoBuffer& OutChunk); + CompositeBuffer& OutChunk, + ZenContentType& OutContentType); std::pair<HttpResponseCode, std::string> GetChunk(const std::string_view ProjectId, const std::string_view OplogId, const std::string_view Cid, diff --git a/src/zenserver/vfs/vfsimpl.cpp b/src/zenserver/vfs/vfsimpl.cpp index f528b2620..5ef89ee77 100644 --- a/src/zenserver/vfs/vfsimpl.cpp +++ b/src/zenserver/vfs/vfsimpl.cpp @@ -38,21 +38,23 @@ VfsOplogDataSource::ReadNamedData(std::string_view Path, void* Buffer, uint64_t void VfsOplogDataSource::ReadChunkData(const Oid& ChunkId, void* Buffer, uint64_t ByteOffset, uint64_t ByteCount) { - IoBuffer ChunkBuffer; - auto Result = - m_ProjectStore->GetChunkRange(m_ProjectId, m_OplogId, ChunkId, 0, ~0ull, ZenContentType::kCompressedBinary, /* out */ ChunkBuffer); + CompositeBuffer ChunkBuffer; + ZenContentType ContentType; + auto Result = m_ProjectStore->GetChunkRange(m_ProjectId, + m_OplogId, + ChunkId, + 0, + ~0ull, + ZenContentType::kCompressedBinary, + /* out */ ChunkBuffer, + /* out */ ContentType); if (Result.first == HttpResponseCode::OK) { - const uint8_t* SourceBuffer = reinterpret_cast<const uint8_t*>(ChunkBuffer.GetData()); - uint64_t AvailableBufferBytes = ChunkBuffer.GetSize(); - - ZEN_ASSERT(AvailableBufferBytes >= ByteOffset); - AvailableBufferBytes -= ByteOffset; - SourceBuffer += ByteOffset; - - ZEN_ASSERT(AvailableBufferBytes >= ByteCount); - memcpy(Buffer, SourceBuffer, ByteCount); + ZEN_ASSERT(ChunkBuffer.GetSize() >= ByteOffset); + ZEN_ASSERT(ChunkBuffer.GetSize() - ByteOffset >= ByteCount); + MutableMemoryView Target(Buffer, ByteCount); + ChunkBuffer.CopyTo(Target, ByteOffset); } } |