This is an automated email from the ASF dual-hosted git repository.
ColinLeeo pushed a commit to branch develop
in repository https://gitbox.apache.org/repos/asf/tsfile.git
The following commit(s) were added to refs/heads/develop by this push:
new 585125b12 Optimize C++ batch decode and output fast paths (#898)
585125b12 is described below
commit 585125b12621251410b86c916dc292cfda24d402
Author: Colin Lee <[email protected]>
AuthorDate: Thu Aug 6 10:54:12 2026 +0800
Optimize C++ batch decode and output fast paths (#898)
* Optimize C++ batch decode fast paths
* Fix reader test iterator lifetime
---
cpp/src/common/tsblock/tsblock.h | 22 ++++
.../common/tsblock/vector/fixed_length_vector.h | 7 +
cpp/src/encoding/gorilla_decoder.h | 68 +++++++++-
cpp/src/encoding/ts2diff_decoder.h | 37 ++++++
cpp/src/reader/aligned_chunk_reader.cc | 24 ++++
cpp/test/common/tsblock/tslock_test.cc | 37 ++++++
cpp/test/encoding/encoding_coverage_test.cc | 35 +++++
cpp/test/encoding/gorilla_codec_test.cc | 143 +++++++++++++++++++++
cpp/test/reader/tsfile_reader_test.cc | 92 +++++++++++++
9 files changed, 464 insertions(+), 1 deletion(-)
diff --git a/cpp/src/common/tsblock/tsblock.h b/cpp/src/common/tsblock/tsblock.h
index 7959f7c36..056186211 100644
--- a/cpp/src/common/tsblock/tsblock.h
+++ b/cpp/src/common/tsblock/tsblock.h
@@ -174,6 +174,28 @@ class RowAppender {
}
}
+ FORCE_INLINE bool can_bulk_append_fixed(uint32_t slot_index,
+ uint32_t elem_size) const {
+ ASSERT(slot_index < tsblock_->tuple_desc_->get_column_count());
+ Vector* vec = tsblock_->vectors_[slot_index];
+ TSDataType datatype = vec->get_vector_type();
+ if (datatype == STRING || datatype == TEXT || datatype == BLOB) {
+ return false;
+ }
+ return static_cast<FixedLengthVector*>(vec)->get_type_len() ==
+ elem_size;
+ }
+
+ FORCE_INLINE void bulk_append_fixed(uint32_t slot_index, const char*
values,
+ uint32_t count) {
+ ASSERT(slot_index < tsblock_->tuple_desc_->get_column_count());
+ Vector* vec = tsblock_->vectors_[slot_index];
+ ASSERT(vec->get_vector_type() != STRING &&
+ vec->get_vector_type() != TEXT &&
+ vec->get_vector_type() != BLOB);
+ static_cast<FixedLengthVector*>(vec)->append_batch(values, count);
+ }
+
FORCE_INLINE void append_null(uint32_t slot_index) {
Vector* vec = tsblock_->vectors_[slot_index];
vec->set_null(tsblock_->row_count_ - 1);
diff --git a/cpp/src/common/tsblock/vector/fixed_length_vector.h
b/cpp/src/common/tsblock/vector/fixed_length_vector.h
index df20c97d9..eb5596343 100644
--- a/cpp/src/common/tsblock/vector/fixed_length_vector.h
+++ b/cpp/src/common/tsblock/vector/fixed_length_vector.h
@@ -49,6 +49,13 @@ class FixedLengthVector : public Vector {
values_.append_fixed_value(value, len);
}
+ FORCE_INLINE void append_batch(const char* values, uint32_t count) {
+ values_.append_fixed_value(values, count * type_len_);
+ add_row_nums(count);
+ }
+
+ FORCE_INLINE uint32_t get_type_len() const { return type_len_; }
+
// cppcheck-suppress missingOverride
FORCE_INLINE char* read(uint32_t* __restrict len, bool* __restrict null,
uint32_t rowid) OVERRIDE {
diff --git a/cpp/src/encoding/gorilla_decoder.h
b/cpp/src/encoding/gorilla_decoder.h
index c8c76d965..ccbbdef88 100644
--- a/cpp/src/encoding/gorilla_decoder.h
+++ b/cpp/src/encoding/gorilla_decoder.h
@@ -19,8 +19,13 @@
#ifndef ENCODING_GORILLA_DECODER_H
#define ENCODING_GORILLA_DECODER_H
+#include <algorithm>
#include <climits>
+#if defined(_MSC_VER)
+#include <intrin.h>
+#endif
+
#include "common/allocator/byte_stream.h"
#include "decoder.h"
#include "encode_utils.h"
@@ -30,6 +35,23 @@
namespace storage {
+FORCE_INLINE int gorilla_count_leading_zeros_nonzero(uint64_t value) {
+#if defined(__GNUC__) || defined(__clang__)
+ return __builtin_clzll(value);
+#elif defined(_MSC_VER)
+ unsigned long index;
+ _BitScanReverse64(&index, value);
+ return 63 - static_cast<int>(index);
+#else
+ int count = 0;
+ while ((value & (UINT64_C(1) << 63)) == 0) {
+ value <<= 1;
+ ++count;
+ }
+ return count;
+#endif
+}
+
// ── Raw-pointer bit reader ────────────────────────────────────────────────
// Operates directly on a contiguous byte array, bypassing ByteStream's
// per-byte read_buf() overhead (atomic loads, page boundary checks, memcpy).
@@ -91,6 +113,37 @@ struct GorillaBitReader {
return bit;
}
+ // Consume up to max_count consecutive zero control bits. The first one bit
+ // remains unread so the normal control decoder can handle the following
+ // changed value. This turns long repeated-value runs into one leading-zero
+ // count per reservoir instead of one read_next() call per value.
+ FORCE_INLINE int consume_zero_bits(int max_count) {
+ int consumed = 0;
+ while (consumed < max_count) {
+ if (UNLIKELY(!refill_if_empty())) {
+ break;
+ }
+
+ const int available_bits = bits;
+ const uint64_t aligned =
+ available_bits == 64 ? buffer : buffer << (64 -
available_bits);
+ const int zero_bits =
+ aligned == 0 ? available_bits
+ : gorilla_count_leading_zeros_nonzero(aligned);
+ const int remaining = max_count - consumed;
+ const int take = std::min(zero_bits, remaining);
+ bits -= take;
+ consumed += take;
+
+ // A one bit follows the consumed zeros. Leave it in the reservoir
+ // for read_control_bits(), or stop once the requested limit is
met.
+ if (zero_bits < available_bits || consumed == max_count) {
+ break;
+ }
+ }
+ return consumed;
+ }
+
FORCE_INLINE uint64_t read_long(int n) {
if (UNLIKELY(n < 0 || n > 64)) {
invalid = true;
@@ -460,8 +513,21 @@ class GorillaDecoder : public Decoder {
// Main batch loop
while (actual < capacity && has_next_) {
- out[actual++] =
+ const Output decoded =
GorillaDecodeOutput<T, Output>::convert(stored_value_);
+ const int repeated = r.consume_zero_bits(capacity - actual - 1);
+ const int run_length = repeated + 1;
+ // Include the current value in the bulk fill. Besides removing a
+ // scalar store and a second position update for every run, this
+ // gives the compiler one contiguous range to vectorize. Integer
+ // and floating-point outputs are copied without arithmetic, so
+ // special IEEE-754 bit patterns remain unchanged.
+ std::fill_n(out + actual, run_length, decoded);
+ actual += run_length;
+
+ // Prime the next value even when the repeated run exactly fills
the
+ // caller's buffer. This preserves the scalar decoder invariant
that
+ // stored_value_ is the next value to return on the following call.
if (UNLIKELY(!GorillaRawOps<T>::read_next(
r, stored_value_, stored_leading_zeros_,
stored_trailing_zeros_))) {
diff --git a/cpp/src/encoding/ts2diff_decoder.h
b/cpp/src/encoding/ts2diff_decoder.h
index 224d7402e..206b7f559 100644
--- a/cpp/src/encoding/ts2diff_decoder.h
+++ b/cpp/src/encoding/ts2diff_decoder.h
@@ -648,6 +648,43 @@ inline int
TS2DIFFDecoder<int64_t>::read_batch_int64(int64_t* out, int capacity,
int64_t prev = first_value_;
int32_t i = 0;
+ // An evenly spaced timestamp block has no packed residual data. Build
+ // the arithmetic progression directly instead of entering the generic
+ // bit-extraction path (whose SIMD guard requires readable input
bytes).
+ if (bit_width_ == 0) {
+#ifdef ENABLE_SIMD
+ if (remaining >= 4) {
+ int64_t value1 = prev + delta_min_;
+ int64_t value2 = value1 + delta_min_;
+ int64_t value3 = value2 + delta_min_;
+ int64_t value4 = value3 + delta_min_;
+ simde__m256i values =
+ simde_mm256_set_epi64x(value4, value3, value2, value1);
+
+ simde__m256i step = simde_mm256_set1_epi64x(delta_min_);
+ step = simde_mm256_add_epi64(step, step);
+ step = simde_mm256_add_epi64(step, step);
+
+ for (; i + 3 < remaining; i += 4) {
+ simde_mm256_storeu_si256(
+ reinterpret_cast<simde__m256i*>(out + actual), values);
+ actual += 4;
+ values = simde_mm256_add_epi64(values, step);
+ }
+ prev = out[actual - 1];
+ }
+#endif
+
+ for (; i < remaining; ++i) {
+ prev += delta_min_;
+ out[actual++] = prev;
+ }
+
+ first_value_ = prev;
+ current_index_ = 0;
+ continue;
+ }
+
#ifdef ENABLE_SIMD
// SIMD path: decode 4 INT64 values at a time
for (; i + 3 < remaining; i += 4) {
diff --git a/cpp/src/reader/aligned_chunk_reader.cc
b/cpp/src/reader/aligned_chunk_reader.cc
index 97c469288..a140c1e98 100644
--- a/cpp/src/reader/aligned_chunk_reader.cc
+++ b/cpp/src/reader/aligned_chunk_reader.cc
@@ -922,6 +922,30 @@ int AlignedChunkReader::decode_tv_batch(ByteStream&
time_in,
}
}
+ // Dense fixed-width batches are already laid out exactly as the two
+ // destination vectors expect. Appending them row by row would issue
+ // two tiny memcpy calls per row (time + value), plus virtual dispatch
+ // and bookkeeping. Copy each column once instead. Integral value
+ // filters still need the scalar satisfy(time, value) check unless the
+ // decoder proved the whole block passes, so those batches retain the
+ // fallback below.
+ const bool needs_integral_value_filter =
+ std::is_integral<T>::value && filter != nullptr && !block_all_pass;
+ if (pass_count == time_count && nonnull_count == time_count &&
+ !needs_integral_value_filter &&
+ row_appender.can_bulk_append_fixed(0, sizeof(int64_t)) &&
+ row_appender.can_bulk_append_fixed(1, sizeof(T))) {
+ row_appender.bulk_append_fixed(0,
+ reinterpret_cast<const
char*>(times),
+ static_cast<uint32_t>(time_count));
+ row_appender.bulk_append_fixed(
+ 1, reinterpret_cast<const char*>(values),
+ static_cast<uint32_t>(time_count));
+ row_appender.add_rows(static_cast<uint32_t>(time_count));
+ cur_value_index += time_count;
+ continue;
+ }
+
int val_idx = 0;
for (int i = 0; i < time_count; ++i) {
cur_value_index++;
diff --git a/cpp/test/common/tsblock/tslock_test.cc
b/cpp/test/common/tsblock/tslock_test.cc
index 750585aaf..c58b368f5 100644
--- a/cpp/test/common/tsblock/tslock_test.cc
+++ b/cpp/test/common/tsblock/tslock_test.cc
@@ -63,6 +63,43 @@ TEST(TsBlockTest, ColAppender_AddRowAndAppend) {
EXPECT_EQ(col_appender.get_col_row_count(), 50);
}
+TEST(TsBlockTest, RowAppenderBulkAppendFixedAtExactCapacity) {
+ TupleDesc tuple_desc;
+ tuple_desc.push_back(ColumnSchema("time", TIMESTAMP, UNCOMPRESSED, PLAIN));
+ tuple_desc.push_back(ColumnSchema("value", FLOAT, UNCOMPRESSED, PLAIN));
+ TsBlock ts_block(&tuple_desc, 4);
+ ASSERT_EQ(ts_block.init(), E_OK);
+ RowAppender row_appender(&ts_block);
+
+ const int64_t times[] = {101, 103, 107, 109};
+ const float values[] = {1.25f, 2.5f, 3.75f, 5.0f};
+ ASSERT_TRUE(row_appender.can_bulk_append_fixed(0, sizeof(int64_t)));
+ ASSERT_TRUE(row_appender.can_bulk_append_fixed(1, sizeof(float)));
+ EXPECT_FALSE(row_appender.can_bulk_append_fixed(1, sizeof(double)));
+
+ row_appender.bulk_append_fixed(0, reinterpret_cast<const char*>(times), 4);
+ row_appender.bulk_append_fixed(1, reinterpret_cast<const char*>(values),
4);
+ row_appender.add_rows(4);
+
+ EXPECT_EQ(ts_block.get_row_count(), 4u);
+ EXPECT_EQ(row_appender.remaining(), 0u);
+ EXPECT_EQ(ts_block.get_vector(0)->get_row_num(), 4u);
+ EXPECT_EQ(ts_block.get_vector(1)->get_row_num(), 4u);
+
+ ColIterator time_iter(0, &ts_block);
+ ColIterator value_iter(1, &ts_block);
+ for (uint32_t i = 0; i < 4; ++i) {
+ uint32_t len = 0;
+ EXPECT_EQ(*reinterpret_cast<int64_t*>(time_iter.read(&len)), times[i]);
+ EXPECT_EQ(len, sizeof(int64_t));
+ EXPECT_FLOAT_EQ(*reinterpret_cast<float*>(value_iter.read(&len)),
+ values[i]);
+ EXPECT_EQ(len, sizeof(float));
+ time_iter.next();
+ value_iter.next();
+ }
+}
+
TEST(TsBlockTest, RowIterator_ReadAndNext) {
TupleDesc tuple_desc;
ColumnSchema col1("test_col1", INT32, SNAPPY, RLE);
diff --git a/cpp/test/encoding/encoding_coverage_test.cc
b/cpp/test/encoding/encoding_coverage_test.cc
index 6970b9387..5d6ef813e 100644
--- a/cpp/test/encoding/encoding_coverage_test.cc
+++ b/cpp/test/encoding/encoding_coverage_test.cc
@@ -330,6 +330,41 @@ TEST(EncodingCoverage, TS2DIFFBatchInt64MultipleBlocks) {
for (int i = 0; i < N; i++) EXPECT_EQ(out[i], values[i]) << "i=" << i;
}
+TEST(EncodingCoverage, TS2DIFFBatchInt64EvenlySpacedNegativeDelta) {
+ TS2DIFFEncoder<int64_t> enc;
+ common::ByteStream s(8192, common::MOD_DEFAULT);
+ // Full encoder blocks contain 127 residuals, so this covers repeated SIMD
+ // groups, the 3-value scalar tail, and a final partial block.
+ const int N = 389;
+ std::vector<int64_t> values(N);
+ for (int i = 0; i < N; i++) {
+ values[i] = INT64_C(9000000000000) - static_cast<int64_t>(i) * 29;
+ ASSERT_EQ(enc.encode(values[i], s), common::E_OK);
+ }
+ ASSERT_EQ(enc.flush(s), common::E_OK);
+
+ uint32_t total = s.total_size();
+ std::vector<uint8_t> buf(total);
+ uint32_t got = 0;
+ s.read_buf(buf.data(), total, got);
+ common::ByteStream wrapped(common::MOD_DEFAULT);
+ wrapped.wrap_from((const char*)buf.data(), total);
+
+ TS2DIFFDecoder<int64_t> dec;
+ std::vector<int64_t> out(N);
+ int total_decoded = 0;
+ while (dec.has_remaining(wrapped) && total_decoded < N) {
+ int actual = 0;
+ ASSERT_EQ(dec.read_batch_int64(out.data() + total_decoded,
+ N - total_decoded, actual, wrapped),
+ common::E_OK);
+ if (actual == 0) break;
+ total_decoded += actual;
+ }
+ EXPECT_EQ(total_decoded, N);
+ for (int i = 0; i < N; i++) EXPECT_EQ(out[i], values[i]) << "i=" << i;
+}
+
// ── Plain encoder: encode_batch fast paths for each type ───────────────
TEST(EncodingCoverage, PlainEncoderBatchAllTypes) {
PlainEncoder enc;
diff --git a/cpp/test/encoding/gorilla_codec_test.cc
b/cpp/test/encoding/gorilla_codec_test.cc
index 039a9a4f0..ccf0ae1da 100644
--- a/cpp/test/encoding/gorilla_codec_test.cc
+++ b/cpp/test/encoding/gorilla_codec_test.cc
@@ -324,6 +324,108 @@ TEST_F(GorillaCodecTest, FloatBatchDecode) {
}
}
+TEST_F(GorillaCodecTest, FloatBatchDecodeLongRepeatedRuns) {
+ storage::FloatGorillaEncoder encoder;
+ common::ByteStream stream(1024, common::MOD_DEFAULT);
+ const int N = 600;
+ std::vector<float> expected(N);
+ for (int i = 0; i < N; i++) {
+ if (i < 258) {
+ // Two complete 129-row batches. The repeated run crosses several
+ // 64-bit reservoirs and ends exactly at a batch boundary.
+ expected[i] = 17.25f;
+ } else if (i < 517) {
+ expected[i] = -3.5f;
+ } else {
+ expected[i] = static_cast<float>(i - 517) * 0.125f;
+ }
+ ASSERT_EQ(encoder.encode(expected[i], stream), common::E_OK);
+ }
+ ASSERT_EQ(encoder.flush(stream), common::E_OK);
+
+ const uint32_t total = stream.total_size();
+ std::vector<uint8_t> encoded(total);
+ uint32_t got = 0;
+ stream.read_buf(encoded.data(), total, got);
+ ASSERT_EQ(got, total);
+
+ common::ByteStream wrapped(common::MOD_DEFAULT);
+ wrapped.wrap_from(reinterpret_cast<const char*>(encoded.data()), total);
+ storage::FloatGorillaDecoder decoder;
+ std::vector<float> decoded(N);
+ int total_decoded = 0;
+ while (total_decoded < N) {
+ const int capacity = std::min(129, N - total_decoded);
+ int actual = 0;
+ ASSERT_EQ(decoder.read_batch_float(decoded.data() + total_decoded,
+ capacity, actual, wrapped),
+ common::E_OK);
+ ASSERT_EQ(actual, capacity);
+ total_decoded += actual;
+ }
+
+ for (int i = 0; i < N; i++) {
+ EXPECT_EQ(common::float_to_int(decoded[i]),
+ common::float_to_int(expected[i]))
+ << "i=" << i;
+ }
+}
+
+TEST_F(GorillaCodecTest, FloatBatchRepeatedFillPreservesBitPatterns) {
+ constexpr uint32_t NAN_BITS = 0x7FC12345U;
+ float payload_nan = 0;
+ std::memcpy(&payload_nan, &NAN_BITS, sizeof(payload_nan));
+ ASSERT_TRUE(std::isnan(payload_nan));
+ ASSERT_NE(common::float_to_int(payload_nan),
+ common::float_to_int(GORILLA_ENCODING_ENDING_FLOAT));
+
+ const int N = 520;
+ std::vector<float> expected(N);
+ for (int i = 0; i < N; i++) {
+ if (i < 257) {
+ expected[i] = payload_nan;
+ } else if (i < 514) {
+ expected[i] = -0.0f;
+ } else {
+ expected[i] = static_cast<float>(i - 514) + 0.25f;
+ }
+ }
+
+ storage::FloatGorillaEncoder encoder;
+ common::ByteStream stream(1024, common::MOD_DEFAULT);
+ for (float value : expected) {
+ ASSERT_EQ(encoder.encode(value, stream), common::E_OK);
+ }
+ ASSERT_EQ(encoder.flush(stream), common::E_OK);
+
+ const uint32_t total = stream.total_size();
+ std::vector<uint8_t> encoded(total);
+ uint32_t got = 0;
+ stream.read_buf(encoded.data(), total, got);
+ ASSERT_EQ(got, total);
+
+ common::ByteStream wrapped(common::MOD_DEFAULT);
+ wrapped.wrap_from(reinterpret_cast<const char*>(encoded.data()), total);
+ storage::FloatGorillaDecoder decoder;
+ std::vector<float> decoded(N);
+ int total_decoded = 0;
+ while (total_decoded < N) {
+ const int capacity = std::min(129, N - total_decoded);
+ int actual = 0;
+ ASSERT_EQ(decoder.read_batch_float(decoded.data() + total_decoded,
+ capacity, actual, wrapped),
+ common::E_OK);
+ ASSERT_EQ(actual, capacity);
+ total_decoded += actual;
+ }
+
+ for (int i = 0; i < N; i++) {
+ EXPECT_EQ(common::float_to_int(decoded[i]),
+ common::float_to_int(expected[i]))
+ << "i=" << i;
+ }
+}
+
TEST_F(GorillaCodecTest, FloatBatchDecodeUnwrappedInput) {
storage::FloatGorillaEncoder encoder;
common::ByteStream stream(1024, common::MOD_DEFAULT);
@@ -418,6 +520,47 @@ TEST_F(GorillaCodecTest, DoubleBatchDecodeOneValueAtATime)
{
}
}
+TEST_F(GorillaCodecTest, Int64BatchDecodeLongRepeatedRuns) {
+ storage::LongGorillaEncoder encoder;
+ common::ByteStream stream(1024, common::MOD_DEFAULT);
+ const int N = 513;
+ std::vector<int64_t> expected(N);
+ for (int i = 0; i < N; i++) {
+ if (i < 333) {
+ expected[i] = 0x123456789ABCDEFLL;
+ } else if (i < 500) {
+ expected[i] = -9876543210LL;
+ } else {
+ expected[i] = static_cast<int64_t>(i) * 17 - 9;
+ }
+ ASSERT_EQ(encoder.encode(expected[i], stream), common::E_OK);
+ }
+ ASSERT_EQ(encoder.flush(stream), common::E_OK);
+
+ const uint32_t total = stream.total_size();
+ std::vector<uint8_t> encoded(total);
+ uint32_t got = 0;
+ stream.read_buf(encoded.data(), total, got);
+ ASSERT_EQ(got, total);
+
+ common::ByteStream wrapped(common::MOD_DEFAULT);
+ wrapped.wrap_from(reinterpret_cast<const char*>(encoded.data()), total);
+ storage::LongGorillaDecoder decoder;
+ std::vector<int64_t> decoded(N);
+ int total_decoded = 0;
+ while (total_decoded < N) {
+ const int capacity = std::min(73, N - total_decoded);
+ int actual = 0;
+ ASSERT_EQ(decoder.read_batch_int64(decoded.data() + total_decoded,
+ capacity, actual, wrapped),
+ common::E_OK);
+ ASSERT_EQ(actual, capacity);
+ total_decoded += actual;
+ }
+
+ EXPECT_EQ(decoded, expected);
+}
+
TEST_F(GorillaCodecTest, DoubleBatchScalarAndSkipInterleave) {
storage::DoubleGorillaEncoder encoder;
common::ByteStream stream(1024, common::MOD_DEFAULT);
diff --git a/cpp/test/reader/tsfile_reader_test.cc
b/cpp/test/reader/tsfile_reader_test.cc
index cdb6d33cf..df4c642a8 100644
--- a/cpp/test/reader/tsfile_reader_test.cc
+++ b/cpp/test/reader/tsfile_reader_test.cc
@@ -23,6 +23,7 @@
#include <cmath>
#include <map>
+#include <numeric>
#include <random>
#include <unordered_map>
#include <vector>
@@ -1511,6 +1512,97 @@ TEST_F(TsFileReaderTest,
AlignedSchemaReportsValueDataType) {
reader.close();
}
+TEST_F(TsFileReaderTest,
+ AlignedFloatBatchCopyPreservesDenseNullAndFilteredAlignment) {
+ const std::string device = "root.dev_aligned_float_batch";
+ MeasurementSchema schema("v0", FLOAT, GORILLA, UNCOMPRESSED);
+ ASSERT_EQ(tsfile_writer_->register_aligned_timeseries(device, schema),
+ E_OK);
+
+ const int row_count = 300;
+ const int null_row = 150;
+ auto schemas = std::make_shared<std::vector<MeasurementSchema>>(1, schema);
+ Tablet tablet(device, schemas, row_count);
+ for (int row = 0; row < row_count; ++row) {
+ ASSERT_EQ(tablet.add_timestamp(row, 10000 + row * 3), E_OK);
+ if (row != null_row) {
+ ASSERT_EQ(tablet.add_value(row, 0u, row + 0.25f), E_OK);
+ }
+ }
+ ASSERT_EQ(tsfile_writer_->write_tablet_aligned(tablet), E_OK);
+ ASSERT_EQ(tsfile_writer_->flush(), E_OK);
+ ASSERT_EQ(tsfile_writer_->close(), E_OK);
+
+ storage::TsFileIOReader io_reader;
+ ASSERT_EQ(io_reader.init(file_name_), E_OK);
+ auto device_id = std::make_shared<StringArrayDeviceID>(device);
+
+ auto scan_and_check = [&](storage::Filter* filter,
+ const std::vector<int>& expected_rows) {
+ storage::TsFileSeriesScanIterator* ssi = nullptr;
+ common::PageArena pa;
+ pa.init(512, common::MOD_TSFILE_READER);
+ ASSERT_EQ(io_reader.alloc_ssi(device_id, "v0", ssi, pa, filter), E_OK);
+ ASSERT_NE(ssi, nullptr);
+
+ size_t expected_index = 0;
+ while (true) {
+ common::TsBlock* block = nullptr;
+ int ret = ssi->get_next(block, /*alloc_tsblock=*/true, filter);
+ if (ret == E_NO_MORE_DATA) break;
+ ASSERT_EQ(ret, E_OK);
+ ASSERT_NE(block, nullptr);
+
+ {
+ common::ColIterator time_iter(0, block);
+ common::ColIterator value_iter(1, block);
+ while (!time_iter.end()) {
+ ASSERT_LT(expected_index, expected_rows.size());
+ const int expected_row = expected_rows[expected_index++];
+ uint32_t len = 0;
+ bool is_null = false;
+
EXPECT_EQ(*reinterpret_cast<int64_t*>(time_iter.read(&len)),
+ 10000 + expected_row * 3);
+ EXPECT_EQ(len, sizeof(int64_t));
+ char* value = value_iter.read(&len, &is_null);
+ if (expected_row == null_row) {
+ EXPECT_TRUE(is_null);
+ EXPECT_EQ(value, nullptr);
+ } else {
+ ASSERT_FALSE(is_null);
+ ASSERT_NE(value, nullptr);
+ EXPECT_EQ(len, sizeof(float));
+ EXPECT_FLOAT_EQ(*reinterpret_cast<float*>(value),
+ expected_row + 0.25f);
+ }
+ time_iter.next();
+ value_iter.next();
+ }
+ }
+ ssi->revert_tsblock();
+ }
+ EXPECT_EQ(expected_index, expected_rows.size());
+ io_reader.revert_ssi(ssi);
+ };
+
+ std::vector<int> all_rows(row_count);
+ std::iota(all_rows.begin(), all_rows.end(), 0);
+ scan_and_check(/*filter=*/nullptr, all_rows);
+
+ std::vector<int64_t> selected_times;
+ std::vector<int> selected_rows;
+ for (int row = 0; row < row_count; row += 11) {
+ selected_rows.push_back(row);
+ selected_times.push_back(10000 + row * 3);
+ }
+ selected_rows.push_back(null_row);
+ selected_times.push_back(10000 + null_row * 3);
+ std::sort(selected_rows.begin(), selected_rows.end());
+ std::sort(selected_times.begin(), selected_times.end());
+ storage::TimeIn time_filter(selected_times, /*not_in=*/false);
+ scan_and_check(&time_filter, selected_rows);
+}
+
namespace storage {
class TsFileReaderMetaArenaTest {
public: