adb: implement LZ4 compression.
Add support for LZ4 compression, which compresses and decompresses far
more quickly than brotli, at the cost of worse compression ratio.
`adb sync -d system` speeds (in MB/s) on aosp_blueline-eng:
none brotli lz4
USB 3.0 120 110 190
USB 2.0 38 75 63
Bug: https://issuetracker.google.com/150827486
Test: python3 -m unittest test_device.FileOperationsTest{Uncompressed,Brotli,LZ4}
Change-Id: Ibef6ac15a76b4e5dcd02d7fb9433cbb1c02b8382
diff --git a/adb/compression_utils.h b/adb/compression_utils.h
index f349697..a0c48a2 100644
--- a/adb/compression_utils.h
+++ b/adb/compression_utils.h
@@ -24,6 +24,7 @@
#include <brotli/decode.h>
#include <brotli/encode.h>
+#include <lz4frame.h>
#include "types.h"
@@ -229,3 +230,154 @@
size_t output_bytes_left_;
std::unique_ptr<BrotliEncoderState, void (*)(BrotliEncoderState*)> encoder_;
};
+
+struct LZ4Decoder final : public Decoder {
+ explicit LZ4Decoder(std::span<char> output_buffer)
+ : Decoder(output_buffer), decoder_(nullptr, nullptr) {
+ LZ4F_dctx* dctx;
+ if (LZ4F_createDecompressionContext(&dctx, LZ4F_VERSION) != 0) {
+ LOG(FATAL) << "failed to initialize LZ4 decompression context";
+ }
+ decoder_ = std::unique_ptr<LZ4F_dctx, decltype(&LZ4F_freeDecompressionContext)>(
+ dctx, LZ4F_freeDecompressionContext);
+ }
+
+ DecodeResult Decode(std::span<char>* output) final {
+ size_t available_in = input_buffer_.front_size();
+ const char* next_in = input_buffer_.front_data();
+
+ size_t available_out = output_buffer_.size();
+ char* next_out = output_buffer_.data();
+
+ size_t rc = LZ4F_decompress(decoder_.get(), next_out, &available_out, next_in,
+ &available_in, nullptr);
+ if (LZ4F_isError(rc)) {
+ LOG(ERROR) << "LZ4F_decompress failed: " << LZ4F_getErrorName(rc);
+ return DecodeResult::Error;
+ }
+
+ input_buffer_.drop_front(available_in);
+
+ if (rc == 0) {
+ if (!input_buffer_.empty()) {
+ LOG(ERROR) << "LZ4 stream hit end before reading all data";
+ return DecodeResult::Error;
+ }
+ lz4_done_ = true;
+ }
+
+ *output = std::span<char>(output_buffer_.data(), available_out);
+
+ if (finished_) {
+ return input_buffer_.empty() && lz4_done_ ? DecodeResult::Done
+ : DecodeResult::MoreOutput;
+ }
+
+ return DecodeResult::NeedInput;
+ }
+
+ private:
+ bool lz4_done_ = false;
+ std::unique_ptr<LZ4F_dctx, LZ4F_errorCode_t (*)(LZ4F_dctx*)> decoder_;
+};
+
+struct LZ4Encoder final : public Encoder {
+ explicit LZ4Encoder(size_t output_block_size)
+ : Encoder(output_block_size), encoder_(nullptr, nullptr) {
+ LZ4F_cctx* cctx;
+ if (LZ4F_createCompressionContext(&cctx, LZ4F_VERSION) != 0) {
+ LOG(FATAL) << "failed to initialize LZ4 compression context";
+ }
+ encoder_ = std::unique_ptr<LZ4F_cctx, decltype(&LZ4F_freeCompressionContext)>(
+ cctx, LZ4F_freeCompressionContext);
+ Block header(LZ4F_HEADER_SIZE_MAX);
+ size_t rc = LZ4F_compressBegin(encoder_.get(), header.data(), header.size(), nullptr);
+ if (LZ4F_isError(rc)) {
+ LOG(FATAL) << "LZ4F_compressBegin failed: %s", LZ4F_getErrorName(rc);
+ }
+ header.resize(rc);
+ output_buffer_.append(std::move(header));
+ }
+
+ // As an optimization, only emit a block if we have an entire output block ready, or we're done.
+ bool OutputReady() const {
+ return output_buffer_.size() >= output_block_size_ || lz4_finalized_;
+ }
+
+ // TODO: Switch the output type to IOVector to remove a copy?
+ EncodeResult Encode(Block* output) final {
+ size_t available_in = input_buffer_.front_size();
+ const char* next_in = input_buffer_.front_data();
+
+ // LZ4 makes no guarantees about being able to recover from trying to compress with an
+ // insufficiently large output buffer. LZ4F_compressBound tells us how much buffer we
+ // need to compress a given number of bytes, but the smallest value seems to be bigger
+ // than SYNC_DATA_MAX, so we need to buffer ourselves.
+
+ // Input size chosen to be a local maximum for LZ4F_compressBound (i.e. the block size).
+ constexpr size_t max_input_size = 65536;
+ const size_t encode_block_size = LZ4F_compressBound(max_input_size, nullptr);
+
+ if (available_in != 0) {
+ if (lz4_finalized_) {
+ LOG(ERROR) << "LZ4Encoder received data after Finish?";
+ return EncodeResult::Error;
+ }
+
+ available_in = std::min(available_in, max_input_size);
+
+ Block encode_block(encode_block_size);
+ size_t available_out = encode_block.capacity();
+ char* next_out = encode_block.data();
+
+ size_t rc = LZ4F_compressUpdate(encoder_.get(), next_out, available_out, next_in,
+ available_in, nullptr);
+ if (LZ4F_isError(rc)) {
+ LOG(ERROR) << "LZ4F_compressUpdate failed: " << LZ4F_getErrorName(rc);
+ return EncodeResult::Error;
+ }
+
+ input_buffer_.drop_front(available_in);
+
+ available_out -= rc;
+ next_out += rc;
+
+ encode_block.resize(encode_block_size - available_out);
+ output_buffer_.append(std::move(encode_block));
+ }
+
+ if (finished_ && !lz4_finalized_) {
+ lz4_finalized_ = true;
+
+ Block final_block(encode_block_size + 4);
+ size_t rc = LZ4F_compressEnd(encoder_.get(), final_block.data(), final_block.size(),
+ nullptr);
+ if (LZ4F_isError(rc)) {
+ LOG(ERROR) << "LZ4F_compressEnd failed: " << LZ4F_getErrorName(rc);
+ return EncodeResult::Error;
+ }
+
+ final_block.resize(rc);
+ output_buffer_.append(std::move(final_block));
+ }
+
+ if (OutputReady()) {
+ size_t len = std::min(output_block_size_, output_buffer_.size());
+ *output = output_buffer_.take_front(len).coalesce();
+ } else {
+ output->clear();
+ }
+
+ if (lz4_finalized_ && output_buffer_.empty()) {
+ return EncodeResult::Done;
+ } else if (OutputReady()) {
+ return EncodeResult::MoreOutput;
+ }
+ return EncodeResult::NeedInput;
+ }
+
+ private:
+ bool lz4_finalized_ = false;
+ std::unique_ptr<LZ4F_cctx, LZ4F_errorCode_t (*)(LZ4F_cctx*)> encoder_;
+ IOVector output_buffer_;
+};