This is an automated email from the ASF dual-hosted git repository.
airborne12 pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/doris.git
The following commit(s) were added to refs/heads/master by this push:
new 6f6a8266211 [improvement](be) Decode SNII zstd and raw .prx windows
faster (#69770)
6f6a8266211 is described below
commit 6f6a82662114076eb1bc2ee78efd425994c87bcd
Author: Jack <[email protected]>
AuthorDate: Thu Oct 29 09:10:06 2026 +0800
[improvement](be) Decode SNII zstd and raw .prx windows faster (#69770)
### What problem does this PR solve?
Issue Number: None
Related PR: #69649, #69769
Problem Summary: A phrase query decodes the positions of the selected
documents of every .prx window it reads. Timed per window on a 10M-row
AgentLogs load, a zstd window cost 18.6 us against 1.4 us for a PFOR
window: 11.7 us to decode the plain payload, 4.8 us to decompress, and
about 2 us more because every window created and freed its own zstd
context (about 96 KB) and a zero-filled output vector. Two commits:
1. **Decode the plain payload without a Status per document.**
`decode_payload_csr_selective` made at least one out-of-line call
returning a Status for every document of the window, selected or not
(`get_varint32_fast`, `skip_varints`, `decode_delta_run`). The loop now
keeps the cursor in a register and handles the usual cases inline: a
single-byte count, walking past the deltas of a document that is not
selected, and a run of single-byte deltas. Anything else -- a multi-byte
value, a run that does not fit, a prefix sum that would overflow -- goes
to the checked decoders from the same cursor, so they decide and report
as before. The output vector grows as before; nothing is reserved up
front.
2. **Reuse one zstd context and buffer per index source.**
`ZstdDecompressor` keeps one context, created at the first zstd frame,
and one output buffer that only grows and is not zero-filled
(decompression writes every byte before it is read). The buffer comes
from the Doris allocator (`DorisUniqueBufferPtr`), so the decoding
task's memory tracker accounts for it while it is held.
`SniiIndexSource` owns one, and the cursors it opens pass it to the .prx
decoder through `PrxDecodeContext`. The cursors of a source decode on
its thread one window at a time, and the decoded payload is consumed
before the next window is read. A source lives for one query on one
segment, so a query holds at most one context and one buffer per segment
it reads, and only if it decodes a zstd window; before, each window
created and released them. Compaction decodes through an allocation
gate, which keeps the old path, as does a cursor given no decompressor.
Hot queries, old against new read path in one process (run-time switch
in an experiment build, 6 paired cycles), on 99M AgentLogs rows written
with the smallest frames, as every segment written before #69649 is
(mostly zstd windows): DS7 (unfiltered phrase) 0.87x, A6 0.89x, A10
0.93x, DS4 (phrase prefix) 0.94x, A9 0.95x, DS1 0.96x; repeating the old
path against itself gave 0.93-1.05. The gain follows the zstd windows a
query reads: the phrase queries of AgentLogs written with #69649 read
PFOR windows and do not change, and on TextBench, whose large windows
stay zstd, the time per window is the decompression itself and the
change is within noise. Every query returns the same result.
### Release note
None
---
.../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 8500e23e543..4fe4de76998 100644
--- a/be/src/storage/index/inverted/snii/format/prx_pod.cpp
+++ b/be/src/storage/index/inverted/snii/format/prx_pod.cpp
@@ -1057,9 +1057,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;
@@ -1069,24 +1082,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");
@@ -1148,7 +1191,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");
@@ -1163,7 +1206,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,
@@ -1238,7 +1283,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 56b3aa1f869..a3a3a46d54a 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>
@@ -550,6 +552,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]