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 24dc7762f1a [improvement](be) Account the NULL docids and point buffer
of the SNII BKD writer (#69728)
24dc7762f1a is described below
commit 24dc7762f1a5575b59a160593f05bee39a9c7d87
Author: Jack <[email protected]>
AuthorDate: Thu Oct 29 09:08:38 2026 +0800
[improvement](be) Account the NULL docids and point buffer of the SNII BKD
writer (#69728)
### What problem does this PR solve?
Issue Number: None
Related PR: None
Problem Summary: `SniiBkdIndexColumnWriter`, the numeric, date and IP
index writer, installed no memory reporter. Its point buffer, and its
NULL docid vector, which grows with the row count, were invisible to the
process-wide SNII build tracker (the `SniiIndexBuild` MemTracker and the
`snii_index_build_mem_consumption` gauge) that the text writer feeds.
The writer now creates a reporter in the unregistered population, since
a forced spill cannot reclaim a BKD build, and hands it to the
BkdBuilder. It charges the NULL docids through the growth routine the
text writer already used, which is extracted to
`reserve_docids_for_append` next to `MemoryReporter` so that the two
writers share it. It gives the vector and its charge back as soon as
`finish()` has moved the docids into the bitmap, because IndexBuilder
keeps finished writers alive until the whole rowset is closed.
This only adds accounting. The reporter has no cap, so it cannot fail a
build, and its callback charges a plain MemTracker, not a limiter.
### Release note
The SNII index build memory metric now also counts the point buffers and
NULL rows of numeric, date and IP indexes.
### Compatibility
- No on-disk format change and no reader change: the BKD writer writes
the same bytes as before.
- The reporter only mirrors byte counts into the `SniiIndexBuild`
MemTracker and the `snii_index_build_mem_consumption` gauge through a
callback. It has no cap, so a reservation cannot fail (only an int64
overflow of the counter could), and the callback charges a plain
MemTracker, never a MemTrackerLimiter, which would count the same bytes
twice because the allocator hook already attributes them. So a build
cannot fail where it passed before.
- Nothing is exchanged between nodes, so mixed versions are not
affected. The text writer uses the extracted helper with the same
behavior: the larger capacity is charged before the allocation and the
old charge is returned after the move.
### Performance
- Only the capacity check of `reserve_docids_for_append` is inline in
its callers. The growth step is out of line (`NO_INLINE`), since it runs
O(log n) times per writer. Compiled with `-O2` and `-O3`, the text
writer's `_reserve_null_docids_for_append` goes from about 200
instructions, with a six-register prologue on every call, to about 10
instructions with no prologue when the capacity is enough. An earlier
version of the helper, with the growth step inline, was not inlined into
its callers and cost the text writer one extra call frame per call,
about 2% in the first row of the benchmark below.
- Appending NULL docids in runs of random length, 100 million rows, best
of 5 repetitions per process, interleaved, one pinned core. `plain` is
the BKD writer before this change, `before` the text writer's routine
before, `after` the shared helper:
| docids per call | plain | before | after | after / before |
|---|---|---|---|---|
| 1 to 20 | 60.3 ms | 51.8 ms | 50.5 ms | 0.976 |
| 1 to 200 | 217.9 ms | 244.7 ms | 251.0 ms | 1.026 |
| 1 to 20000 | 363.2 ms | 325.9 ms | 320.1 ms | 0.982 |
The text writer is unchanged within the noise of this machine (medians
vary by up to 7%). The BKD writer pays for the accounting only in the
middle row, 33 ms for 50 million NULL docids, about 0.7 ns each; in the
other two rows it is faster than before.
---
.../index/inverted/snii/snii_bkd_index_writer.cpp | 24 +++++++++++++
.../index/inverted/snii/snii_bkd_index_writer.h | 11 ++++++
.../index/inverted/snii/snii_index_writer.cpp | 18 ++--------
.../index/inverted/snii/writer/memory_reporter.h | 35 ++++++++++++++++++
.../index/inverted/snii/snii_bkd_adapter_test.cpp | 41 ++++++++++++++++++++++
5 files changed, 113 insertions(+), 16 deletions(-)
diff --git a/be/src/storage/index/inverted/snii/snii_bkd_index_writer.cpp
b/be/src/storage/index/inverted/snii/snii_bkd_index_writer.cpp
index 20d71e1143f..cd71f8172e6 100644
--- a/be/src/storage/index/inverted/snii/snii_bkd_index_writer.cpp
+++ b/be/src/storage/index/inverted/snii/snii_bkd_index_writer.cpp
@@ -17,6 +17,7 @@
#include "storage/index/inverted/snii/snii_bkd_index_writer.h"
+#include <algorithm>
#include <cstring>
#include <utility>
@@ -25,6 +26,7 @@
#include "storage/index/inverted/snii/bkd/bkd_types.h"
#include "storage/index/inverted/snii/format/metadata_directory.h"
#include "storage/index/inverted/snii/format/null_bitmap.h"
+#include "storage/index/inverted/snii/writer/snii_build_memory_tracker.h"
#include "storage/index/inverted/snii/writer/snii_compound_writer.h"
#include "storage/key_coder.h"
#include "storage/tablet/tablet_schema.h"
@@ -84,12 +86,26 @@ Status SniiBkdIndexColumnWriter::init() {
}
_value_size = cast_set<uint32_t>(field_type_size(_value_type));
+ // The same accounting the text writer has: the builder's point buffer and
the NULL docids are
+ // mirrored into the process-wide SNII build tracker. Unregistered,
because a forced spill cannot
+ // reclaim them -- the builder spills on its own buffer bound only.
+ _memory_reporter = std::make_unique<::doris::snii::writer::MemoryReporter>(
+ ::doris::snii::writer::snii_build_consume_release(
+
::doris::snii::writer::BuildMemoryPopulation::kUnregistered));
+ _null_docids_reservation = _memory_reporter->make_reservation();
+
::doris::snii::bkd::BkdBuilderOptions options;
options.bytes_per_dim = _value_size;
options.field_type = _value_type;
+ options.reporter = _memory_reporter.get();
return ::doris::snii::bkd::BkdBuilder::create(options, &_builder);
}
+Status SniiBkdIndexColumnWriter::_reserve_null_docids_for_append(size_t count)
{
+ return ::doris::snii::writer::reserve_docids_for_append(
+ _memory_reporter.get(), &_null_docids_reservation, &_null_docids,
count);
+}
+
Status SniiBkdIndexColumnWriter::_add_value(const uint8_t* value, uint32_t
docid) {
DORIS_CHECK(_builder != nullptr);
std::string encoded;
@@ -153,6 +169,7 @@ Status SniiBkdIndexColumnWriter::add_array_values(size_t
field_size, const uint8
}
Status SniiBkdIndexColumnWriter::add_nulls(uint32_t count) {
+ RETURN_IF_ERROR(_reserve_null_docids_for_append(count));
for (uint32_t i = 0; i < count; ++i) {
_null_docids.push_back(_rid);
++_rid;
@@ -167,6 +184,9 @@ Status SniiBkdIndexColumnWriter::add_array_nulls(const
uint8_t* null_map, size_t
// array level.
DORIS_CHECK_GE(_rid, num_rows);
const uint32_t first_rid = _rid - cast_set<uint32_t>(num_rows);
+ const auto null_count =
+ static_cast<size_t>(std::count(null_map, null_map + num_rows,
static_cast<uint8_t>(1)));
+ RETURN_IF_ERROR(_reserve_null_docids_for_append(null_count));
for (size_t i = 0; i < num_rows; ++i) {
if (null_map[i] == 1) {
_null_docids.push_back(first_rid + cast_set<uint32_t>(i));
@@ -191,6 +211,10 @@ Status SniiBkdIndexColumnWriter::finish() {
::doris::snii::format::NullBitmapWriter null_writer;
null_writer.add_many(_null_docids);
+ // The docids live in the bitmap now. This writer can be kept alive long
after finish() returns,
+ // so give the vector and its charge back instead of holding them until
destruction.
+ std::vector<uint32_t>().swap(_null_docids);
+ _null_docids_reservation.reset();
::doris::snii::ByteSink null_sink;
RETURN_IF_ERROR(null_writer.finish(_rid, &null_sink));
diff --git a/be/src/storage/index/inverted/snii/snii_bkd_index_writer.h
b/be/src/storage/index/inverted/snii/snii_bkd_index_writer.h
index 8dfa648f32d..9c837084e90 100644
--- a/be/src/storage/index/inverted/snii/snii_bkd_index_writer.h
+++ b/be/src/storage/index/inverted/snii/snii_bkd_index_writer.h
@@ -27,6 +27,7 @@
#include "storage/index/inverted/snii/bkd/bkd_builder.h"
#include "storage/index/inverted/snii/bkd/staged_blob_file.h"
#include "storage/index/inverted/snii/encoding/byte_sink.h"
+#include "storage/index/inverted/snii/writer/memory_reporter.h"
#include "storage/olap_common.h"
namespace doris {
@@ -84,9 +85,19 @@ private:
uint32_t _value_size = 0;
const KeyCoder* _value_key_coder = nullptr;
+ // Reserves room for `count` more NULL docids, charging the larger
capacity to the reporter first.
+ Status _reserve_null_docids_for_append(size_t count);
+
+ // Build-time RAM accounting, shared with the builder: the NULL docids
below and the builder's
+ // point buffer show up in the process-wide SNII build tracker while they
are held. Declared
+ // before everything that holds a Reservation on it, so it is destroyed
last.
+ std::unique_ptr<::doris::snii::writer::MemoryReporter> _memory_reporter;
+
// Segment-local row id of the NEXT row to arrive. Advanced by both value
// runs and null runs, so it is the row count once the column is exhausted.
uint32_t _rid = 0;
+ // Declared before the vector it charges for, so the vector is freed
before its charge is returned.
+ ::doris::snii::writer::MemoryReporter::Reservation
_null_docids_reservation;
std::vector<uint32_t> _null_docids;
std::unique_ptr<::doris::snii::bkd::BkdBuilder> _builder;
diff --git a/be/src/storage/index/inverted/snii/snii_index_writer.cpp
b/be/src/storage/index/inverted/snii/snii_index_writer.cpp
index 94766c5ade6..e92b61fbb3e 100644
--- a/be/src/storage/index/inverted/snii/snii_index_writer.cpp
+++ b/be/src/storage/index/inverted/snii/snii_index_writer.cpp
@@ -354,23 +354,9 @@ Status SniiIndexColumnWriter::add_array_values(size_t
field_size, const uint8_t*
}
Status SniiIndexColumnWriter::_reserve_null_docids_for_append(size_t count) {
- const size_t need = _null_docids.size() + count;
- // Repeated NULL runs reuse capacity until geometric growth is needed.
- if (need <= _null_docids.capacity()) {
- return Status::OK();
- }
- const size_t target_capacity = std::max(need, _null_docids.capacity() * 2);
// Uninitialized writers in unit tests have no reporter.
- if (_memory_reporter == nullptr) {
- _null_docids.reserve(target_capacity);
- return Status::OK();
- }
- ::doris::snii::writer::MemoryReporter::Reservation replacement;
- RETURN_IF_ERROR(_null_docids_reservation.prepare_replacement(
- static_cast<uint64_t>(target_capacity) * sizeof(uint32_t),
&replacement));
- _null_docids.reserve(target_capacity);
- _null_docids_reservation = std::move(replacement);
- return Status::OK();
+ return ::doris::snii::writer::reserve_docids_for_append(
+ _memory_reporter.get(), &_null_docids_reservation, &_null_docids,
count);
}
// Hand back the token stream for one row. The two ways to get one differ in
ownership AND in
diff --git a/be/src/storage/index/inverted/snii/writer/memory_reporter.h
b/be/src/storage/index/inverted/snii/writer/memory_reporter.h
index 0f81821927c..68bc43385df 100644
--- a/be/src/storage/index/inverted/snii/writer/memory_reporter.h
+++ b/be/src/storage/index/inverted/snii/writer/memory_reporter.h
@@ -23,8 +23,10 @@
#include <functional>
#include <limits>
#include <utility>
+#include <vector>
#include "common/check.h"
+#include "common/compiler_util.h"
#include "common/logging.h"
#include "common/status.h"
@@ -327,4 +329,37 @@ inline void MemoryReporter::release(uint64_t bytes, bool
postings) {
}
}
+// Grows the vector for reserve_docids_for_append. Out of line: growth is
geometric, so it runs
+// O(log n) times, and keeping it out of the callers leaves them only the
capacity check.
+NO_INLINE inline Status grow_docids_for_append(MemoryReporter* reporter,
+ MemoryReporter::Reservation*
reservation,
+ std::vector<uint32_t>* docids,
size_t need) {
+ const size_t target_capacity = std::max(need, docids->capacity() * 2);
+ if (reporter == nullptr) {
+ docids->reserve(target_capacity);
+ return Status::OK();
+ }
+ MemoryReporter::Reservation replacement;
+ RETURN_IF_ERROR(reservation->prepare_replacement(
+ static_cast<uint64_t>(target_capacity) * sizeof(uint32_t),
&replacement));
+ docids->reserve(target_capacity);
+ *reservation = std::move(replacement);
+ return Status::OK();
+}
+
+// Makes room for `count` more docids in a vector whose capacity is charged to
`reservation`; a null
+// `reporter` (unit tests) charges nothing. Capacity grows geometrically, so a
long sequence of small
+// NULL runs does not reallocate and copy the whole array on every call. The
larger capacity is
+// charged BEFORE it is allocated and the old charge is returned only after
the move, which is the
+// transient double residency of a vector growth.
+inline Status reserve_docids_for_append(MemoryReporter* reporter,
+ MemoryReporter::Reservation*
reservation,
+ std::vector<uint32_t>* docids, size_t
count) {
+ const size_t need = docids->size() + count;
+ if (need <= docids->capacity()) {
+ return Status::OK();
+ }
+ return grow_docids_for_append(reporter, reservation, docids, need);
+}
+
} // namespace doris::snii::writer
diff --git a/be/test/storage/index/inverted/snii/snii_bkd_adapter_test.cpp
b/be/test/storage/index/inverted/snii/snii_bkd_adapter_test.cpp
index 3f5296bf38e..6b8cbaac9b7 100644
--- a/be/test/storage/index/inverted/snii/snii_bkd_adapter_test.cpp
+++ b/be/test/storage/index/inverted/snii/snii_bkd_adapter_test.cpp
@@ -21,6 +21,7 @@
#include <cstdint>
#include <memory>
+#include <numeric>
#include <optional>
#include <set>
#include <string>
@@ -29,6 +30,7 @@
#include "common/status.h"
#include "io/fs/local_file_system.h"
#include "runtime/exec_env.h"
+#include "runtime/memory/mem_tracker.h"
#include "runtime/runtime_state.h"
#include "storage/index/index_file_reader.h"
#include "storage/index/index_file_writer.h"
@@ -46,6 +48,7 @@
#include "storage/index/inverted/snii/snii_bkd_index_reader.h"
#include "storage/index/inverted/snii/snii_bkd_index_writer.h"
#include "storage/index/inverted/snii/staged_file_probe.h"
+#include "storage/index/inverted/snii/writer/snii_build_memory_tracker.h"
#include "storage/key_coder.h"
#include "storage/tablet/tablet_schema.h"
@@ -317,6 +320,44 @@ TEST_F(SniiBkdAdapterTest,
SealingDrainsTheStagedDataWhileTheProducerIsStillAliv
assert_ok(index_file_writer.finish_close());
}
+// The writer's build memory is visible in the process-wide SNII build tracker
while it is held, as
+// the text writer's is: the NULL docids until finish() moves them into the
bitmap, the builder's
+// point buffer until the build is done. None of it may stay charged once
finish() returns, even
+// though the producer is still alive (IndexBuilder keeps it until the rowset
is closed).
+TEST_F(SniiBkdAdapterTest, BuildMemoryIsChargedAndGivenBackAtFinish) {
+ ::doris::MemTracker* tracker =
::doris::snii::writer::snii_build_mem_tracker();
+ const int64_t baseline = tracker->consumption();
+ const int64_t registered_baseline =
::doris::snii::writer::snii_registered_build_bytes();
+
+ const std::string prefix = test_path("accounted_producer");
+ io::FileWriterPtr file_writer;
+ assert_ok(io::global_local_filesystem()->create_file(
+ InvertedIndexDescriptor::get_index_file_path_v2(prefix),
&file_writer));
+ IndexFileWriter index_file_writer(io::global_local_filesystem(), prefix,
"accounted_rowset",
+ /*seg_id=*/0,
InvertedIndexStorageFormatPB::SNII,
+ std::move(file_writer),
/*can_use_ram_dir=*/true,
+ /*tablet_id=*/303);
+ SniiBkdIndexColumnWriter writer(&index_file_writer, &_meta, kFieldType);
+ assert_ok(writer.init());
+
+ constexpr uint32_t kNulls = 200000;
+ assert_ok(writer.add_nulls(kNulls));
+ std::vector<int64_t> values(1000);
+ std::iota(values.begin(), values.end(), 0);
+ assert_ok(writer.add_values(reinterpret_cast<const
uint8_t*>(values.data()), values.size()));
+ // At least the 4 bytes of every NULL docid, and the builder's point
buffer on top.
+ EXPECT_GE(tracker->consumption() - baseline, static_cast<int64_t>(kNulls)
* 4);
+ // A BKD build cannot be reclaimed by a forced spill, so it is not offered
to the spill decision.
+ EXPECT_EQ(::doris::snii::writer::snii_registered_build_bytes(),
registered_baseline);
+
+ assert_ok(writer.finish());
+ EXPECT_EQ(tracker->consumption(), baseline)
+ << "finish() left build memory charged to a producer that is still
alive";
+
+ assert_ok(index_file_writer.begin_close());
+ assert_ok(index_file_writer.finish_close());
+}
+
// NULL rows are carried by the SNII null-bitmap POD, not by the point set: a
// NULL that leaked in as a point would answer `col > x` for a row that has no
// value at all.
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]