From de6e446efa45477ddcc7051a4e8f711e5220f0fc Mon Sep 17 00:00:00 2001 From: Daniel Zheng Date: Tue, 8 Aug 2023 13:58:46 -0700 Subject: [PATCH 1/2] Adding compressor class Adding in compressor class to clean up code for cow_compress.cpp. Since we are making some api changes (to zstd) that are unique to each compression methods, these should be implementation details should be hidden to the parent class Test: m libsnapshot Change-Id: I9194e2c615aefed078458f525382253228bc1b69 --- .../include/libsnapshot/cow_compress.h | 48 ++++ .../include/libsnapshot/cow_writer.h | 10 +- .../libsnapshot_cow/cow_compress.cpp | 218 +++++++++++------- .../libsnapshot/libsnapshot_cow/test_v2.cpp | 3 +- .../libsnapshot/libsnapshot_cow/writer_v2.cpp | 18 +- .../libsnapshot/libsnapshot_cow/writer_v2.h | 3 + 6 files changed, 204 insertions(+), 96 deletions(-) create mode 100644 fs_mgr/libsnapshot/include/libsnapshot/cow_compress.h diff --git a/fs_mgr/libsnapshot/include/libsnapshot/cow_compress.h b/fs_mgr/libsnapshot/include/libsnapshot/cow_compress.h new file mode 100644 index 000000000..8add041cd --- /dev/null +++ b/fs_mgr/libsnapshot/include/libsnapshot/cow_compress.h @@ -0,0 +1,48 @@ +// +// Copyright (C) 2023 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +#pragma once + +#include +#include +#include "libsnapshot/cow_format.h" + +namespace android { +namespace snapshot { + +class ICompressor { + public: + explicit ICompressor(uint32_t compression_level) : compression_level_(compression_level) {} + + virtual ~ICompressor() {} + // Factory methods for compression methods. + static std::unique_ptr Gz(uint32_t compression_level); + static std::unique_ptr Brotli(uint32_t compression_level); + static std::unique_ptr Lz4(uint32_t compression_level); + static std::unique_ptr Zstd(uint32_t compression_level); + + static std::unique_ptr Create(CowCompression compression); + + uint32_t GetCompressionLevel() const { return compression_level_; } + + [[nodiscard]] virtual std::basic_string Compress(const void* data, + size_t length) const = 0; + + private: + uint32_t compression_level_; +}; +} // namespace snapshot +} // namespace android \ No newline at end of file diff --git a/fs_mgr/libsnapshot/include/libsnapshot/cow_writer.h b/fs_mgr/libsnapshot/include/libsnapshot/cow_writer.h index 74b8bb845..3016e933b 100644 --- a/fs_mgr/libsnapshot/include/libsnapshot/cow_writer.h +++ b/fs_mgr/libsnapshot/include/libsnapshot/cow_writer.h @@ -14,6 +14,8 @@ #pragma once +#include + #include #include @@ -107,16 +109,14 @@ class ICowWriter { class CompressWorker { public: - CompressWorker(CowCompression compression, uint32_t block_size); + CompressWorker(std::unique_ptr&& compressor, uint32_t block_size); bool RunThread(); void EnqueueCompressBlocks(const void* buffer, size_t num_blocks); bool GetCompressedBuffers(std::vector>* compressed_buf); void Finalize(); static uint32_t GetDefaultCompressionLevel(CowCompressionAlgorithm compression); - static std::basic_string Compress(CowCompression compression, const void* data, - size_t length); - static bool CompressBlocks(CowCompression compression, size_t block_size, const void* buffer, + static bool CompressBlocks(ICompressor* compressor, size_t block_size, const void* buffer, size_t num_blocks, std::vector>* compressed_data); @@ -128,7 +128,7 @@ class CompressWorker { std::vector> compressed_data; }; - CowCompression compression_; + std::unique_ptr compressor_; uint32_t block_size_; std::queue work_queue_; diff --git a/fs_mgr/libsnapshot/libsnapshot_cow/cow_compress.cpp b/fs_mgr/libsnapshot/libsnapshot_cow/cow_compress.cpp index 96d601612..d023f19e3 100644 --- a/fs_mgr/libsnapshot/libsnapshot_cow/cow_compress.cpp +++ b/fs_mgr/libsnapshot/libsnapshot_cow/cow_compress.cpp @@ -22,8 +22,11 @@ #include #include +#include +#include #include #include +#include #include #include #include @@ -51,6 +54,22 @@ std::optional CompressionAlgorithmFromString(std::strin } } +std::unique_ptr ICompressor::Create(CowCompression compression) { + switch (compression.algorithm) { + case kCowCompressLz4: + return ICompressor::Lz4(compression.compression_level); + case kCowCompressBrotli: + return ICompressor::Brotli(compression.compression_level); + case kCowCompressGz: + return ICompressor::Gz(compression.compression_level); + case kCowCompressZstd: + return ICompressor::Zstd(compression.compression_level); + case kCowCompressNone: + return nullptr; + } + return nullptr; +} + // 1. Default compression level is determined by compression algorithm // 2. There might be compatibility issues if a value is changed here, as some older versions of // Android will assume a different compression level, causing cow_size estimation differences that @@ -77,101 +96,116 @@ uint32_t CompressWorker::GetDefaultCompressionLevel(CowCompressionAlgorithm comp return 0; } -std::basic_string CompressWorker::Compress(CowCompression compression, const void* data, - size_t length) { - switch (compression.algorithm) { - case kCowCompressGz: { - const auto bound = compressBound(length); - std::basic_string buffer(bound, '\0'); +class GzCompressor final : public ICompressor { + public: + GzCompressor(uint32_t compression_level) : ICompressor(compression_level){}; - uLongf dest_len = bound; - auto rv = compress2(buffer.data(), &dest_len, reinterpret_cast(data), - length, compression.compression_level); - if (rv != Z_OK) { - LOG(ERROR) << "compress2 returned: " << rv; - return {}; - } - buffer.resize(dest_len); - return buffer; - } - case kCowCompressBrotli: { - const auto bound = BrotliEncoderMaxCompressedSize(length); - if (!bound) { - LOG(ERROR) << "BrotliEncoderMaxCompressedSize returned 0"; - return {}; - } - std::basic_string buffer(bound, '\0'); + std::basic_string Compress(const void* data, size_t length) const override { + const auto bound = compressBound(length); + std::basic_string buffer(bound, '\0'); - size_t encoded_size = bound; - auto rv = BrotliEncoderCompress( - compression.compression_level, BROTLI_DEFAULT_WINDOW, BROTLI_DEFAULT_MODE, - length, reinterpret_cast(data), &encoded_size, buffer.data()); - if (!rv) { - LOG(ERROR) << "BrotliEncoderCompress failed"; - return {}; - } - buffer.resize(encoded_size); - return buffer; + uLongf dest_len = bound; + auto rv = compress2(buffer.data(), &dest_len, reinterpret_cast(data), length, + GetCompressionLevel()); + if (rv != Z_OK) { + LOG(ERROR) << "compress2 returned: " << rv; + return {}; } - case kCowCompressLz4: { - const auto bound = LZ4_compressBound(length); - if (!bound) { - LOG(ERROR) << "LZ4_compressBound returned 0"; - return {}; - } - std::basic_string buffer(bound, '\0'); + buffer.resize(dest_len); + return buffer; + }; +}; - const auto compressed_size = LZ4_compress_default( - static_cast(data), reinterpret_cast(buffer.data()), length, - buffer.size()); - if (compressed_size <= 0) { - LOG(ERROR) << "LZ4_compress_default failed, input size: " << length - << ", compression bound: " << bound << ", ret: " << compressed_size; - return {}; - } - // Don't run compression if the compressed output is larger - if (compressed_size >= length) { - buffer.resize(length); - memcpy(buffer.data(), data, length); - } else { - buffer.resize(compressed_size); - } - return buffer; +class Lz4Compressor final : public ICompressor { + public: + Lz4Compressor(uint32_t compression_level) : ICompressor(compression_level){}; + + std::basic_string Compress(const void* data, size_t length) const override { + const auto bound = LZ4_compressBound(length); + if (!bound) { + LOG(ERROR) << "LZ4_compressBound returned 0"; + return {}; } - case kCowCompressZstd: { - std::basic_string buffer(ZSTD_compressBound(length), '\0'); - const auto compressed_size = ZSTD_compress(buffer.data(), buffer.size(), data, length, - compression.compression_level); - if (compressed_size <= 0) { - LOG(ERROR) << "ZSTD compression failed " << compressed_size; - return {}; - } - // Don't run compression if the compressed output is larger - if (compressed_size >= length) { - buffer.resize(length); - memcpy(buffer.data(), data, length); - } else { - buffer.resize(compressed_size); - } - return buffer; + std::basic_string buffer(bound, '\0'); + + const auto compressed_size = + LZ4_compress_default(static_cast(data), + reinterpret_cast(buffer.data()), length, buffer.size()); + if (compressed_size <= 0) { + LOG(ERROR) << "LZ4_compress_default failed, input size: " << length + << ", compression bound: " << bound << ", ret: " << compressed_size; + return {}; } - default: - LOG(ERROR) << "unhandled compression type: " << compression.algorithm; - break; - } - return {}; -} + // Don't run compression if the compressed output is larger + if (compressed_size >= length) { + buffer.resize(length); + memcpy(buffer.data(), data, length); + } else { + buffer.resize(compressed_size); + } + return buffer; + }; +}; + +class BrotliCompressor final : public ICompressor { + public: + BrotliCompressor(uint32_t compression_level) : ICompressor(compression_level){}; + + std::basic_string Compress(const void* data, size_t length) const override { + const auto bound = BrotliEncoderMaxCompressedSize(length); + if (!bound) { + LOG(ERROR) << "BrotliEncoderMaxCompressedSize returned 0"; + return {}; + } + std::basic_string buffer(bound, '\0'); + + size_t encoded_size = bound; + auto rv = BrotliEncoderCompress( + GetCompressionLevel(), BROTLI_DEFAULT_WINDOW, BROTLI_DEFAULT_MODE, length, + reinterpret_cast(data), &encoded_size, buffer.data()); + if (!rv) { + LOG(ERROR) << "BrotliEncoderCompress failed"; + return {}; + } + buffer.resize(encoded_size); + return buffer; + }; +}; + +class ZstdCompressor final : public ICompressor { + public: + ZstdCompressor(uint32_t compression_level) : ICompressor(compression_level){}; + + std::basic_string Compress(const void* data, size_t length) const override { + std::basic_string buffer(ZSTD_compressBound(length), '\0'); + const auto compressed_size = + ZSTD_compress(buffer.data(), buffer.size(), data, length, GetCompressionLevel()); + if (compressed_size <= 0) { + LOG(ERROR) << "ZSTD compression failed " << compressed_size; + return {}; + } + // Don't run compression if the compressed output is larger + if (compressed_size >= length) { + buffer.resize(length); + memcpy(buffer.data(), data, length); + } else { + buffer.resize(compressed_size); + } + return buffer; + }; +}; + bool CompressWorker::CompressBlocks(const void* buffer, size_t num_blocks, std::vector>* compressed_data) { - return CompressBlocks(compression_, block_size_, buffer, num_blocks, compressed_data); + return CompressBlocks(compressor_.get(), block_size_, buffer, num_blocks, compressed_data); } -bool CompressWorker::CompressBlocks(CowCompression compression, size_t block_size, - const void* buffer, size_t num_blocks, +bool CompressWorker::CompressBlocks(ICompressor* compressor, size_t block_size, const void* buffer, + size_t num_blocks, std::vector>* compressed_data) { const uint8_t* iter = reinterpret_cast(buffer); while (num_blocks) { - auto data = Compress(compression, iter, block_size); + auto data = compressor->Compress(iter, block_size); if (data.empty()) { PLOG(ERROR) << "CompressBlocks: Compression failed"; return false; @@ -270,6 +304,22 @@ bool CompressWorker::GetCompressedBuffers(std::vector return true; } +std::unique_ptr ICompressor::Brotli(uint32_t compression_level) { + return std::make_unique(compression_level); +} + +std::unique_ptr ICompressor::Gz(uint32_t compression_level) { + return std::make_unique(compression_level); +} + +std::unique_ptr ICompressor::Lz4(uint32_t compression_level) { + return std::make_unique(compression_level); +} + +std::unique_ptr ICompressor::Zstd(uint32_t compression_level) { + return std::make_unique(compression_level); +} + void CompressWorker::Finalize() { { std::unique_lock lock(lock_); @@ -278,8 +328,8 @@ void CompressWorker::Finalize() { cv_.notify_all(); } -CompressWorker::CompressWorker(CowCompression compression, uint32_t block_size) - : compression_(compression), block_size_(block_size) {} +CompressWorker::CompressWorker(std::unique_ptr&& compressor, uint32_t block_size) + : compressor_(std::move(compressor)), block_size_(block_size) {} } // namespace snapshot } // namespace android diff --git a/fs_mgr/libsnapshot/libsnapshot_cow/test_v2.cpp b/fs_mgr/libsnapshot/libsnapshot_cow/test_v2.cpp index 2258d9fb7..0ecad9d2c 100644 --- a/fs_mgr/libsnapshot/libsnapshot_cow/test_v2.cpp +++ b/fs_mgr/libsnapshot/libsnapshot_cow/test_v2.cpp @@ -480,7 +480,8 @@ TEST_P(CompressionTest, HorribleStream) { std::string expected = "The quick brown fox jumps over the lazy dog."; expected.resize(4096, '\0'); - auto result = CompressWorker::Compress(compression, expected.data(), expected.size()); + std::unique_ptr compressor = ICompressor::Create(compression); + auto result = compressor->Compress(expected.data(), expected.size()); ASSERT_FALSE(result.empty()); HorribleStream stream(result); diff --git a/fs_mgr/libsnapshot/libsnapshot_cow/writer_v2.cpp b/fs_mgr/libsnapshot/libsnapshot_cow/writer_v2.cpp index 6d04c6a0c..7115821b6 100644 --- a/fs_mgr/libsnapshot/libsnapshot_cow/writer_v2.cpp +++ b/fs_mgr/libsnapshot/libsnapshot_cow/writer_v2.cpp @@ -184,7 +184,8 @@ void CowWriterV2::InitWorkers() { return; } for (int i = 0; i < num_compress_threads_; i++) { - auto wt = std::make_unique(compression_, header_.block_size); + std::unique_ptr compressor = ICompressor::Create(compression_); + auto wt = std::make_unique(std::move(compressor), header_.block_size); threads_.emplace_back(std::async(std::launch::async, &CompressWorker::RunThread, wt.get())); compress_threads_.push_back(std::move(wt)); } @@ -339,10 +340,12 @@ bool CowWriterV2::CompressBlocks(size_t num_blocks, const void* data) { const uint8_t* iter = reinterpret_cast(data); compressed_buf_.clear(); if (num_threads <= 1) { - return CompressWorker::CompressBlocks(compression_, options_.block_size, data, num_blocks, - &compressed_buf_); + if (!compressor_) { + compressor_ = ICompressor::Create(compression_); + } + return CompressWorker::CompressBlocks(compressor_.get(), options_.block_size, data, + num_blocks, &compressed_buf_); } - // Submit the blocks per thread. The retrieval of // compressed buffers has to be done in the same order. // We should not poll for completed buffers in a different order as the @@ -412,8 +415,11 @@ bool CowWriterV2::EmitBlocks(uint64_t new_block_start, const void* data, size_t buf_iter_++; return data; } else { - auto data = - CompressWorker::Compress(compression_, iter, header_.block_size); + if (!compressor_) { + compressor_ = ICompressor::Create(compression_); + } + + auto data = compressor_->Compress(iter, header_.block_size); return data; } }(); diff --git a/fs_mgr/libsnapshot/libsnapshot_cow/writer_v2.h b/fs_mgr/libsnapshot/libsnapshot_cow/writer_v2.h index 3f357e024..131a06845 100644 --- a/fs_mgr/libsnapshot/libsnapshot_cow/writer_v2.h +++ b/fs_mgr/libsnapshot/libsnapshot_cow/writer_v2.h @@ -65,6 +65,9 @@ class CowWriterV2 : public CowWriterBase { private: CowFooter footer_{}; CowCompression compression_; + // in the case that we are using one thread for compression, we can store and re-use the same + // compressor + std::unique_ptr compressor_; uint64_t current_op_pos_ = 0; uint64_t next_op_pos_ = 0; uint64_t next_data_pos_ = 0; From 03acfdbeb6dcf6ffb32b1823a91d8b29bbce03b5 Mon Sep 17 00:00:00 2001 From: Daniel Zheng Date: Wed, 9 Aug 2023 13:48:08 -0700 Subject: [PATCH 2/2] Optimize zstd compression reuse the same context for zstd compression. One context should be used per compression thread for optimal performance. Discussion and reccomendations can be found at go/android-hw-compression-vendor Results (level 3 compression) full ota on raven with optimizations: 1191.304 seconds without optimizations: 1461.854 seconds compression ratio remains unchanged and merge time difference are negligible ~1% delta (probably just noise) Test: ota_from_target_files, update_device.py Change-Id: I3feede9f1f119874e369c54b29c594475fbf7376 --- .../libsnapshot/libsnapshot_cow/cow_compress.cpp | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/fs_mgr/libsnapshot/libsnapshot_cow/cow_compress.cpp b/fs_mgr/libsnapshot/libsnapshot_cow/cow_compress.cpp index d023f19e3..466b93c25 100644 --- a/fs_mgr/libsnapshot/libsnapshot_cow/cow_compress.cpp +++ b/fs_mgr/libsnapshot/libsnapshot_cow/cow_compress.cpp @@ -18,6 +18,7 @@ #include #include +#include #include #include @@ -174,12 +175,18 @@ class BrotliCompressor final : public ICompressor { class ZstdCompressor final : public ICompressor { public: - ZstdCompressor(uint32_t compression_level) : ICompressor(compression_level){}; + ZstdCompressor(uint32_t compression_level) + : ICompressor(compression_level), zstd_context_(ZSTD_createCCtx(), ZSTD_freeCCtx) { + ZSTD_CCtx_setParameter(zstd_context_.get(), ZSTD_c_compressionLevel, compression_level); + // FIXME: hardcoding a value of 12 here for 4k blocks, should change to be either set by + // user, or optimized depending on block size + ZSTD_CCtx_setParameter(zstd_context_.get(), ZSTD_c_windowLog, 12); + }; std::basic_string Compress(const void* data, size_t length) const override { std::basic_string buffer(ZSTD_compressBound(length), '\0'); const auto compressed_size = - ZSTD_compress(buffer.data(), buffer.size(), data, length, GetCompressionLevel()); + ZSTD_compress2(zstd_context_.get(), buffer.data(), buffer.size(), data, length); if (compressed_size <= 0) { LOG(ERROR) << "ZSTD compression failed " << compressed_size; return {}; @@ -193,6 +200,9 @@ class ZstdCompressor final : public ICompressor { } return buffer; }; + + private: + std::unique_ptr zstd_context_; }; bool CompressWorker::CompressBlocks(const void* buffer, size_t num_blocks,