This is an automated email from the ASF dual-hosted git repository.
yiguolei pushed a commit to branch branch-4.2
in repository https://gitbox.apache.org/repos/asf/doris.git
The following commit(s) were added to refs/heads/branch-4.2 by this push:
new e6e42a76954 branch-4.2: [improvement](be) Decode SNII zstd and raw
.prx windows faster #69770 (#69788)
e6e42a76954 is described below
commit e6e42a76954f5c28bd51915e33b703deb63e269d
Author: github-actions[bot]
<41898282+github-actions[bot]@users.noreply.github.com>
AuthorDate: Thu Oct 29 16:30:42 2026 +0800
branch-4.2: [improvement](be) Decode SNII zstd and raw .prx windows faster
#69770 (#69788)
Cherry-picked from #69770
Co-authored-by: Jack <[email protected]>
---
.../index/inverted/snii/encoding/byte_source.h | 6 ++
.../index/inverted/snii/encoding/zstd_codec.cpp | 38 ++++++++++
.../index/inverted/snii/encoding/zstd_codec.h | 21 ++++++
.../index/inverted/snii/format/prx_decode_stats.h | 7 ++
.../storage/index/inverted/snii/format/prx_pod.cpp | 80 +++++++++++++++++-----
.../inverted/snii/reader/snii_index_source.cpp | 2 +-
.../index/inverted/snii/reader/snii_index_source.h | 4 ++
.../inverted/snii/reader/snii_postings_cursor.cpp | 8 ++-
.../inverted/snii/reader/snii_postings_cursor.h | 5 +-
.../inverted/snii/encoding/zstd_codec_test.cpp | 73 ++++++++++++++++++++
.../index/inverted/snii/format/prx_pod_test.cpp | 76 ++++++++++++++++++++
11 files changed, 298 insertions(+), 22 deletions(-)
diff --git a/be/src/storage/index/inverted/snii/encoding/byte_source.h
b/be/src/storage/index/inverted/snii/encoding/byte_source.h
index c6d4c9f83a2..86d0568f10d 100644
--- a/be/src/storage/index/inverted/snii/encoding/byte_source.h
+++ b/be/src/storage/index/inverted/snii/encoding/byte_source.h
@@ -80,6 +80,12 @@ public:
Status get_zigzag(int64_t* v);
Status get_bytes(size_t n, Slice* out);
+ // Raw cursor access for a hot loop that decodes many small runs without a
Status per value and
+ // hands anything unusual back to the checked decoders above (set_cursor,
then the call).
+ const uint8_t* cursor() const { return s_.data() + pos_; }
+ const uint8_t* end() const { return s_.data() + s_.size(); }
+ void set_cursor(const uint8_t* p) { pos_ = static_cast<size_t>(p -
s_.data()); }
+
size_t remaining() const { return s_.size() - pos_; }
size_t position() const { return pos_; }
bool eof() const { return pos_ == s_.size(); }
diff --git a/be/src/storage/index/inverted/snii/encoding/zstd_codec.cpp
b/be/src/storage/index/inverted/snii/encoding/zstd_codec.cpp
index 4139f49f957..ffc3461103c 100644
--- a/be/src/storage/index/inverted/snii/encoding/zstd_codec.cpp
+++ b/be/src/storage/index/inverted/snii/encoding/zstd_codec.cpp
@@ -49,4 +49,42 @@ Status zstd_decompress(Slice input, size_t
expected_uncomp_len, std::vector<uint
return Status::OK();
}
+ZstdDecompressor::~ZstdDecompressor() {
+ ZSTD_freeDCtx(static_cast<ZSTD_DCtx*>(_context));
+}
+
+Status ZstdDecompressor::decompress(Slice input, size_t expected_uncomp_len,
Slice* out) {
+ if (_context == nullptr) {
+ _context = ZSTD_createDCtx();
+ if (_context == nullptr) {
+ return Status::Error<ErrorCode::MEM_ALLOC_FAILED, false>(
+ "zstd decompress: cannot create a context");
+ }
+ }
+ if (expected_uncomp_len > _capacity) {
+ // Every byte is written by the decompression before it is read, so
the buffer is not
+ // zero-filled. The old buffer goes first, so the two are never held
together.
+ _buffer.reset();
+ _capacity = 0;
+ _buffer = make_unique_buffer<uint8_t>(expected_uncomp_len);
+ if (_buffer == nullptr) {
+ return Status::Error<ErrorCode::MEM_ALLOC_FAILED, false>(
+ "zstd decompress: cannot allocate {} bytes",
expected_uncomp_len);
+ }
+ _capacity = expected_uncomp_len;
+ }
+ const size_t n = ZSTD_decompressDCtx(static_cast<ZSTD_DCtx*>(_context),
_buffer.get(),
+ expected_uncomp_len, input.data(),
input.size());
+ if (ZSTD_isError(n)) {
+ return Status::Error<ErrorCode::INVERTED_INDEX_FILE_CORRUPTED, false>(
+ std::string("zstd decompress: ") + ZSTD_getErrorName(n));
+ }
+ if (n != expected_uncomp_len) {
+ return Status::Error<ErrorCode::INVERTED_INDEX_FILE_CORRUPTED, false>(
+ "zstd decompressed length mismatch");
+ }
+ *out = Slice(_buffer.get(), expected_uncomp_len);
+ return Status::OK();
+}
+
} // namespace doris::snii
diff --git a/be/src/storage/index/inverted/snii/encoding/zstd_codec.h
b/be/src/storage/index/inverted/snii/encoding/zstd_codec.h
index c5ebf4206f8..c62056dbd4d 100644
--- a/be/src/storage/index/inverted/snii/encoding/zstd_codec.h
+++ b/be/src/storage/index/inverted/snii/encoding/zstd_codec.h
@@ -22,6 +22,7 @@
#include <vector>
#include "common/status.h"
+#include "core/custom_allocator.h"
#include "storage/index/inverted/snii/common/slice.h"
namespace doris::snii {
@@ -30,4 +31,24 @@ namespace doris::snii {
Status zstd_compress(Slice input, int level, std::vector<uint8_t>* out);
Status zstd_decompress(Slice input, size_t expected_uncomp_len,
std::vector<uint8_t>* out);
+// Decompresses many small frames in a row -- one per .prx window of a posting
-- with one
+// decompression context and one output buffer, instead of a context created
and a zeroed vector
+// allocated for every frame. The view returned by decompress() is valid until
the next call.
+// Not thread-safe; owned by a single reader. The buffer lasts as long as the
decompressor and is as
+// large as the largest frame decoded, so it is charged to the memory tracker
of the decoding task.
+class ZstdDecompressor {
+public:
+ ZstdDecompressor() = default;
+ ~ZstdDecompressor();
+ ZstdDecompressor(const ZstdDecompressor&) = delete;
+ ZstdDecompressor& operator=(const ZstdDecompressor&) = delete;
+
+ Status decompress(Slice input, size_t expected_uncomp_len, Slice* out);
+
+private:
+ void* _context = nullptr; // ZSTD_DCtx, created on the first frame
+ DorisUniqueBufferPtr<uint8_t> _buffer;
+ size_t _capacity = 0;
+};
+
} // namespace doris::snii
diff --git a/be/src/storage/index/inverted/snii/format/prx_decode_stats.h
b/be/src/storage/index/inverted/snii/format/prx_decode_stats.h
index eda0af5cc0c..18fe996f8de 100644
--- a/be/src/storage/index/inverted/snii/format/prx_decode_stats.h
+++ b/be/src/storage/index/inverted/snii/format/prx_decode_stats.h
@@ -23,6 +23,10 @@
#include "common/status.h"
+namespace doris::snii {
+class ZstdDecompressor;
+} // namespace doris::snii
+
namespace doris::snii::format {
// Optional allocation seam for full CSR decode callers that must gate every
@@ -73,6 +77,9 @@ struct PrxDecodeContext {
PrxDecodeStats* stats = nullptr;
PrxDecodedShape* shape = nullptr;
PrxCsrAllocationGate* allocation_gate = nullptr;
+ // Optional: a reader that decodes many windows in a row passes its own
decompressor, so the
+ // zstd windows reuse one context and one buffer. An allocation gate takes
precedence.
+ ZstdDecompressor* zstd = nullptr;
};
} // namespace doris::snii::format
diff --git a/be/src/storage/index/inverted/snii/format/prx_pod.cpp
b/be/src/storage/index/inverted/snii/format/prx_pod.cpp
index 5c541276dd8..3455b3921b5 100644
--- a/be/src/storage/index/inverted/snii/format/prx_pod.cpp
+++ b/be/src/storage/index/inverted/snii/format/prx_pod.cpp
@@ -964,9 +964,22 @@ Status decode_payload_csr_selective(Slice plain,
std::span<const uint32_t> doc_o
uint64_t total_pos = 0;
uint32_t decoded_max_frequency = 0;
bool decoded_zero_frequency = false;
+ // The loop below runs once per document of the window, selected or not,
so it keeps the cursor
+ // in a register and builds no Status on the usual path: a single-byte
count, a skipped run of
+ // short varints, a selected run of single-byte deltas. Anything else -- a
multi-byte value, a
+ // run that does not fit, a prefix sum that would overflow -- is handed to
the checked decoders
+ // with the cursor where it was, so they decide and report exactly as they
always did.
+ const uint8_t* const end = src.end();
+ const uint8_t* p = src.cursor();
for (uint32_t d = 0; d < doc_count; ++d) {
uint32_t pos_count = 0;
- RETURN_IF_ERROR(src.get_varint32_fast(&pos_count));
+ if (p < end && *p < 0x80) {
+ pos_count = *p++;
+ } else {
+ src.set_cursor(p);
+ RETURN_IF_ERROR(src.get_varint32(&pos_count));
+ p = src.cursor();
+ }
decoded_max_frequency = std::max(decoded_max_frequency, pos_count);
decoded_zero_frequency |= pos_count == 0;
total_pos += pos_count;
@@ -976,24 +989,54 @@ Status decode_payload_csr_selective(Slice plain,
std::span<const uint32_t> doc_o
}
const bool selected = next_doc < doc_ordinals.size() &&
doc_ordinals[next_doc] == d;
if (!selected) {
- // Skip this doc's position deltas without decoding them -- the CSR
- // layout is sequential so we must advance past them, but only the
- // candidate (selected) docs' positions are ever used. With a
sparse
- // candidate set (the common phrase / phrase-prefix case after
docid
- // narrowing) most docs in a window are skipped, so this avoids the
- // dominant varint-decode cost.
- RETURN_IF_ERROR(src.skip_varints(pos_count));
+ // The CSR layout is sequential, so a skipped doc's deltas are
walked past without being
+ // decoded. With a sparse candidate set most docs of a window are
skipped.
+ const uint8_t* q = p;
+ uint32_t skipped = 0;
+ for (; skipped < pos_count && q < end; ++skipped) {
+ while (q < end && (*q & 0x80) != 0) {
+ ++q;
+ }
+ if (q >= end) {
+ break;
+ }
+ ++q;
+ }
+ if (skipped != pos_count) {
+ return Status::Error<ErrorCode::INVERTED_INDEX_FILE_CORRUPTED,
false>(
+ "byte_source: varint skip past end");
+ }
+ p = q;
continue;
}
- // Selected doc: decode its `pos_count` ascending position deltas with
a
- // tight inline prefix-sum decoder (single-byte fast path, no per-value
- // get_varint32/Status call chain). This is the CPU hotspot for
narrowed
- // phrase/phrase-prefix candidate sets, where each selected doc's
varint
- // run dominates after non-selected docs are skipped.
- RETURN_IF_ERROR(src.decode_delta_run(pos_count, pos_flat));
+ // Selected doc: its `pos_count` ascending deltas become positions. A
single-byte delta is
+ // below 0x80, so the running sum cannot overflow unless it is checked.
+ const size_t flat_before = pos_flat->size();
+ const uint8_t* q = p;
+ uint32_t position = 0;
+ uint32_t decoded = 0;
+ if (static_cast<size_t>(end - q) >= pos_count) {
+ for (; decoded < pos_count; ++decoded) {
+ const uint8_t byte = *q;
+ if (byte >= 0x80 || __builtin_add_overflow(position, uint32_t
{byte}, &position)) {
+ break;
+ }
+ pos_flat->push_back(position);
+ ++q;
+ }
+ }
+ if (decoded == pos_count) {
+ p = q;
+ } else {
+ pos_flat->resize(flat_before);
+ src.set_cursor(p);
+ RETURN_IF_ERROR(src.decode_delta_run(pos_count, pos_flat));
+ p = src.cursor();
+ }
pos_off->push_back(static_cast<uint32_t>(pos_flat->size()));
++next_doc;
}
+ src.set_cursor(p);
if (!src.eof())
return Status::Error<ErrorCode::INVERTED_INDEX_FILE_CORRUPTED, false>(
"prx: trailing bytes after payload");
@@ -1055,7 +1098,7 @@ Status decode_csr_frame(const PrxFrameView& encoded,
std::span<const uint32_t> d
bool decode_all_docs, bool all_docs_selected,
std::vector<uint32_t>* pos_flat,
std::vector<uint32_t>* pos_off,
PrxDecodeStats* stats, PrxDecodedShape* shape,
- PrxCsrAllocationGate* allocation_gate) {
+ PrxCsrAllocationGate* allocation_gate,
ZstdDecompressor* zstd) {
if (!decode_all_docs && allocation_gate != nullptr) {
return Status::Error<ErrorCode::INVALID_ARGUMENT, false>(
"prx: selective decode cannot use an allocation gate");
@@ -1070,7 +1113,9 @@ Status decode_csr_frame(const PrxFrameView& encoded,
std::span<const uint32_t> d
std::vector<uint8_t> local_decompressed;
Slice plain = encoded.payload;
- if (encoded.codec == PrxCodec::kZstd) {
+ if (encoded.codec == PrxCodec::kZstd && allocation_gate == nullptr && zstd
!= nullptr) {
+ RETURN_IF_ERROR(zstd->decompress(encoded.payload,
encoded.uncompressed_length, &plain));
+ } else if (encoded.codec == PrxCodec::kZstd) {
std::vector<uint8_t>* decompressed = &local_decompressed;
if (allocation_gate != nullptr) {
RETURN_IF_ERROR(allocation_gate->reserve_decompression(encoded.uncompressed_length,
@@ -1145,7 +1190,8 @@ Status read_prx_window_csr_impl(ByteSource* source,
std::span<const uint32_t> do
RETURN_IF_ERROR(decode_csr_frame(encoded, doc_ordinals, decode_all_docs,
all_docs_selected,
pos_flat, pos_off, stats,
context == nullptr ? nullptr :
context->shape,
- context == nullptr ? nullptr :
context->allocation_gate));
+ context == nullptr ? nullptr :
context->allocation_gate,
+ context == nullptr ? nullptr :
context->zstd));
if (collect_stats) {
// Exclude the logical-selection scan from decode timing.
frame_stats.decode_ns = elapsed_ns(decode_start);
diff --git a/be/src/storage/index/inverted/snii/reader/snii_index_source.cpp
b/be/src/storage/index/inverted/snii/reader/snii_index_source.cpp
index c3131c987e2..51df3f0e2c6 100644
--- a/be/src/storage/index/inverted/snii/reader/snii_index_source.cpp
+++ b/be/src/storage/index/inverted/snii/reader/snii_index_source.cpp
@@ -124,7 +124,7 @@ Status SniiIndexSource::_cursor(Term& term, bool positions,
bool scoring, SniiRe
}
*out = std::make_unique<SniiPostingsCursor>(_idx, term.hit.entry,
term.hit.frq_base,
term.hit.prx_base, positions,
scoring, norms, wave,
- _prx_stats);
+ _prx_stats, &_zstd);
if (auto prelude = term.prelude.lock()) {
(*out)->set_prelude(std::move(prelude));
}
diff --git a/be/src/storage/index/inverted/snii/reader/snii_index_source.h
b/be/src/storage/index/inverted/snii/reader/snii_index_source.h
index ef46af713f1..b8bd6306569 100644
--- a/be/src/storage/index/inverted/snii/reader/snii_index_source.h
+++ b/be/src/storage/index/inverted/snii/reader/snii_index_source.h
@@ -28,6 +28,7 @@
#include "common/status.h"
#include "storage/index/inverted/query/spi/index_source.h"
+#include "storage/index/inverted/snii/encoding/zstd_codec.h"
#include "storage/index/inverted/snii/format/frq_prelude.h"
#include "storage/index/inverted/snii/format/norms_pod.h"
#include "storage/index/inverted/snii/reader/dict_block_cache.h"
@@ -89,6 +90,9 @@ private:
const LogicalIndexReader& _idx;
format::PrxDecodeStats* _prx_stats;
+ // The cursors decode on the source's thread one window at a time, so they
share one zstd
+ // context and buffer instead of creating them for every window.
+ ZstdDecompressor _zstd;
SniiReadWave _wave;
std::unordered_map<std::string, Term> _terms;
DictBlockCache _scoring_dict_cache;
diff --git a/be/src/storage/index/inverted/snii/reader/snii_postings_cursor.cpp
b/be/src/storage/index/inverted/snii/reader/snii_postings_cursor.cpp
index 4fd33f6a4f2..9ac4a57eff9 100644
--- a/be/src/storage/index/inverted/snii/reader/snii_postings_cursor.cpp
+++ b/be/src/storage/index/inverted/snii/reader/snii_postings_cursor.cpp
@@ -98,7 +98,8 @@ uint64_t SniiReadWave::held_bytes() const {
SniiPostingsCursor::SniiPostingsCursor(const LogicalIndexReader& idx,
format::DictEntry entry,
uint64_t frq_base, uint64_t prx_base,
bool positions,
bool scoring, const
format::NormsPodReader* norms,
- SniiReadWave* wave,
format::PrxDecodeStats* prx_stats)
+ SniiReadWave* wave,
format::PrxDecodeStats* prx_stats,
+ ZstdDecompressor* zstd)
: _idx(idx),
_entry(std::move(entry)),
_frq_base(frq_base),
@@ -108,6 +109,7 @@ SniiPostingsCursor::SniiPostingsCursor(const
LogicalIndexReader& idx, format::Di
_norms(norms),
_wave(wave),
_prx_stats(prx_stats),
+ _zstd(zstd),
_kind(_posting_kind(_entry)) {
if (_wave == nullptr) {
_own_wave = std::make_unique<SniiReadWave>(_idx.reader());
@@ -533,7 +535,7 @@ Status
SniiPostingsCursor::_ensure_positions(std::span<const uint32_t> ordinals)
const uint32_t window = _current_window;
RETURN_IF_ERROR(_ensure_prx(window));
ByteSource source(_windows[window].prx);
- format::PrxDecodeContext context {.stats = _prx_stats};
+ format::PrxDecodeContext context {.stats = _prx_stats, .zstd = _zstd};
if (ordinals.empty() || ordinals.size() == _doc_count) {
RETURN_IF_ERROR(format::read_prx_window_csr(&source, &_pos_flat,
&_pos_off, &context));
} else {
@@ -795,7 +797,7 @@ Status SniiPostingsCursor::_decode_selected(std::span<const
uint32_t> ordinals,
RETURN_IF_ERROR(_ensure_prx(_current_window));
ByteSource source(_windows[_current_window].prx);
format::PrxDecodedShape shape;
- format::PrxDecodeContext context {.stats = _prx_stats, .shape = &shape};
+ format::PrxDecodeContext context {.stats = _prx_stats, .shape = &shape,
.zstd = _zstd};
RETURN_IF_ERROR(format::read_prx_window_csr_selective(&source, ordinals,
&_selected_flat,
&_selected_off,
&context));
if (!source.eof()) {
diff --git a/be/src/storage/index/inverted/snii/reader/snii_postings_cursor.h
b/be/src/storage/index/inverted/snii/reader/snii_postings_cursor.h
index 64c6d653e92..e10a51211e4 100644
--- a/be/src/storage/index/inverted/snii/reader/snii_postings_cursor.h
+++ b/be/src/storage/index/inverted/snii/reader/snii_postings_cursor.h
@@ -85,7 +85,8 @@ public:
SniiPostingsCursor(const LogicalIndexReader& idx, format::DictEntry entry,
uint64_t frq_base,
uint64_t prx_base, bool positions, bool scoring,
const format::NormsPodReader* norms, SniiReadWave* wave
= nullptr,
- format::PrxDecodeStats* prx_stats = nullptr);
+ format::PrxDecodeStats* prx_stats = nullptr,
+ ZstdDecompressor* zstd = nullptr);
~SniiPostingsCursor() override;
// A prelude the caller already read; call before the first read.
@@ -187,6 +188,8 @@ private:
const format::NormsPodReader* _norms;
SniiReadWave* _wave;
format::PrxDecodeStats* _prx_stats;
+ // Decompresses the zstd .prx windows when given; each window decompresses
on its own otherwise.
+ ZstdDecompressor* _zstd;
Kind _kind;
// The wave of a cursor given none.
std::unique_ptr<SniiReadWave> _own_wave;
diff --git a/be/test/storage/index/inverted/snii/encoding/zstd_codec_test.cpp
b/be/test/storage/index/inverted/snii/encoding/zstd_codec_test.cpp
index 73f00f1c128..d4b325456e5 100644
--- a/be/test/storage/index/inverted/snii/encoding/zstd_codec_test.cpp
+++ b/be/test/storage/index/inverted/snii/encoding/zstd_codec_test.cpp
@@ -19,11 +19,15 @@
#include <gtest/gtest.h>
+#include <algorithm>
#include <vector>
#include "common/status.h"
+#include "runtime/memory/mem_tracker_limiter.h"
+#include "runtime/thread_context.h"
using namespace doris::snii;
+using doris::AttachTask; // SCOPED_ATTACH_TASK names it unqualified
TEST(SniiZstd, RoundTrip) {
std::vector<uint8_t> in;
@@ -49,3 +53,72 @@ TEST(SniiZstd, EmptyInput) {
ASSERT_TRUE(zstd_decompress(Slice(comp), 0, &decomp).ok());
EXPECT_TRUE(decomp.empty());
}
+
+namespace {
+std::vector<uint8_t> pattern(size_t n, int seed) {
+ std::vector<uint8_t> v(n);
+ for (size_t i = 0; i < n; ++i) {
+ v[i] = static_cast<uint8_t>((i * 31 + seed) % 13);
+ }
+ return v;
+}
+} // namespace
+
+// One decompressor serves frames that grow and shrink; every output equals
its input.
+TEST(SniiZstdDecompressor, ReusedAcrossFramesOfDifferentSizes) {
+ ZstdDecompressor decompressor;
+ for (size_t n : {100UL, 5000UL, 300UL, 0UL, 20000UL, 7UL}) {
+ const std::vector<uint8_t> in = pattern(n, static_cast<int>(n));
+ std::vector<uint8_t> comp;
+ ASSERT_TRUE(zstd_compress(Slice(in), 3, &comp).ok());
+ Slice out;
+ ASSERT_TRUE(decompressor.decompress(Slice(comp), in.size(),
&out).ok()) << n;
+ ASSERT_EQ(out.size(), in.size());
+ EXPECT_TRUE(std::equal(in.begin(), in.end(), out.data())) << n;
+ }
+}
+
+// The errors are the ones zstd_decompress reports, and a failed frame does
not spoil the next one.
+TEST(SniiZstdDecompressor, ReportsCorruptionAndRecovers) {
+ const std::vector<uint8_t> in = pattern(1000, 3);
+ std::vector<uint8_t> comp;
+ ASSERT_TRUE(zstd_compress(Slice(in), 3, &comp).ok());
+ ZstdDecompressor decompressor;
+ Slice out;
+
+ const doris::Status wrong_length = decompressor.decompress(Slice(comp),
in.size() - 1, &out);
+
EXPECT_TRUE(wrong_length.is<doris::ErrorCode::INVERTED_INDEX_FILE_CORRUPTED>())
+ << wrong_length.to_string();
+
+ std::vector<uint8_t> garbage = comp;
+ garbage[garbage.size() / 2] ^= 0xFF;
+ garbage.resize(garbage.size() - 3);
+ const doris::Status corrupt = decompressor.decompress(Slice(garbage),
in.size(), &out);
+ EXPECT_TRUE(corrupt.is<doris::ErrorCode::INVERTED_INDEX_FILE_CORRUPTED>())
+ << corrupt.to_string();
+
+ ASSERT_TRUE(decompressor.decompress(Slice(comp), in.size(), &out).ok());
+ ASSERT_EQ(out.size(), in.size());
+ EXPECT_TRUE(std::equal(in.begin(), in.end(), out.data()));
+}
+
+// The buffer a decompressor keeps between frames is as large as the largest
frame it decoded, so
+// it is charged to the memory tracker of the task that decodes, until the
decompressor goes away.
+TEST(SniiZstdDecompressor, ChargesItsBufferToTheTaskMemTracker) {
+ const std::vector<uint8_t> in = pattern(8 << 20, 5);
+ std::vector<uint8_t> comp;
+ ASSERT_TRUE(zstd_compress(Slice(in), 3, &comp).ok());
+ auto tracker =
doris::MemTrackerLimiter::create_shared(doris::MemTrackerLimiter::Type::OTHER,
+
"UT-SniiZstdDecompressor");
+ SCOPED_ATTACH_TASK(tracker);
+ const int64_t before = tracker->consumption();
+ {
+ ZstdDecompressor decompressor;
+ Slice out;
+ ASSERT_TRUE(decompressor.decompress(Slice(comp), in.size(),
&out).ok());
+ doris::thread_context()->thread_mem_tracker_mgr->flush_untracked_mem();
+ EXPECT_GE(tracker->consumption() - before,
static_cast<int64_t>(in.size()));
+ }
+ doris::thread_context()->thread_mem_tracker_mgr->flush_untracked_mem();
+ EXPECT_LT(tracker->consumption() - before,
static_cast<int64_t>(in.size()));
+}
diff --git a/be/test/storage/index/inverted/snii/format/prx_pod_test.cpp
b/be/test/storage/index/inverted/snii/format/prx_pod_test.cpp
index abb12c1cbf0..d2135be63af 100644
--- a/be/test/storage/index/inverted/snii/format/prx_pod_test.cpp
+++ b/be/test/storage/index/inverted/snii/format/prx_pod_test.cpp
@@ -19,9 +19,11 @@
#include <gtest/gtest.h>
+#include <algorithm>
#include <cstdint>
#include <cstring>
#include <numeric>
+#include <random>
#include <string>
#include <type_traits>
#include <vector>
@@ -543,6 +545,80 @@ TEST(PrxPodTest,
CodecSelectionMatrixMatchesCsrOracleAtLowAndHighTf) {
}
}
+// The plain-payload selective decoder keeps a fast path for single-byte
counts and deltas and hands
+// everything else to the checked decoders. Random windows mix both kinds of
value -- multi-byte
+// deltas, docs without positions, positions near the top of the 32-bit range
-- and random
+// selections, and every result must equal the positions the window was built
from.
+TEST(PrxPodTest, SelectivePlainDecodeMatchesInputForRandomWindows) {
+ std::mt19937 rng(20261028);
+ // A reader-owned decompressor, shared by every window below as a cursor
shares it.
+ doris::snii::ZstdDecompressor shared_zstd;
+ for (int round = 0; round < 300; ++round) {
+ const uint32_t doc_count = 1 + rng() % 400;
+ const uint32_t style = rng() % 4;
+ PerDoc docs(doc_count);
+ for (auto& doc : docs) {
+ const uint32_t count = (style == 3 && rng() % 4 == 0) ? 0 : rng()
% 6;
+ uint32_t position = rng() % 200;
+ for (uint32_t i = 0; i < count; ++i) {
+ doc.push_back(position);
+ uint32_t step = 1 + rng() % 100; // single-byte delta
+ if (style >= 1 && rng() % 8 == 0) {
+ step = 128 + rng() % 20000; // two or three bytes
+ }
+ if (style == 2 && rng() % 40 == 0) {
+ step = 1U << (21 + rng() % 8); // up to five bytes
+ }
+ position = (UINT32_MAX - position < step) ? position :
position + step;
+ }
+ // keep the list strictly valid for the builder: ascending,
duplicates allowed
+ std::sort(doc.begin(), doc.end());
+ }
+ std::vector<uint32_t> ordinals;
+ const uint32_t mode = rng() % 4;
+ for (uint32_t d = 0; d < doc_count; ++d) {
+ const bool take = mode == 0 ? (rng() % 10 == 0)
+ : mode == 1 ? (rng() % 2 == 0)
+ : mode == 2 ? true
+ : (d == 0 || d + 1 == doc_count);
+ if (take) {
+ ordinals.push_back(d);
+ }
+ }
+ for (PrxCodec codec : {PrxCodec::kRaw, PrxCodec::kZstd}) {
+ SCOPED_TRACE("round=" + std::to_string(round) + " docs=" +
std::to_string(doc_count) +
+ " style=" + std::to_string(style) + " mode=" +
std::to_string(mode) +
+ " codec=" +
std::to_string(static_cast<uint8_t>(codec)));
+ const std::vector<uint8_t> encoded = BuildProfileFrame(docs,
codec);
+ std::vector<uint32_t> expected_flat;
+ std::vector<uint32_t> expected_offsets;
+ ExpectedSelectiveCsr(docs, ordinals, &expected_flat,
&expected_offsets);
+ std::vector<uint32_t> flat;
+ std::vector<uint32_t> offsets;
+ ByteSource source((Slice(encoded)));
+ ASSERT_TRUE(read_prx_window_csr_selective(&source, ordinals,
&flat, &offsets).ok());
+ EXPECT_EQ(flat, expected_flat);
+ EXPECT_EQ(offsets, expected_offsets);
+
+ // The same window through the shared decompressor, selectively
and in full.
+ PrxDecodeContext context {.zstd = &shared_zstd};
+ ByteSource reused_source((Slice(encoded)));
+ ASSERT_TRUE(read_prx_window_csr_selective(&reused_source,
ordinals, &flat, &offsets,
+ &context)
+ .ok());
+ EXPECT_EQ(flat, expected_flat);
+ EXPECT_EQ(offsets, expected_offsets);
+ std::vector<uint32_t> all(doc_count);
+ std::iota(all.begin(), all.end(), 0U);
+ ExpectedSelectiveCsr(docs, all, &expected_flat, &expected_offsets);
+ ByteSource full_source((Slice(encoded)));
+ ASSERT_TRUE(read_prx_window_csr(&full_source, &flat, &offsets,
&context).ok());
+ EXPECT_EQ(flat, expected_flat);
+ EXPECT_EQ(offsets, expected_offsets);
+ }
+ }
+}
+
TEST(PrxPodTest, ZstdDeclaredLengthMismatchCommitsNoStats) {
ByteSink sink;
ASSERT_TRUE(build_prx_window(PerDoc {{1, 3}, {7}}, 3, &sink).ok());
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]