| /* |
| * Licensed to the Apache Software Foundation (ASF) under one |
| * or more contributor license agreements. See the NOTICE file |
| * distributed with this work for additional information |
| * regarding copyright ownership. The ASF licenses this file |
| * to you under the Apache License, Version 2.0 (the |
| * License); you may not use this file except in compliance |
| * with the License. You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, |
| * software distributed under the License is distributed on an |
| * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY |
| * KIND, either express or implied. See the License for the |
| * specific language governing permissions and limitations |
| * under the License. |
| */ |
| |
| #include "aligned_chunk_reader.h" |
| |
| #include <algorithm> |
| #include <limits> |
| #include <type_traits> |
| |
| #include "common/global.h" |
| #ifdef ENABLE_THREADS |
| #include "common/thread_pool.h" |
| #endif |
| #include "compress/compressor_factory.h" |
| #include "encoding/decoder_factory.h" |
| |
| using namespace common; |
| namespace storage { |
| |
| int AlignedChunkReader::init(ReadFile* read_file, String m_name, |
| TSDataType data_type, Filter* time_filter) { |
| read_file_ = read_file; |
| measurement_name_.shallow_copy_from(m_name); |
| time_decoder_ = DecoderFactory::alloc_time_decoder(); |
| value_decoder_ = nullptr; |
| time_compressor_ = nullptr; |
| value_compressor_ = nullptr; |
| time_filter_ = time_filter; |
| time_uncompressed_buf_ = nullptr; |
| value_uncompressed_buf_ = nullptr; |
| if (IS_NULL(time_decoder_)) { |
| return E_OOM; |
| } |
| return E_OK; |
| } |
| |
| void AlignedChunkReader::reset() { |
| time_chunk_meta_ = nullptr; |
| value_chunk_meta_ = nullptr; |
| time_chunk_header_.reset(); |
| value_chunk_header_.reset(); |
| cur_time_page_header_.reset(); |
| cur_value_page_header_.reset(); |
| |
| char* file_data_buf = time_in_stream_.get_wrapped_buf(); |
| if (file_data_buf != nullptr) { |
| mem_free(file_data_buf); |
| } |
| time_in_stream_.clear_wrapped_buf(); |
| time_in_stream_.reset(); |
| file_data_buf = value_in_stream_.get_wrapped_buf(); |
| if (file_data_buf != nullptr) { |
| mem_free(file_data_buf); |
| } |
| value_in_stream_.clear_wrapped_buf(); |
| value_in_stream_.reset(); |
| file_data_time_buf_size_ = 0; |
| file_data_value_buf_size_ = 0; |
| time_chunk_visit_offset_ = 0; |
| value_chunk_visit_offset_ = 0; |
| page_plan_built_ = false; |
| current_page_loaded_ = false; |
| current_page_plan_index_ = 0; |
| time_predecoded_ = false; |
| page_all_times_.clear(); |
| page_time_count_ = 0; |
| page_time_cursor_ = 0; |
| |
| // Free leftover uncompressed buffers from the previous chunk. |
| if (time_uncompressed_buf_ != nullptr && time_compressor_ != nullptr) { |
| time_compressor_->after_uncompress(time_uncompressed_buf_); |
| time_uncompressed_buf_ = nullptr; |
| } |
| |
| // Multi-value reset |
| for (auto* col : value_columns_) { |
| // Free uncompressed buffer before resetting. |
| if (col->uncompressed_buf != nullptr && col->compressor != nullptr) { |
| col->compressor->after_uncompress(col->uncompressed_buf); |
| col->uncompressed_buf = nullptr; |
| } |
| char* buf = col->in_stream.get_wrapped_buf(); |
| if (buf != nullptr) mem_free(buf); |
| col->in_stream.clear_wrapped_buf(); |
| col->in_stream.reset(); |
| col->in.reset(); |
| col->chunk_header.reset(); |
| col->cur_page_header.reset(); |
| col->file_data_buf_size = 0; |
| col->chunk_visit_offset = 0; |
| col->notnull_bitmap.clear(); |
| col->cur_value_index = -1; |
| col->chunk_meta = nullptr; |
| for (auto& pps : col->per_page_state) { |
| pps.predecode_pa.destroy(); |
| } |
| col->per_page_state.clear(); |
| col->pending_decoded_values.clear(); |
| col->pending_decoded_count = 0; |
| col->pending_decoded_cursor = 0; |
| col->pending_decoded = false; |
| // Note: decoder/compressor are NOT freed here — they are reused by |
| // alloc_compressor_and_decoder() in load_by_aligned_meta_multi(). |
| } |
| release_current_page_state(); |
| chunk_pages_.clear(); |
| per_page_times_.clear(); |
| } |
| |
| void AlignedChunkReader::destroy() { |
| // .clear() leaves the vector's internal heap buffer allocated, which |
| // mem_free can't reach because we placement-new the reader. swap with |
| // an empty vector to actually release the backing storage so ASan's |
| // LeakSanitizer doesn't flag the (rather large) ChunkPageInfo buffers. |
| std::vector<ChunkPageInfo>{}.swap(chunk_pages_); |
| std::vector<int64_t>{}.swap(page_all_times_); |
| if (time_uncompressed_buf_ != nullptr && time_compressor_ != nullptr) { |
| time_compressor_->after_uncompress(time_uncompressed_buf_); |
| time_uncompressed_buf_ = nullptr; |
| } |
| if (value_uncompressed_buf_ != nullptr && value_compressor_ != nullptr) { |
| value_compressor_->after_uncompress(value_uncompressed_buf_); |
| value_uncompressed_buf_ = nullptr; |
| } |
| value_page_col_notnull_bitmap_.clear(); |
| value_page_col_notnull_bitmap_.shrink_to_fit(); |
| if (time_decoder_ != nullptr) { |
| time_decoder_->~Decoder(); |
| DecoderFactory::free(time_decoder_); |
| time_decoder_ = nullptr; |
| } |
| if (value_decoder_ != nullptr) { |
| value_decoder_->~Decoder(); |
| DecoderFactory::free(value_decoder_); |
| value_decoder_ = nullptr; |
| } |
| if (time_compressor_ != nullptr) { |
| time_compressor_->~Compressor(); |
| CompressorFactory::free(time_compressor_); |
| time_compressor_ = nullptr; |
| } |
| if (value_compressor_ != nullptr) { |
| value_compressor_->~Compressor(); |
| CompressorFactory::free(value_compressor_); |
| value_compressor_ = nullptr; |
| } |
| char* buf = time_in_stream_.get_wrapped_buf(); |
| if (buf != nullptr) { |
| mem_free(buf); |
| time_in_stream_.clear_wrapped_buf(); |
| } |
| cur_time_page_header_.reset(); |
| buf = value_in_stream_.get_wrapped_buf(); |
| if (buf != nullptr) { |
| mem_free(buf); |
| value_in_stream_.clear_wrapped_buf(); |
| } |
| cur_value_page_header_.reset(); |
| chunk_header_.~ChunkHeader(); |
| |
| // Multi-value destroy |
| for (size_t ci = 0; ci < value_columns_.size(); ci++) { |
| auto* col = value_columns_[ci]; |
| if (col->decoder != nullptr) { |
| col->decoder->~Decoder(); |
| DecoderFactory::free(col->decoder); |
| col->decoder = nullptr; |
| } |
| if (col->compressor != nullptr) { |
| col->compressor->~Compressor(); |
| CompressorFactory::free(col->compressor); |
| col->compressor = nullptr; |
| } |
| for (auto& pps : col->per_page_state) { |
| pps.predecode_pa.destroy(); |
| } |
| col->per_page_state.clear(); |
| col->pending_decoded_values.clear(); |
| buf = col->in_stream.get_wrapped_buf(); |
| if (buf != nullptr) { |
| mem_free(buf); |
| col->in_stream.clear_wrapped_buf(); |
| } |
| col->cur_page_header.reset(); |
| delete col; |
| } |
| // This reader is placement-new'd and torn down via destroy() + mem_free |
| // without ever running ~AlignedChunkReader (see |
| // TsFileSeriesScanIterator::destroy), so .clear() would leave these |
| // vectors' backing buffers allocated and unreachable. swap with an empty |
| // vector to actually release the storage, matching the chunk_pages_ / |
| // page_all_times_ handling above. |
| std::vector<ValueColumnState*>().swap(value_columns_); |
| release_current_page_state(); |
| std::vector<std::vector<int64_t>>().swap(per_page_times_); |
| #ifdef ENABLE_THREADS |
| decode_pool_ = nullptr; // borrowed, not owned |
| for (auto* d : time_decoder_pool_) { |
| if (d != nullptr) { |
| d->~Decoder(); |
| DecoderFactory::free(d); |
| } |
| } |
| std::vector<Decoder*>().swap(time_decoder_pool_); |
| for (auto* c : time_compressor_pool_) { |
| if (c != nullptr) { |
| c->~Compressor(); |
| CompressorFactory::free(c); |
| } |
| } |
| std::vector<Compressor*>().swap(time_compressor_pool_); |
| #endif |
| } |
| |
| int AlignedChunkReader::load_by_aligned_meta(ChunkMeta* time_chunk_meta, |
| ChunkMeta* value_chunk_meta) { |
| int ret = E_OK; |
| time_chunk_meta_ = time_chunk_meta; |
| value_chunk_meta_ = value_chunk_meta; |
| #if DEBUG_SE |
| std::cout << "AlignedChunkReader::load_by_meta, meta=" << *time_chunk_meta |
| << ", " << *value_chunk_meta << std::endl; |
| #endif |
| /* ================ deserialize time_chunk_header ================*/ |
| // TODO configurable |
| file_data_time_buf_size_ = 1024; |
| file_data_value_buf_size_ = 1024; |
| int32_t ret_read_len = 0; |
| char* time_file_data_buf = |
| (char*)mem_alloc(file_data_time_buf_size_, MOD_CHUNK_READER); |
| if (IS_NULL(time_file_data_buf)) { |
| return E_OOM; |
| } |
| ret = read_file_->read(time_chunk_meta_->offset_of_chunk_header_, |
| time_file_data_buf, file_data_time_buf_size_, |
| ret_read_len); |
| if (IS_SUCC(ret) && ret_read_len < ChunkHeader::MIN_SERIALIZED_SIZE) { |
| ret = E_TSFILE_CORRUPTED; |
| LOGE("file corrupted, ret=" << ret << ", offset=" |
| << time_chunk_meta_->offset_of_chunk_header_ |
| << "read_len=" << ret_read_len); |
| mem_free(time_file_data_buf); |
| } |
| if (IS_SUCC(ret)) { |
| time_in_stream_.wrap_from(time_file_data_buf, ret_read_len); |
| if (RET_FAIL(time_chunk_header_.deserialize_from(time_in_stream_))) { |
| } else { |
| time_chunk_visit_offset_ = time_in_stream_.read_pos(); |
| } |
| } |
| /* ================ deserialize value_chunk_header ================*/ |
| ret_read_len = 0; |
| char* value_file_data_buf = |
| (char*)mem_alloc(file_data_value_buf_size_, MOD_CHUNK_READER); |
| if (IS_NULL(value_file_data_buf)) { |
| return E_OOM; |
| } |
| ret = read_file_->read(value_chunk_meta_->offset_of_chunk_header_, |
| value_file_data_buf, file_data_value_buf_size_, |
| ret_read_len); |
| if (IS_SUCC(ret) && ret_read_len < ChunkHeader::MIN_SERIALIZED_SIZE) { |
| ret = E_TSFILE_CORRUPTED; |
| LOGE("file corrupted, ret=" |
| << ret << ", offset=" << value_chunk_meta_->offset_of_chunk_header_ |
| << "read_len=" << ret_read_len); |
| mem_free(value_file_data_buf); |
| } |
| if (IS_SUCC(ret)) { |
| value_in_stream_.wrap_from(value_file_data_buf, ret_read_len); |
| if (RET_FAIL(value_chunk_header_.deserialize_from(value_in_stream_))) { |
| } else if (RET_FAIL(alloc_compressor_and_decoder( |
| time_decoder_, time_compressor_, |
| time_chunk_header_.encoding_type_, |
| time_chunk_header_.data_type_, |
| time_chunk_header_.compression_type_))) { |
| } else if (RET_FAIL(alloc_compressor_and_decoder( |
| value_decoder_, value_compressor_, |
| value_chunk_header_.encoding_type_, |
| value_chunk_header_.data_type_, |
| value_chunk_header_.compression_type_))) { |
| } else { |
| value_chunk_visit_offset_ = value_in_stream_.read_pos(); |
| #if DEBUG_SE |
| std::cout << "AlignedChunkReader::load_by_meta, time_chunk_header=" |
| << time_chunk_header_ |
| << ", value_chunk_header=" << value_chunk_header_ |
| << std::endl; |
| #endif |
| } |
| } |
| return ret; |
| } |
| |
| int AlignedChunkReader::alloc_compressor_and_decoder( |
| storage::Decoder*& decoder, storage::Compressor*& compressor, |
| TSEncoding encoding, TSDataType data_type, CompressionType compression) { |
| if (decoder != nullptr) { |
| decoder->reset(); |
| } else { |
| decoder = DecoderFactory::alloc_value_decoder(encoding, data_type); |
| if (IS_NULL(decoder)) { |
| return E_OOM; |
| } |
| } |
| |
| if (compressor != nullptr) { |
| compressor->reset(false); |
| } else { |
| compressor = CompressorFactory::alloc_compressor(compression); |
| if (compressor == nullptr) { |
| return E_OOM; |
| } |
| } |
| return E_OK; |
| } |
| |
| int AlignedChunkReader::get_next_page(TsBlock* ret_tsblock, |
| Filter* oneshoot_filter, PageArena& pa) { |
| if (multi_value_mode_) { |
| return get_next_page_multi(ret_tsblock, oneshoot_filter, pa); |
| } |
| int ret = E_OK; |
| Filter* filter = |
| (oneshoot_filter != nullptr ? oneshoot_filter : time_filter_); |
| bool pt = prev_time_page_not_finish(); |
| bool pv = prev_value_page_not_finish(); |
| if (pt && pv) { |
| ret = decode_time_value_buf_into_tsblock(ret_tsblock, filter, &pa); |
| return ret; |
| } |
| if (!pt && !pv) { |
| while (IS_SUCC(ret)) { |
| if (RET_FAIL(get_cur_page_header( |
| time_chunk_meta_, time_in_stream_, cur_time_page_header_, |
| time_chunk_visit_offset_, time_chunk_header_))) { |
| } else if (RET_FAIL(get_cur_page_header( |
| value_chunk_meta_, value_in_stream_, |
| cur_value_page_header_, value_chunk_visit_offset_, |
| value_chunk_header_))) { |
| } else if (cur_page_statisify_filter(filter)) { |
| break; |
| } else if (RET_FAIL(skip_cur_page())) { |
| } |
| if (!has_more_data()) { |
| ret = E_NO_MORE_DATA; |
| break; |
| } |
| } |
| if (IS_SUCC(ret)) { |
| ret = decode_cur_time_page_data() || decode_cur_value_page_data(); |
| } |
| } |
| if (IS_SUCC(ret)) { |
| ret = decode_time_value_buf_into_tsblock(ret_tsblock, filter, &pa); |
| } |
| return ret; |
| } |
| |
| int AlignedChunkReader::get_cur_page_header(ChunkMeta*& chunk_meta, |
| common::ByteStream& in_stream, |
| PageHeader& cur_page_header, |
| uint32_t& chunk_visit_offset, |
| ChunkHeader& chunk_header, |
| int32_t* override_buf_size) { |
| int ret = E_OK; |
| bool retry = true; |
| int cur_page_header_serialized_size = 0; |
| // TODO: configurable |
| int retry_read_want_size = 1024; |
| if (chunk_visit_offset - chunk_header.serialized_size_ >= |
| chunk_header.data_size_) { |
| cur_page_header.reset(); |
| return E_OK; |
| } |
| |
| do { |
| in_stream.mark_read_pos(); |
| cur_page_header.reset(); |
| ret = cur_page_header.deserialize_from( |
| in_stream, !chunk_has_only_one_page(chunk_header), |
| chunk_header.data_type_); |
| cur_page_header_serialized_size = in_stream.get_mark_len(); |
| if (deserialize_buf_not_enough(ret) && retry) { |
| retry = false; |
| retry_read_want_size += 1024; |
| int32_t& file_data_buf_size = |
| override_buf_size != nullptr ? *override_buf_size |
| : chunk_header.data_type_ == common::VECTOR |
| ? file_data_time_buf_size_ |
| : file_data_value_buf_size_; |
| // do not shrink buffer for page header, otherwise, the buffer is |
| // most likely to grow back when reading page data |
| if (E_OK == read_from_file_and_rewrap( |
| in_stream, chunk_meta, chunk_visit_offset, |
| file_data_buf_size, retry_read_want_size, false)) { |
| continue; |
| } |
| } |
| break; |
| } while (true); |
| if (IS_SUCC(ret)) { |
| // visit a header |
| chunk_visit_offset += cur_page_header_serialized_size; |
| } |
| #if DEBUG_SE |
| std::cout << "get_cur_page_header, ret=" << ret << ", retry=" << retry |
| << ", cur_page_header=" << cur_page_header |
| << ", chunk_meta->offset_of_chunk_header_=" |
| << chunk_meta->offset_of_chunk_header_ |
| << ", cur_page_header_serialized_size=" |
| << cur_page_header_serialized_size << std::endl; |
| #endif |
| return ret; |
| } |
| |
| // reader at least @want_size bytes from file and wrap the buffer into |
| // @in_stream_ |
| int AlignedChunkReader::read_from_file_and_rewrap( |
| common::ByteStream& in_stream_, ChunkMeta*& chunk_meta, |
| uint32_t& chunk_visit_offset, int32_t& file_data_buf_size, int want_size, |
| bool may_shrink) { |
| int ret = E_OK; |
| const int DEFAULT_READ_SIZE = 4096; // may use page_size + page_header_size |
| char* file_data_buf = in_stream_.get_wrapped_buf(); |
| int64_t offset = chunk_meta->offset_of_chunk_header_ + chunk_visit_offset; |
| int read_size = |
| (want_size < DEFAULT_READ_SIZE ? DEFAULT_READ_SIZE : want_size); |
| if (file_data_buf_size < read_size || |
| (may_shrink && read_size < file_data_buf_size / 10)) { |
| file_data_buf = (char*)mem_realloc(file_data_buf, read_size); |
| if (IS_NULL(file_data_buf)) { |
| in_stream_.clear_wrapped_buf(); |
| return E_OOM; |
| } |
| file_data_buf_size = read_size; |
| // Update stream pointer immediately so it stays valid even if |
| // the subsequent read fails and the caller frees via destroy(). |
| in_stream_.wrap_from(file_data_buf, read_size); |
| } |
| int ret_read_len = 0; |
| if (RET_FAIL( |
| read_file_->read(offset, file_data_buf, read_size, ret_read_len))) { |
| } else { |
| in_stream_.wrap_from(file_data_buf, ret_read_len); |
| #ifdef DEBUG_SE |
| std::cout << "file offset = " << offset << " len = " << ret_read_len |
| << std::endl; |
| DEBUG_hex_dump_buf("wrapped buf = ", file_data_buf, 256); |
| #endif |
| } |
| return ret; |
| } |
| |
| bool AlignedChunkReader::cur_page_statisify_filter(Filter* filter) { |
| bool value_satisfy = filter == nullptr || |
| cur_value_page_header_.statistic_ == nullptr || |
| filter->satisfy(cur_value_page_header_.statistic_); |
| bool time_satisfy = filter == nullptr || |
| cur_time_page_header_.statistic_ == nullptr || |
| filter->satisfy(cur_time_page_header_.statistic_); |
| return time_satisfy && value_satisfy; |
| } |
| |
| int AlignedChunkReader::skip_cur_page() { |
| int ret = E_OK; |
| // visit a page tv data |
| time_chunk_visit_offset_ += cur_time_page_header_.compressed_size_; |
| time_in_stream_.wrapped_buf_advance_read_pos( |
| cur_time_page_header_.compressed_size_); |
| value_chunk_visit_offset_ += cur_value_page_header_.compressed_size_; |
| value_in_stream_.wrapped_buf_advance_read_pos( |
| cur_value_page_header_.compressed_size_); |
| return ret; |
| } |
| |
| int AlignedChunkReader::decode_cur_time_page_data() { |
| int ret = E_OK; |
| |
| // Step 1: make sure we load the whole page data in @in_stream_ |
| if (time_in_stream_.remaining_size() < |
| cur_time_page_header_.compressed_size_) { |
| // std::cout << "decode_cur_page_data. in_stream_.remaining_size="<< |
| // in_stream_.remaining_size() << ", cur_page_header_.compressed_size_=" |
| // << cur_page_header_.compressed_size_ << std::endl; |
| if (RET_FAIL(read_from_file_and_rewrap( |
| time_in_stream_, time_chunk_meta_, time_chunk_visit_offset_, |
| file_data_time_buf_size_, |
| cur_time_page_header_.compressed_size_))) { |
| } |
| } |
| |
| char* time_compressed_buf = nullptr; |
| char* time_uncompressed_buf = nullptr; |
| uint32_t time_compressed_buf_size = 0; |
| uint32_t time_uncompressed_buf_size = 0; |
| |
| // Step 2: do uncompress |
| if (IS_SUCC(ret)) { |
| time_compressed_buf = |
| time_in_stream_.get_wrapped_buf() + time_in_stream_.read_pos(); |
| #ifdef DEBUG_SE |
| std::cout << "AlignedChunkReader::decode_cur_page_data,time_in_stream_." |
| "get_wrapped_buf=" |
| << (void*)(time_in_stream_.get_wrapped_buf()) |
| << ", time_in_stream_.read_pos=" << time_in_stream_.read_pos() |
| << std::endl; |
| #endif |
| time_compressed_buf_size = cur_time_page_header_.compressed_size_; |
| time_in_stream_.wrapped_buf_advance_read_pos(time_compressed_buf_size); |
| time_chunk_visit_offset_ += time_compressed_buf_size; |
| if (RET_FAIL(time_compressor_->reset(false))) { |
| } else if (RET_FAIL(time_compressor_->uncompress( |
| time_compressed_buf, time_compressed_buf_size, |
| time_uncompressed_buf, time_uncompressed_buf_size))) { |
| } else { |
| time_uncompressed_buf_ = time_uncompressed_buf; |
| } |
| #ifdef DEBUG_SE |
| DEBUG_hex_dump_buf( |
| "AlignedChunkReader reader, time_uncompressed buf = ", |
| time_uncompressed_buf, time_uncompressed_buf_size); |
| #endif |
| if (ret != E_OK || time_uncompressed_buf_size != |
| cur_time_page_header_.uncompressed_size_) { |
| ret = E_TSFILE_CORRUPTED; |
| ASSERT(false); |
| } |
| } |
| |
| time_decoder_->reset(); |
| #ifdef DEBUG_SE |
| DEBUG_hex_dump_buf("AlignedChunkReader reader, time_buf = ", time_buf, |
| time_buf_size); |
| #endif |
| time_in_.wrap_from(time_uncompressed_buf_, time_uncompressed_buf_size); |
| return ret; |
| } |
| |
| int AlignedChunkReader::decode_cur_value_page_data() { |
| int ret = E_OK; |
| |
| // Step 1: make sure we load the whole page data in @in_stream_ |
| if (value_in_stream_.remaining_size() < |
| cur_value_page_header_.compressed_size_) { |
| // std::cout << "decode_cur_page_data. in_stream_.remaining_size="<< |
| // in_stream_.remaining_size() << ", cur_page_header_.compressed_size_=" |
| // << cur_page_header_.compressed_size_ << std::endl; |
| if (RET_FAIL(read_from_file_and_rewrap( |
| value_in_stream_, value_chunk_meta_, value_chunk_visit_offset_, |
| file_data_value_buf_size_, |
| cur_value_page_header_.compressed_size_))) { |
| } |
| } |
| |
| char* value_compressed_buf = nullptr; |
| char* value_uncompressed_buf = nullptr; |
| uint32_t value_compressed_buf_size = 0; |
| uint32_t value_uncompressed_buf_size = 0; |
| char* value_buf = nullptr; |
| uint32_t value_buf_size = 0; |
| |
| if (cur_value_page_header_.compressed_size_ == 0) { |
| value_in_.wrap_from(value_buf, 0); |
| return E_OK; |
| } |
| |
| // Step 2: do uncompress |
| if (IS_SUCC(ret)) { |
| value_compressed_buf = |
| value_in_stream_.get_wrapped_buf() + value_in_stream_.read_pos(); |
| value_compressed_buf_size = cur_value_page_header_.compressed_size_; |
| value_in_stream_.wrapped_buf_advance_read_pos( |
| value_compressed_buf_size); |
| value_chunk_visit_offset_ += value_compressed_buf_size; |
| if (RET_FAIL(value_compressor_->reset(false))) { |
| } else if (RET_FAIL(value_compressor_->uncompress( |
| value_compressed_buf, value_compressed_buf_size, |
| value_uncompressed_buf, value_uncompressed_buf_size))) { |
| } else { |
| value_uncompressed_buf_ = value_uncompressed_buf; |
| } |
| #ifdef DEBUG_SE |
| DEBUG_hex_dump_buf( |
| "AlignedChunkReader reader, value_uncompressed buf = ", |
| value_uncompressed_buf, value_uncompressed_buf_size); |
| #endif |
| if (ret != E_OK || value_uncompressed_buf_size != |
| cur_value_page_header_.uncompressed_size_) { |
| ret = E_TSFILE_CORRUPTED; |
| ASSERT(false); |
| } |
| } |
| // Step 3: get value_buf |
| if (IS_SUCC(ret)) { |
| uint32_t value_uncompressed_buf_offset = 0; |
| value_page_data_num_ = |
| SerializationUtil::read_ui32(value_uncompressed_buf); |
| value_uncompressed_buf_offset += sizeof(uint32_t); |
| value_page_col_notnull_bitmap_.resize((value_page_data_num_ + 7) / 8); |
| for (unsigned char& i : value_page_col_notnull_bitmap_) { |
| i = *(value_uncompressed_buf + value_uncompressed_buf_offset); |
| value_uncompressed_buf_offset++; |
| } |
| cur_value_index = -1; |
| value_buf = value_uncompressed_buf + value_uncompressed_buf_offset; |
| value_buf_size = |
| value_uncompressed_buf_size - value_uncompressed_buf_offset; |
| } |
| value_decoder_->reset(); |
| #ifdef DEBUG_SE |
| DEBUG_hex_dump_buf("AlignedChunkReader reader, value_buf = ", value_buf, |
| value_buf_size); |
| #endif |
| value_in_.wrap_from(value_buf, value_buf_size); |
| return ret; |
| } |
| |
| int AlignedChunkReader::decode_time_value_buf_into_tsblock( |
| TsBlock*& ret_tsblock, Filter* filter, common::PageArena* pa) { |
| int ret = common::E_OK; |
| ret = decode_tv_buf_into_tsblock_by_datatype(time_in_, value_in_, |
| ret_tsblock, filter, pa); |
| // if we return during @decode_tv_buf_into_tsblock, we should keep |
| // @uncompressed_buf_ valid until all TV pairs are decoded. |
| if (ret != E_OVERFLOW) { |
| if (time_uncompressed_buf_ != nullptr) { |
| time_compressor_->after_uncompress(time_uncompressed_buf_); |
| time_uncompressed_buf_ = nullptr; |
| } |
| if (value_uncompressed_buf_ != nullptr) { |
| value_compressor_->after_uncompress(value_uncompressed_buf_); |
| value_uncompressed_buf_ = nullptr; |
| } |
| if (!prev_value_page_not_finish()) { |
| value_in_.reset(); |
| } |
| if (!prev_time_page_not_finish()) { |
| time_in_.reset(); |
| } |
| value_page_col_notnull_bitmap_.clear(); |
| value_page_col_notnull_bitmap_.shrink_to_fit(); |
| } else { |
| ret = E_OK; |
| } |
| return ret; |
| } |
| |
| #define DECODE_TYPED_TV_INTO_TSBLOCK(CppType, ReadType, time_in, value_in, \ |
| row_appender) \ |
| do { \ |
| uint32_t mask = 1 << 7; \ |
| int64_t time = 0; \ |
| CppType value; \ |
| while (time_decoder_->has_remaining(time_in)) { \ |
| cur_value_index++; \ |
| if (value_page_col_notnull_bitmap_.empty() || \ |
| ((value_page_col_notnull_bitmap_[cur_value_index / 8] & \ |
| 0xFF) & \ |
| (mask >> (cur_value_index % 8))) == 0) { \ |
| if (UNLIKELY(!row_appender.add_row())) { \ |
| ret = E_OVERFLOW; \ |
| cur_value_index--; \ |
| break; \ |
| } \ |
| ret = time_decoder_->read_int64(time, time_in); \ |
| if (ret != E_OK) { \ |
| break; \ |
| } \ |
| row_appender.append(0, (char*)&time, sizeof(time)); \ |
| row_appender.append_null(1); \ |
| continue; \ |
| } \ |
| assert(value_decoder_->has_remaining(value_in)); \ |
| if (!value_decoder_->has_remaining(value_in)) { \ |
| return common::E_DATA_INCONSISTENCY; \ |
| } \ |
| if (UNLIKELY(!row_appender.add_row())) { \ |
| ret = E_OVERFLOW; \ |
| cur_value_index--; \ |
| break; \ |
| } else if (RET_FAIL(time_decoder_->read_int64(time, time_in))) { \ |
| } else if (RET_FAIL(value_decoder_->read_##ReadType(value, \ |
| value_in))) { \ |
| } else if (filter != nullptr && !filter->satisfy(time, value)) { \ |
| row_appender.backoff_add_row(); \ |
| continue; \ |
| } else { \ |
| /*std::cout << "decoder: time=" << time << ", value=" << value \ |
| * << std::endl;*/ \ |
| row_appender.append(0, (char*)&time, sizeof(time)); \ |
| row_appender.append(1, (char*)&value, sizeof(value)); \ |
| } \ |
| } \ |
| } while (false) |
| |
| int AlignedChunkReader::i32_DECODE_TYPED_TV_INTO_TSBLOCK( |
| ByteStream& time_in, ByteStream& value_in, RowAppender& row_appender, |
| Filter* filter) { |
| int ret = E_OK; |
| uint32_t mask = 1 << 7; |
| int64_t time = 0; |
| int32_t value; |
| while (time_decoder_->has_remaining(time_in)) { |
| cur_value_index++; |
| if (value_page_col_notnull_bitmap_.empty() || |
| ((value_page_col_notnull_bitmap_[cur_value_index / 8] & 0xFF) & |
| (mask >> (cur_value_index % 8))) == 0) { |
| ret = time_decoder_->read_int64(time, time_in); |
| if (ret != E_OK) { |
| break; |
| } |
| if (UNLIKELY(!row_appender.add_row())) { |
| ret = E_OVERFLOW; |
| break; |
| } |
| row_appender.append(0, (char*)&time, sizeof(time)); |
| row_appender.append_null(1); |
| continue; |
| } |
| assert(value_decoder_->has_remaining(value_in)); |
| if (!value_decoder_->has_remaining(value_in)) { |
| return common::E_DATA_INCONSISTENCY; |
| } |
| if (UNLIKELY(!row_appender.add_row())) { |
| ret = E_OVERFLOW; |
| cur_value_index--; |
| break; |
| } else if (RET_FAIL(time_decoder_->read_int64(time, time_in))) { |
| } else if (RET_FAIL(value_decoder_->read_int32(value, value_in))) { |
| } else if (filter != nullptr && !filter->satisfy(time, value)) { |
| row_appender.backoff_add_row(); |
| continue; |
| } else { |
| /*std::cout << "decoder: time=" << time << ", value=" << value |
| * << std::endl;*/ |
| row_appender.append(0, (char*)&time, sizeof(time)); |
| row_appender.append(1, (char*)&value, sizeof(value)); |
| } |
| } |
| return ret; |
| } |
| |
| namespace { |
| // Type-dispatched value batch read / skip for decode_tv_batch<T>. Overload |
| // resolution on the value pointer type selects the matching Decoder method, so |
| // the four fixed-width value types share one decode loop. |
| FORCE_INLINE int read_value_batch_typed(Decoder* d, int32_t* out, int cap, |
| int& actual, ByteStream& in) { |
| return d->read_batch_int32(out, cap, actual, in); |
| } |
| FORCE_INLINE int read_value_batch_typed(Decoder* d, int64_t* out, int cap, |
| int& actual, ByteStream& in) { |
| return d->read_batch_int64(out, cap, actual, in); |
| } |
| FORCE_INLINE int read_value_batch_typed(Decoder* d, float* out, int cap, |
| int& actual, ByteStream& in) { |
| return d->read_batch_float(out, cap, actual, in); |
| } |
| FORCE_INLINE int read_value_batch_typed(Decoder* d, double* out, int cap, |
| int& actual, ByteStream& in) { |
| return d->read_batch_double(out, cap, actual, in); |
| } |
| FORCE_INLINE int skip_value_typed(Decoder* d, int32_t*, int n, int& skipped, |
| ByteStream& in) { |
| return d->skip_int32(n, skipped, in); |
| } |
| FORCE_INLINE int skip_value_typed(Decoder* d, int64_t*, int n, int& skipped, |
| ByteStream& in) { |
| return d->skip_int64(n, skipped, in); |
| } |
| FORCE_INLINE int skip_value_typed(Decoder* d, float*, int n, int& skipped, |
| ByteStream& in) { |
| return d->skip_float(n, skipped, in); |
| } |
| FORCE_INLINE int skip_value_typed(Decoder* d, double*, int n, int& skipped, |
| ByteStream& in) { |
| return d->skip_double(n, skipped, in); |
| } |
| } // namespace |
| |
| // Unified aligned time+value page decode for fixed-width value types |
| // (INT32/INT64/FLOAT/DOUBLE). These differ only in the value array type, the |
| // typed read/skip calls (dispatched via the helpers above), and whether the |
| // per-value Filter::satisfy (which takes an int64 value) is applied — only |
| // integral value columns use it; float/double are filtered on time only. |
| template <typename T> |
| int AlignedChunkReader::decode_tv_batch(ByteStream& time_in, |
| ByteStream& value_in, |
| RowAppender& row_appender, |
| Filter* filter) { |
| int ret = E_OK; |
| const int BATCH = 129; |
| int64_t times[BATCH]; |
| T values[BATCH]; |
| const uint32_t null_mask_base = 1 << 7; |
| |
| while (time_decoder_->has_remaining(time_in)) { |
| if (row_appender.remaining() < (uint32_t)BATCH) { |
| ret = E_OVERFLOW; |
| break; |
| } |
| |
| // Block-level time filter check: skip entire block if out of range. |
| bool block_all_pass = false; |
| if (filter != nullptr) { |
| int64_t block_min, block_max; |
| int block_count; |
| if (time_decoder_->peek_next_block_range_int64( |
| time_in, block_min, block_max, block_count)) { |
| if (!filter->satisfy_start_end_time(block_min, block_max)) { |
| int skipped = 0; |
| time_decoder_->skip_peeked_block_int64(time_in, skipped); |
| int nonnull = 0; |
| for (int i = 0; i < block_count; ++i) { |
| int vi = cur_value_index + 1 + i; |
| if (!value_page_col_notnull_bitmap_.empty() && |
| ((value_page_col_notnull_bitmap_[vi / 8] & 0xFF) & |
| (null_mask_base >> (vi % 8))) != 0) { |
| ++nonnull; |
| } |
| } |
| cur_value_index += block_count; |
| if (nonnull > 0) { |
| // skip_* may legitimately fail (truncated page) or |
| // short-read (corrupt bitmap vs. data); both must abort |
| // the loop rather than silently desync the value |
| // decoder. |
| int sk = 0; |
| if (RET_FAIL(skip_value_typed(value_decoder_, values, |
| nonnull, sk, value_in))) { |
| break; |
| } |
| if (sk != nonnull) { |
| ret = E_TSFILE_CORRUPTED; |
| break; |
| } |
| } |
| continue; |
| } |
| if (filter->contain_start_end_time(block_min, block_max)) { |
| block_all_pass = true; |
| } |
| } |
| } |
| |
| int time_count = 0; |
| if (RET_FAIL(time_decoder_->read_batch_int64(times, BATCH, time_count, |
| time_in))) { |
| break; |
| } |
| if (time_count == 0) break; |
| |
| bool is_null[BATCH]; |
| int nonnull_count = 0; |
| for (int i = 0; i < time_count; ++i) { |
| int vi = cur_value_index + 1 + i; |
| if (value_page_col_notnull_bitmap_.empty() || |
| ((value_page_col_notnull_bitmap_[vi / 8] & 0xFF) & |
| (null_mask_base >> (vi % 8))) == 0) { |
| is_null[i] = true; |
| } else { |
| is_null[i] = false; |
| ++nonnull_count; |
| } |
| } |
| |
| bool time_mask[BATCH]; |
| int pass_count = time_count; |
| if (filter != nullptr && !block_all_pass) { |
| pass_count = |
| filter->satisfy_batch_time(times, time_count, time_mask); |
| } |
| |
| if (pass_count == 0) { |
| if (nonnull_count > 0) { |
| int skipped = 0; |
| if (RET_FAIL(skip_value_typed(value_decoder_, values, |
| nonnull_count, skipped, |
| value_in))) { |
| break; |
| } |
| if (skipped != nonnull_count) { |
| ret = E_TSFILE_CORRUPTED; |
| break; |
| } |
| } |
| cur_value_index += time_count; |
| continue; |
| } |
| |
| int value_count = 0; |
| if (nonnull_count > 0) { |
| if (RET_FAIL(read_value_batch_typed(value_decoder_, values, |
| nonnull_count, value_count, |
| value_in))) { |
| break; |
| } |
| } |
| |
| int val_idx = 0; |
| for (int i = 0; i < time_count; ++i) { |
| cur_value_index++; |
| if (filter != nullptr && !block_all_pass && !time_mask[i]) { |
| if (!is_null[i]) ++val_idx; |
| continue; |
| } |
| if (is_null[i]) { |
| if (UNLIKELY(!row_appender.add_row())) { |
| ret = E_OVERFLOW; |
| break; |
| } |
| row_appender.append(0, (char*)×[i], sizeof(int64_t)); |
| row_appender.append_null(1); |
| } else { |
| T val = values[val_idx++]; |
| // Per-value filter applies only to integral value columns; |
| // Filter::satisfy takes an int64 value. is_integral<T> is a |
| // compile-time constant, so this branch is elided (and the |
| // int64 cast never evaluated) for float/double. |
| if (std::is_integral<T>::value && filter != nullptr && |
| !block_all_pass && |
| !filter->satisfy(times[i], static_cast<int64_t>(val))) { |
| continue; |
| } |
| if (UNLIKELY(!row_appender.add_row())) { |
| ret = E_OVERFLOW; |
| break; |
| } |
| row_appender.append(0, (char*)×[i], sizeof(int64_t)); |
| row_appender.append(1, (char*)&val, sizeof(T)); |
| } |
| } |
| if (ret != E_OK) break; |
| } |
| return ret; |
| } |
| |
| int AlignedChunkReader::decode_tv_buf_into_tsblock_by_datatype( |
| ByteStream& time_in, ByteStream& value_in, TsBlock* ret_tsblock, |
| Filter* filter, common::PageArena* pa) { |
| int ret = E_OK; |
| RowAppender row_appender(ret_tsblock); |
| switch (value_chunk_header_.data_type_) { |
| case common::BOOLEAN: |
| DECODE_TYPED_TV_INTO_TSBLOCK(bool, boolean, time_in_, value_in_, |
| row_appender); |
| break; |
| case common::DATE: |
| case common::INT32: |
| // Batch decode path: read_batch_int{32,64} consumes whole TS_2DIFF |
| // blocks at once (and uses SIMD when ENABLE_SIMD); replaces a |
| // per-value decode() loop that hot-dominated the read flame graph. |
| ret = decode_tv_batch<int32_t>(time_in_, value_in_, row_appender, |
| filter); |
| break; |
| case common::TIMESTAMP: |
| case common::INT64: |
| ret = decode_tv_batch<int64_t>(time_in_, value_in_, row_appender, |
| filter); |
| break; |
| case common::FLOAT: |
| ret = decode_tv_batch<float>(time_in_, value_in_, row_appender, |
| filter); |
| break; |
| case common::DOUBLE: |
| ret = decode_tv_batch<double>(time_in_, value_in_, row_appender, |
| filter); |
| break; |
| case common::STRING: |
| case common::BLOB: |
| case common::TEXT: |
| ret = STRING_DECODE_TYPED_TV_INTO_TSBLOCK( |
| time_in, value_in, row_appender, *pa, filter); |
| break; |
| default: |
| ret = E_NOT_SUPPORT; |
| ASSERT(false); |
| } |
| if (ret_tsblock->get_row_count() == 0 && ret == E_OK) { |
| ret = E_NO_MORE_DATA; |
| } |
| return ret; |
| } |
| |
| int AlignedChunkReader::STRING_DECODE_TYPED_TV_INTO_TSBLOCK( |
| ByteStream& time_in, ByteStream& value_in, RowAppender& row_appender, |
| PageArena& pa, Filter* filter) { |
| int ret = E_OK; |
| int64_t time = 0; |
| common::String value; |
| uint32_t mask = 1 << 7; |
| while (time_decoder_->has_remaining(time_in)) { |
| cur_value_index++; |
| bool should_read_data = true; |
| if (value_page_col_notnull_bitmap_.empty() || |
| ((value_page_col_notnull_bitmap_[cur_value_index / 8] & 0xFF) & |
| (mask >> (cur_value_index % 8))) == 0) { |
| should_read_data = false; |
| } |
| |
| if (should_read_data) { |
| assert(value_decoder_->has_remaining(value_in)); |
| if (!value_decoder_->has_remaining(value_in)) { |
| return E_DATA_INCONSISTENCY; |
| } |
| } |
| |
| if (UNLIKELY(!row_appender.add_row())) { |
| ret = E_OVERFLOW; |
| cur_value_index--; |
| break; |
| } else if (RET_FAIL(time_decoder_->read_int64(time, time_in))) { |
| } else if (should_read_data && |
| RET_FAIL(value_decoder_->read_String(value, pa, value_in))) { |
| } else if (filter != nullptr && !filter->satisfy(time, value)) { |
| row_appender.backoff_add_row(); |
| continue; |
| } else { |
| row_appender.append(0, (char*)&time, sizeof(time)); |
| if (!should_read_data) { |
| row_appender.append_null(1); |
| } else { |
| row_appender.append(1, value.buf_, value.len_); |
| } |
| } |
| } |
| return ret; |
| } |
| |
| bool AlignedChunkReader::should_skip_page_by_time(int64_t min_time_hint) { |
| if (min_time_hint == std::numeric_limits<int64_t>::min()) { |
| return false; |
| } |
| // Use time page statistic for time-based skipping. |
| if (cur_time_page_header_.statistic_ != nullptr) { |
| return cur_time_page_header_.statistic_->end_time_ < min_time_hint; |
| } |
| if (cur_value_page_header_.statistic_ != nullptr) { |
| return cur_value_page_header_.statistic_->end_time_ < min_time_hint; |
| } |
| return false; |
| } |
| |
| bool AlignedChunkReader::should_skip_page_by_offset(int& row_offset) { |
| if (row_offset <= 0) { |
| return false; |
| } |
| // Use time page statistic for count. |
| Statistic* stat = cur_time_page_header_.statistic_; |
| if (stat == nullptr) { |
| stat = cur_value_page_header_.statistic_; |
| } |
| if (stat == nullptr || stat->count_ == 0) { |
| return false; |
| } |
| int32_t count = stat->count_; |
| if (row_offset >= count) { |
| row_offset -= count; |
| return true; |
| } |
| return false; |
| } |
| |
| int AlignedChunkReader::get_next_page(TsBlock* ret_tsblock, |
| Filter* oneshoot_filter, PageArena& pa, |
| int64_t min_time_hint, int& row_offset, |
| int& row_limit) { |
| if (multi_value_mode_) { |
| // Multi-value aligned path doesn't yet honour row_offset / row_limit |
| // / min_time_hint — they get dropped on the floor, which silently |
| // returns full chunk data when the caller asked for a sub-range. |
| // Refuse the combination so the caller sees an actual error instead |
| // of garbage results. set_row_range(0, -1) keeps the all-rows |
| // contract intact for normal queries. |
| if (row_offset > 0 || row_limit >= 0 || |
| min_time_hint != std::numeric_limits<int64_t>::min()) { |
| return common::E_NOT_SUPPORT; |
| } |
| return get_next_page_multi(ret_tsblock, oneshoot_filter, pa); |
| } |
| int ret = E_OK; |
| Filter* filter = |
| (oneshoot_filter != nullptr ? oneshoot_filter : time_filter_); |
| |
| if (row_limit == 0) { |
| return E_NO_MORE_DATA; |
| } |
| |
| bool pt = prev_time_page_not_finish(); |
| bool pv = prev_value_page_not_finish(); |
| |
| if (pt && pv) { |
| ret = decode_time_value_buf_into_tsblock(ret_tsblock, filter, &pa); |
| return ret; |
| } |
| if (!pt && !pv) { |
| while (IS_SUCC(ret)) { |
| if (RET_FAIL(get_cur_page_header( |
| time_chunk_meta_, time_in_stream_, cur_time_page_header_, |
| time_chunk_visit_offset_, time_chunk_header_))) { |
| } else if (RET_FAIL(get_cur_page_header( |
| value_chunk_meta_, value_in_stream_, |
| cur_value_page_header_, value_chunk_visit_offset_, |
| value_chunk_header_))) { |
| } else if (!cur_page_statisify_filter(filter)) { |
| if (RET_FAIL(skip_cur_page())) { |
| } |
| } else if (should_skip_page_by_time(min_time_hint)) { |
| if (RET_FAIL(skip_cur_page())) { |
| } |
| } else if (should_skip_page_by_offset(row_offset)) { |
| if (RET_FAIL(skip_cur_page())) { |
| } |
| } else { |
| break; |
| } |
| if (!has_more_data()) { |
| ret = E_NO_MORE_DATA; |
| break; |
| } |
| } |
| if (IS_SUCC(ret)) { |
| ret = decode_cur_time_page_data() || decode_cur_value_page_data(); |
| } |
| } |
| if (IS_SUCC(ret)) { |
| ret = decode_time_value_buf_into_tsblock(ret_tsblock, filter, &pa); |
| } |
| return ret; |
| } |
| |
| // ══════════════════════════════════════════════════════════════════════════ |
| // Multi-value AlignedChunkReader implementation |
| // ══════════════════════════════════════════════════════════════════════════ |
| |
| int AlignedChunkReader::load_by_aligned_meta_multi( |
| ChunkMeta* time_chunk_meta, const std::vector<ChunkMeta*>& value_metas) { |
| int ret = E_OK; |
| multi_value_mode_ = true; |
| time_chunk_meta_ = time_chunk_meta; |
| page_plan_built_ = false; |
| current_page_loaded_ = false; |
| current_page_plan_index_ = 0; |
| time_predecoded_ = false; |
| page_all_times_.clear(); |
| page_time_count_ = 0; |
| page_time_cursor_ = 0; |
| |
| // ── Load time chunk header ── |
| file_data_time_buf_size_ = 1024; |
| int32_t ret_read_len = 0; |
| char* time_file_data_buf = |
| (char*)mem_alloc(file_data_time_buf_size_, MOD_CHUNK_READER); |
| if (IS_NULL(time_file_data_buf)) return E_OOM; |
| |
| ret = read_file_->read(time_chunk_meta_->offset_of_chunk_header_, |
| time_file_data_buf, file_data_time_buf_size_, |
| ret_read_len); |
| if (IS_SUCC(ret) && ret_read_len < ChunkHeader::MIN_SERIALIZED_SIZE) { |
| ret = E_TSFILE_CORRUPTED; |
| mem_free(time_file_data_buf); |
| return ret; |
| } |
| if (IS_SUCC(ret)) { |
| time_in_stream_.wrap_from(time_file_data_buf, ret_read_len); |
| if (RET_FAIL(time_chunk_header_.deserialize_from(time_in_stream_))) { |
| return ret; |
| } |
| time_chunk_visit_offset_ = time_in_stream_.read_pos(); |
| } |
| |
| // Alloc time decoder/compressor |
| if (IS_SUCC(ret)) { |
| if (RET_FAIL(alloc_compressor_and_decoder( |
| time_decoder_, time_compressor_, |
| time_chunk_header_.encoding_type_, |
| time_chunk_header_.data_type_, |
| time_chunk_header_.compression_type_))) { |
| return ret; |
| } |
| } |
| |
| // ── Load each value column ── |
| // Reuse existing ValueColumnState objects if count matches (reset() already |
| // cleared their internal state). Otherwise, recreate. |
| if (value_columns_.size() != value_metas.size()) { |
| for (auto* p : value_columns_) delete p; |
| value_columns_.clear(); |
| value_columns_.reserve(value_metas.size()); |
| for (size_t c = 0; c < value_metas.size(); c++) { |
| value_columns_.push_back(new ValueColumnState); |
| } |
| } |
| for (size_t c = 0; c < value_metas.size() && IS_SUCC(ret); c++) { |
| auto* col = value_columns_[c]; |
| col->chunk_meta = value_metas[c]; |
| col->file_data_buf_size = 1024; |
| ret_read_len = 0; |
| char* vbuf = |
| (char*)mem_alloc(col->file_data_buf_size, MOD_CHUNK_READER); |
| if (IS_NULL(vbuf)) return E_OOM; |
| |
| ret = read_file_->read(col->chunk_meta->offset_of_chunk_header_, vbuf, |
| col->file_data_buf_size, ret_read_len); |
| if (IS_SUCC(ret) && ret_read_len < ChunkHeader::MIN_SERIALIZED_SIZE) { |
| ret = E_TSFILE_CORRUPTED; |
| mem_free(vbuf); |
| break; |
| } |
| if (IS_SUCC(ret)) { |
| col->in_stream.wrap_from(vbuf, ret_read_len); |
| if (RET_FAIL(col->chunk_header.deserialize_from(col->in_stream))) { |
| break; |
| } |
| col->chunk_visit_offset = col->in_stream.read_pos(); |
| if (RET_FAIL(alloc_compressor_and_decoder( |
| col->decoder, col->compressor, |
| col->chunk_header.encoding_type_, |
| col->chunk_header.data_type_, |
| col->chunk_header.compression_type_))) { |
| break; |
| } |
| } |
| } |
| |
| return ret; |
| } |
| |
| bool AlignedChunkReader::has_more_data_multi() const { |
| if (page_plan_built_) { |
| if (current_page_loaded_) { |
| return page_time_cursor_ < page_time_count_; |
| } |
| return current_page_plan_index_ < chunk_pages_.size(); |
| } |
| if (prev_time_page_not_finish() || prev_any_value_page_not_finish_multi()) { |
| return true; |
| } |
| if (time_chunk_visit_offset_ - time_chunk_header_.serialized_size_ < |
| time_chunk_header_.data_size_) { |
| return true; |
| } |
| for (const auto* col : value_columns_) { |
| if (col->chunk_visit_offset - col->chunk_header.serialized_size_ < |
| col->chunk_header.data_size_) { |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| bool AlignedChunkReader::prev_any_value_page_not_finish_multi() const { |
| for (const auto* col : value_columns_) { |
| if ((col->decoder && col->decoder->has_remaining(col->in)) || |
| col->in.has_remaining()) { |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| bool AlignedChunkReader::has_variable_length_value_column() const { |
| for (const auto* col : value_columns_) { |
| if (col->chunk_header.data_type_ == common::STRING || |
| col->chunk_header.data_type_ == common::TEXT || |
| col->chunk_header.data_type_ == common::BLOB) { |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| int AlignedChunkReader::count_non_null_prefix( |
| const std::vector<uint8_t>& bitmap, int32_t row_limit) const { |
| if (row_limit <= 0 || bitmap.empty()) { |
| return 0; |
| } |
| const uint32_t mask_base = 1 << 7; |
| int count = 0; |
| for (int32_t i = 0; i < row_limit; i++) { |
| if (((bitmap[i / 8] & 0xFF) & (mask_base >> (i % 8))) != 0) { |
| count++; |
| } |
| } |
| return count; |
| } |
| |
| int AlignedChunkReader::decode_time_page_direct( |
| const ChunkPageInfo& page_info, std::vector<int64_t>& out_times) { |
| return decode_time_page_with(page_info, out_times, time_decoder_, |
| time_compressor_); |
| } |
| |
| // Worker-safe variant: uses caller-provided decoder + compressor instead of |
| // the shared time_decoder_/time_compressor_ members. Used by the parallel |
| // time-page decode dispatch in decode_all_planned_pages. |
| int AlignedChunkReader::decode_time_page_with(const ChunkPageInfo& page_info, |
| std::vector<int64_t>& out_times, |
| Decoder* decoder, |
| Compressor* compressor) { |
| out_times.clear(); |
| if (page_info.time_compressed_size == 0) { |
| return E_OK; |
| } |
| |
| char stack_buf[4096]; |
| char* compressed_buf = stack_buf; |
| bool heap = page_info.time_compressed_size > sizeof(stack_buf); |
| if (heap) { |
| compressed_buf = static_cast<char*>(common::mem_alloc( |
| page_info.time_compressed_size, common::MOD_DEFAULT)); |
| if (compressed_buf == nullptr) { |
| return E_OOM; |
| } |
| } |
| |
| int32_t read_len = 0; |
| int ret = read_file_->read(page_info.time_file_offset, compressed_buf, |
| page_info.time_compressed_size, read_len); |
| if (IS_FAIL(ret)) { |
| if (heap) common::mem_free(compressed_buf); |
| return ret; |
| } |
| // ReadFile::read() returns E_OK + short read_len on EOF; uncompressing |
| // page_info.time_compressed_size from a buffer with uninitialised tail |
| // bytes would feed garbage to the decompressor. |
| if (read_len != static_cast<int32_t>(page_info.time_compressed_size)) { |
| if (heap) common::mem_free(compressed_buf); |
| return E_TSFILE_CORRUPTED; |
| } |
| |
| char* uncompressed_buf = nullptr; |
| uint32_t uncompressed_size = 0; |
| if (RET_FAIL(compressor->reset(false))) { |
| if (heap) common::mem_free(compressed_buf); |
| return ret; |
| } |
| ret = compressor->uncompress(compressed_buf, page_info.time_compressed_size, |
| uncompressed_buf, uncompressed_size); |
| if (heap && compressed_buf != uncompressed_buf) { |
| common::mem_free(compressed_buf); |
| } |
| if (IS_FAIL(ret) || uncompressed_size != page_info.time_uncompressed_size) { |
| if (uncompressed_buf != nullptr) { |
| compressor->after_uncompress(uncompressed_buf); |
| } |
| return E_TSFILE_CORRUPTED; |
| } |
| |
| common::ByteStream in; |
| in.wrap_from(uncompressed_buf, uncompressed_size); |
| decoder->reset(); |
| const int batch_size = 1024; |
| int64_t batch[batch_size]; |
| while (decoder->has_remaining(in)) { |
| int actual = 0; |
| if (RET_FAIL( |
| decoder->read_batch_int64(batch, batch_size, actual, in))) { |
| break; |
| } |
| if (actual == 0) { |
| break; |
| } |
| out_times.insert(out_times.end(), batch, batch + actual); |
| } |
| compressor->after_uncompress(uncompressed_buf); |
| return ret; |
| } |
| |
| int AlignedChunkReader::build_page_plan(Filter* filter) { |
| int ret = E_OK; |
| chunk_pages_.clear(); |
| current_page_plan_index_ = 0; |
| current_page_loaded_ = false; |
| page_plan_built_ = false; |
| |
| const uint32_t num_cols = value_columns_.size(); |
| while (IS_SUCC(ret)) { |
| if (time_chunk_visit_offset_ - time_chunk_header_.serialized_size_ >= |
| time_chunk_header_.data_size_) { |
| break; |
| } |
| |
| if (RET_FAIL(get_cur_page_header( |
| time_chunk_meta_, time_in_stream_, cur_time_page_header_, |
| time_chunk_visit_offset_, time_chunk_header_))) { |
| break; |
| } |
| if (cur_time_page_header_.compressed_size_ == 0 && |
| cur_time_page_header_.uncompressed_size_ == 0) { |
| break; |
| } |
| |
| ChunkPageInfo page_info; |
| page_info.time_file_offset = time_chunk_meta_->offset_of_chunk_header_ + |
| time_chunk_visit_offset_; |
| page_info.time_compressed_size = cur_time_page_header_.compressed_size_; |
| page_info.time_uncompressed_size = |
| cur_time_page_header_.uncompressed_size_; |
| page_info.value_file_offsets.resize(num_cols); |
| page_info.value_compressed_sizes.resize(num_cols); |
| page_info.value_uncompressed_sizes.resize(num_cols); |
| |
| for (uint32_t c = 0; c < num_cols && IS_SUCC(ret); c++) { |
| auto* col = value_columns_[c]; |
| if (RET_FAIL(get_cur_page_header( |
| col->chunk_meta, col->in_stream, col->cur_page_header, |
| col->chunk_visit_offset, col->chunk_header, |
| &col->file_data_buf_size))) { |
| break; |
| } |
| page_info.value_file_offsets[c] = |
| col->chunk_meta->offset_of_chunk_header_ + |
| col->chunk_visit_offset; |
| page_info.value_compressed_sizes[c] = |
| col->cur_page_header.compressed_size_; |
| page_info.value_uncompressed_sizes[c] = |
| col->cur_page_header.uncompressed_size_; |
| } |
| if (IS_FAIL(ret)) { |
| break; |
| } |
| |
| Statistic* stat = cur_time_page_header_.statistic_; |
| if (filter == nullptr) { |
| page_info.pass_type = PagePassType::FULL_PASS; |
| page_info.row_begin = 0; |
| page_info.row_end = stat != nullptr ? stat->count_ : 0; |
| } else if (stat != nullptr && !filter->satisfy(stat)) { |
| page_info.pass_type = PagePassType::SKIP; |
| } else if (stat != nullptr && filter->contain_start_end_time( |
| stat->start_time_, stat->end_time_)) { |
| page_info.pass_type = PagePassType::FULL_PASS; |
| page_info.row_begin = 0; |
| page_info.row_end = stat->count_; |
| } else { |
| page_info.pass_type = PagePassType::BOUNDARY; |
| std::vector<int64_t> times; |
| if (RET_FAIL(decode_time_page_direct(page_info, times))) { |
| break; |
| } |
| int32_t first = -1; |
| int32_t last = -1; |
| for (int32_t i = 0; i < static_cast<int32_t>(times.size()); i++) { |
| if (filter->satisfy_start_end_time(times[i], times[i])) { |
| if (first < 0) first = i; |
| last = i; |
| } |
| } |
| if (first >= 0) { |
| page_info.row_begin = first; |
| page_info.row_end = last + 1; |
| } else { |
| page_info.pass_type = PagePassType::SKIP; |
| } |
| } |
| |
| if (page_info.pass_type != PagePassType::SKIP) { |
| if (page_info.row_end == 0) { |
| std::vector<int64_t> times; |
| if (RET_FAIL(decode_time_page_direct(page_info, times))) { |
| break; |
| } |
| page_info.row_end = static_cast<int32_t>(times.size()); |
| } |
| if (page_info.row_begin < page_info.row_end) { |
| chunk_pages_.push_back(std::move(page_info)); |
| } |
| } |
| |
| time_chunk_visit_offset_ += cur_time_page_header_.compressed_size_; |
| time_in_stream_.wrapped_buf_advance_read_pos( |
| cur_time_page_header_.compressed_size_); |
| for (uint32_t c = 0; c < num_cols; c++) { |
| auto* col = value_columns_[c]; |
| col->chunk_visit_offset += col->cur_page_header.compressed_size_; |
| col->in_stream.wrapped_buf_advance_read_pos( |
| col->cur_page_header.compressed_size_); |
| } |
| } |
| |
| page_plan_built_ = IS_SUCC(ret); |
| |
| if (page_plan_built_) { |
| per_page_times_.assign(chunk_pages_.size(), std::vector<int64_t>{}); |
| for (auto* col : value_columns_) { |
| col->per_page_state.clear(); |
| col->per_page_state.resize(chunk_pages_.size()); |
| } |
| } |
| return ret; |
| } |
| |
| void AlignedChunkReader::release_current_page_state() { |
| time_predecoded_ = false; |
| page_all_times_.clear(); |
| page_time_count_ = 0; |
| page_time_cursor_ = 0; |
| for (auto* col : value_columns_) { |
| if (col->uncompressed_buf != nullptr && col->compressor != nullptr) { |
| col->compressor->after_uncompress(col->uncompressed_buf); |
| col->uncompressed_buf = nullptr; |
| } |
| col->notnull_bitmap.clear(); |
| col->cur_value_index = -1; |
| col->in.reset(); |
| for (auto& pps : col->per_page_state) { |
| pps.predecode_pa.destroy(); |
| } |
| col->per_page_state.clear(); |
| col->pending_decoded_values.clear(); |
| col->pending_decoded_count = 0; |
| col->pending_decoded_cursor = 0; |
| col->pending_decoded = false; |
| } |
| per_page_times_.clear(); |
| current_page_loaded_ = false; |
| } |
| |
| int AlignedChunkReader::decode_value_page_for_slot(uint32_t col_idx, |
| size_t page_idx) { |
| const ChunkPageInfo& page_info = chunk_pages_[page_idx]; |
| auto* col = value_columns_[col_idx]; |
| auto& pps = col->per_page_state[page_idx]; |
| |
| pps.notnull_bitmap.clear(); |
| pps.predecoded_values.clear(); |
| pps.predecoded_strings.clear(); |
| pps.predecoded_read_pos = 0; |
| pps.predecoded_count = 0; |
| pps.predecode_pa.destroy(); |
| |
| if (page_info.value_compressed_sizes[col_idx] == 0) { |
| return E_OK; |
| } |
| |
| char stack_buf[4096]; |
| char* compressed_buf = stack_buf; |
| bool heap = page_info.value_compressed_sizes[col_idx] > sizeof(stack_buf); |
| if (heap) { |
| compressed_buf = static_cast<char*>(common::mem_alloc( |
| page_info.value_compressed_sizes[col_idx], common::MOD_DEFAULT)); |
| if (compressed_buf == nullptr) return E_OOM; |
| } |
| |
| int32_t read_len = 0; |
| int ret = |
| read_file_->read(page_info.value_file_offsets[col_idx], compressed_buf, |
| page_info.value_compressed_sizes[col_idx], read_len); |
| if (IS_FAIL(ret)) { |
| if (heap) common::mem_free(compressed_buf); |
| return ret; |
| } |
| if (read_len != |
| static_cast<int32_t>(page_info.value_compressed_sizes[col_idx])) { |
| if (heap) common::mem_free(compressed_buf); |
| return E_TSFILE_CORRUPTED; |
| } |
| |
| char* uncompressed_buf = nullptr; |
| uint32_t uncompressed_size = 0; |
| if (RET_FAIL(col->compressor->reset(false))) { |
| if (heap) common::mem_free(compressed_buf); |
| return ret; |
| } |
| ret = col->compressor->uncompress(compressed_buf, |
| page_info.value_compressed_sizes[col_idx], |
| uncompressed_buf, uncompressed_size); |
| if (heap && compressed_buf != uncompressed_buf) { |
| common::mem_free(compressed_buf); |
| } |
| if (IS_FAIL(ret) || |
| uncompressed_size != page_info.value_uncompressed_sizes[col_idx]) { |
| if (uncompressed_buf != nullptr) { |
| col->compressor->after_uncompress(uncompressed_buf); |
| } |
| return E_TSFILE_CORRUPTED; |
| } |
| // The value page begins with a uint32 data_num followed by a bitmap of |
| // ceil(data_num/8) bytes; a corrupt or truncated page that doesn't even |
| // hold the data_num header would let read_ui32() walk past the buffer. |
| if (uncompressed_size < sizeof(uint32_t)) { |
| col->compressor->after_uncompress(uncompressed_buf); |
| return E_TSFILE_CORRUPTED; |
| } |
| |
| uint32_t offset = 0; |
| uint32_t data_num = SerializationUtil::read_ui32(uncompressed_buf); |
| offset += sizeof(uint32_t); |
| uint32_t bitmap_bytes = (data_num + 7) / 8; |
| if (uncompressed_size - offset < bitmap_bytes) { |
| col->compressor->after_uncompress(uncompressed_buf); |
| return E_TSFILE_CORRUPTED; |
| } |
| pps.notnull_bitmap.resize(bitmap_bytes); |
| memcpy(pps.notnull_bitmap.data(), uncompressed_buf + offset, bitmap_bytes); |
| offset += bitmap_bytes; |
| |
| char* value_buf = uncompressed_buf + offset; |
| uint32_t value_buf_size = uncompressed_size - offset; |
| common::ByteStream in; |
| in.wrap_from(value_buf, value_buf_size); |
| col->decoder->reset(); |
| |
| auto dt = col->chunk_header.data_type_; |
| int nonnull_total = count_non_null_prefix(pps.notnull_bitmap, |
| static_cast<int32_t>(data_num)); |
| int prefix_nonnull = |
| count_non_null_prefix(pps.notnull_bitmap, page_info.row_begin); |
| pps.predecoded_read_pos = prefix_nonnull; |
| |
| auto cleanup = [&]() { |
| col->compressor->after_uncompress(uncompressed_buf); |
| }; |
| |
| if (dt == common::STRING || dt == common::TEXT || dt == common::BLOB) { |
| pps.predecode_pa.init(512, common::MOD_TSFILE_READER); |
| pps.predecoded_strings.resize(nonnull_total); |
| for (int i = 0; i < nonnull_total; i++) { |
| if (RET_FAIL(col->decoder->read_String(pps.predecoded_strings[i], |
| pps.predecode_pa, in))) { |
| cleanup(); |
| return ret; |
| } |
| } |
| pps.predecoded_count = nonnull_total; |
| cleanup(); |
| return E_OK; |
| } |
| |
| if (nonnull_total == 0) { |
| cleanup(); |
| return E_OK; |
| } |
| |
| uint32_t elem_size = common::get_data_type_size(dt); |
| pps.predecoded_values.resize(static_cast<size_t>(nonnull_total) * |
| elem_size); |
| int actual = 0; |
| switch (dt) { |
| case common::BOOLEAN: { |
| bool* out = reinterpret_cast<bool*>(pps.predecoded_values.data()); |
| for (int i = 0; i < nonnull_total; i++) { |
| if (RET_FAIL(col->decoder->read_boolean(out[i], in))) { |
| cleanup(); |
| return ret; |
| } |
| } |
| actual = nonnull_total; |
| break; |
| } |
| case common::INT32: |
| case common::DATE: |
| if (RET_FAIL(col->decoder->read_batch_int32( |
| reinterpret_cast<int32_t*>(pps.predecoded_values.data()), |
| nonnull_total, actual, in))) { |
| cleanup(); |
| return ret; |
| } |
| break; |
| case common::INT64: |
| case common::TIMESTAMP: |
| if (RET_FAIL(col->decoder->read_batch_int64( |
| reinterpret_cast<int64_t*>(pps.predecoded_values.data()), |
| nonnull_total, actual, in))) { |
| cleanup(); |
| return ret; |
| } |
| break; |
| case common::FLOAT: |
| if (RET_FAIL(col->decoder->read_batch_float( |
| reinterpret_cast<float*>(pps.predecoded_values.data()), |
| nonnull_total, actual, in))) { |
| cleanup(); |
| return ret; |
| } |
| break; |
| case common::DOUBLE: |
| if (RET_FAIL(col->decoder->read_batch_double( |
| reinterpret_cast<double*>(pps.predecoded_values.data()), |
| nonnull_total, actual, in))) { |
| cleanup(); |
| return ret; |
| } |
| break; |
| default: |
| cleanup(); |
| return E_NOT_SUPPORT; |
| } |
| pps.predecoded_count = actual; |
| cleanup(); |
| return E_OK; |
| } |
| |
| // Multi-thread path: one task per value column, each decoding all non-SKIP |
| // pages of that column serially. Time pages dispatched as worker-bucketed |
| // strided tasks using per-worker decoder/compressor (filled from |
| // time_decoder_pool_ / time_compressor_pool_) so they don't contend on the |
| // shared time_decoder_/time_compressor_. |
| // |
| // Single-thread: do NOT pre-decode every page upfront — leave per_page_state |
| // empty so the scatter loop decodes on demand and releases after each page |
| // (see decode_page_lazy() / release_page_slot()). Bounds memory to one page. |
| int AlignedChunkReader::decode_all_planned_pages() { |
| if (chunk_pages_.empty()) return E_OK; |
| |
| #ifdef ENABLE_THREADS |
| if (decode_pool_ != nullptr && value_columns_.size() > 1) { |
| // Lazily grow the per-worker time decoder/compressor pool. Both |
| // factories can return nullptr on OOM/unsupported config; without |
| // checking, the worker task below dereferences null when calling |
| // decode_time_page_with(). |
| size_t worker_count = decode_pool_->num_threads(); |
| if (time_decoder_pool_.size() < worker_count) { |
| time_decoder_pool_.resize(worker_count, nullptr); |
| time_compressor_pool_.resize(worker_count, nullptr); |
| for (size_t w = 0; w < worker_count; w++) { |
| if (time_decoder_pool_[w] == nullptr) { |
| time_decoder_pool_[w] = |
| DecoderFactory::alloc_time_decoder(); |
| if (time_decoder_pool_[w] == nullptr) return E_OOM; |
| } |
| if (time_compressor_pool_[w] == nullptr) { |
| time_compressor_pool_[w] = |
| CompressorFactory::alloc_compressor( |
| time_chunk_header_.compression_type_); |
| if (time_compressor_pool_[w] == nullptr) return E_OOM; |
| } |
| } |
| } |
| |
| std::vector<std::future<void>> futures; |
| std::vector<int> col_rets(value_columns_.size(), E_OK); |
| for (uint32_t c = 0; c < value_columns_.size(); c++) { |
| int* col_ret = &col_rets[c]; |
| futures.push_back(decode_pool_->submit([this, c, col_ret]() { |
| for (size_t p = 0; p < chunk_pages_.size(); p++) { |
| int r = decode_value_page_for_slot(c, p); |
| if (IS_FAIL(r)) { |
| *col_ret = r; |
| return; |
| } |
| } |
| })); |
| } |
| // Time pages dispatched in worker-sized chunks (one task per worker) |
| // to amortize submit/wait overhead. Stride for load balance. |
| size_t time_task_count = std::min(worker_count, chunk_pages_.size()); |
| std::vector<int> time_rets(time_task_count, E_OK); |
| for (size_t k = 0; k < time_task_count; k++) { |
| int* tr = &time_rets[k]; |
| futures.push_back(decode_pool_->submit( |
| [this, k, tr, time_task_count, worker_count]() { |
| size_t wid = common::ThreadPool::current_worker_id(); |
| if (wid >= worker_count) wid = 0; |
| Decoder* dec = time_decoder_pool_[wid]; |
| Compressor* comp = time_compressor_pool_[wid]; |
| for (size_t p = k; p < chunk_pages_.size(); |
| p += time_task_count) { |
| int r = decode_time_page_with( |
| chunk_pages_[p], per_page_times_[p], dec, comp); |
| if (IS_FAIL(r)) { |
| *tr = r; |
| return; |
| } |
| } |
| })); |
| } |
| // Wait on each task's own future rather than draining the whole pool: |
| // it is shared process-wide, so wait_all() would also block on |
| // unrelated concurrent operations' tasks still in flight. |
| for (auto& f : futures) f.get(); |
| for (auto r : time_rets) { |
| if (IS_FAIL(r)) return r; |
| } |
| for (uint32_t c = 0; c < value_columns_.size(); c++) { |
| if (IS_FAIL(col_rets[c])) return col_rets[c]; |
| } |
| return E_OK; |
| } |
| #endif |
| // Single-thread: defer decode to scatter time. |
| return E_OK; |
| } |
| |
| // Decode time + all value columns for a single page slot on demand. |
| // Used by the single-thread path to keep memory bounded to one page. |
| int AlignedChunkReader::decode_page_lazy(size_t page_idx) { |
| int ret = E_OK; |
| if (RET_FAIL(decode_time_page_direct(chunk_pages_[page_idx], |
| per_page_times_[page_idx]))) { |
| return ret; |
| } |
| for (uint32_t c = 0; c < value_columns_.size(); c++) { |
| if (RET_FAIL(decode_value_page_for_slot(c, page_idx))) { |
| return ret; |
| } |
| } |
| return E_OK; |
| } |
| |
| // Release the decoded buffers of one page slot so they can be reused by the |
| // next page (keeps memory footprint bounded for the single-thread path). |
| void AlignedChunkReader::release_page_slot(size_t page_idx) { |
| std::vector<int64_t>{}.swap(per_page_times_[page_idx]); |
| for (auto* col : value_columns_) { |
| if (page_idx >= col->per_page_state.size()) continue; |
| auto& pps = col->per_page_state[page_idx]; |
| std::vector<uint8_t>{}.swap(pps.notnull_bitmap); |
| std::vector<char>{}.swap(pps.predecoded_values); |
| std::vector<common::String>{}.swap(pps.predecoded_strings); |
| pps.predecode_pa.destroy(); |
| pps.predecoded_count = 0; |
| pps.predecoded_read_pos = 0; |
| } |
| } |
| |
| int AlignedChunkReader::get_next_page_multi(TsBlock* ret_tsblock, |
| Filter* oneshoot_filter, |
| PageArena& pa) { |
| int ret = E_OK; |
| Filter* filter = |
| (oneshoot_filter != nullptr ? oneshoot_filter : time_filter_); |
| |
| // Dispatch: |
| // - Multi-column with a thread pool → chunk-level pre-decode: one task |
| // per value column decodes that column's whole chunk up front, then the |
| // scatter loop bulk-memcpys. decode_all_planned_pages() works for any |
| // column count. (An earlier cutoff sent >6 columns down the serial |
| // path because per_page_state — the upfront predecode buffer — grows |
| // with column count and was feared to thrash cache; it still grows, so |
| // very wide aligned chunks are the case to watch if reads regress.) |
| // - Single column, or no thread pool → serial path: decode the current |
| // page's columns inline (multi_DECODE_TV_BATCH), no thread-pool |
| // fan-out. |
| #ifdef ENABLE_THREADS |
| const bool use_chunk_level = |
| decode_pool_ != nullptr && value_columns_.size() > 1; |
| #else |
| const bool use_chunk_level = false; |
| #endif |
| if (!use_chunk_level) { |
| return get_next_page_multi_serial(ret_tsblock, filter, pa); |
| } |
| |
| if (!page_plan_built_) { |
| if (RET_FAIL(build_page_plan(filter))) { |
| return ret; |
| } |
| if (RET_FAIL(decode_all_planned_pages())) { |
| return ret; |
| } |
| } |
| if (chunk_pages_.empty()) { |
| return E_NO_MORE_DATA; |
| } |
| |
| const uint32_t null_mask_base = 1 << 7; |
| const uint32_t num_cols = value_columns_.size(); |
| RowAppender row_appender(ret_tsblock); |
| // Detect single-thread lazy mode by whether decode_all_planned_pages left |
| // per_page_times_ empty (it leaves slots empty when there's no pool). |
| const bool single_thread_lazy = per_page_times_[0].empty(); |
| |
| while (current_page_plan_index_ < chunk_pages_.size()) { |
| const ChunkPageInfo& page_info = chunk_pages_[current_page_plan_index_]; |
| |
| if (!current_page_loaded_) { |
| if (single_thread_lazy) { |
| if (RET_FAIL(decode_page_lazy(current_page_plan_index_))) { |
| return ret; |
| } |
| } |
| page_time_cursor_ = page_info.row_begin; |
| page_time_count_ = page_info.row_end; |
| current_page_loaded_ = true; |
| } |
| const std::vector<int64_t>& times = |
| per_page_times_[current_page_plan_index_]; |
| |
| int32_t remaining_in_page = page_time_count_ - page_time_cursor_; |
| uint32_t budget = row_appender.remaining(); |
| |
| // Fast path: FULL_PASS page, no nulls in any value column, types |
| // match destination, budget > 0. Bulk-memcpys up to |
| // min(budget, remaining_in_page) rows from page_time_cursor_; tail |
| // pages of an SSI tsblock still take the memcpy path instead of |
| // falling into the row-by-row scatter loop. |
| bool can_bulk = page_info.pass_type == PagePassType::FULL_PASS && |
| remaining_in_page > 0 && budget > 0; |
| if (can_bulk) { |
| for (uint32_t c = 0; c < num_cols; c++) { |
| auto* col = value_columns_[c]; |
| auto& pps = col->per_page_state[current_page_plan_index_]; |
| auto dt = col->chunk_header.data_type_; |
| if (dt == common::STRING || dt == common::TEXT || |
| dt == common::BLOB || |
| ret_tsblock->get_vector(c + 1)->get_vector_type() != dt || |
| pps.predecoded_count != page_time_count_) { |
| can_bulk = false; |
| break; |
| } |
| } |
| } |
| |
| if (can_bulk) { |
| uint32_t bulk_count = |
| std::min(budget, static_cast<uint32_t>(remaining_in_page)); |
| size_t time_byte_off = |
| static_cast<size_t>(page_time_cursor_) * sizeof(int64_t); |
| // Bulk-append both bytes AND row count for every Vector. |
| // Skipping add_row_nums() would leave each Vector's row_num_ |
| // at 0 while the TsBlock-level row_count_ jumped to bulk_count; |
| // fill_trailling_nulls() would then mark every just-written |
| // row as null, and column iterators would report the wrong |
| // length. |
| common::Vector* time_vec = ret_tsblock->get_vector(0); |
| time_vec->get_value_data().append_fixed_value( |
| reinterpret_cast<const char*>(times.data()) + time_byte_off, |
| bulk_count * sizeof(int64_t)); |
| time_vec->add_row_nums(bulk_count); |
| for (uint32_t c = 0; c < num_cols; c++) { |
| auto* col = value_columns_[c]; |
| auto& pps = col->per_page_state[current_page_plan_index_]; |
| uint32_t elem_size = |
| common::get_data_type_size(col->chunk_header.data_type_); |
| common::Vector* vec = ret_tsblock->get_vector(c + 1); |
| vec->get_value_data().append_fixed_value( |
| pps.predecoded_values.data() + |
| static_cast<size_t>(page_time_cursor_) * elem_size, |
| bulk_count * elem_size); |
| vec->add_row_nums(bulk_count); |
| } |
| row_appender.add_rows(bulk_count); |
| page_time_cursor_ += bulk_count; |
| if (page_time_cursor_ >= page_time_count_) { |
| if (single_thread_lazy) { |
| release_page_slot(current_page_plan_index_); |
| } |
| current_page_plan_index_++; |
| current_page_loaded_ = false; |
| continue; |
| } |
| // Budget exhausted mid-page; caller will drain and resume. |
| return E_OK; |
| } |
| |
| // Slow path: row-by-row. Handles null bitmap, type promotion, |
| // BOUNDARY pages, and partial-page E_OVERFLOW. |
| // BOUNDARY pages: build_page_plan compressed the page to the |
| // [first-hit, last-hit] range, but timestamps inside that range may |
| // still fail the filter (e.g. TimeIn({2, 8}) leaves 3..7 unmatched). |
| // Re-apply the filter per timestamp here, advancing predecoded |
| // read positions for skipped non-null rows so the cursor stays |
| // aligned with the page's value layout. |
| const bool boundary_filter = |
| page_info.pass_type == PagePassType::BOUNDARY && filter != nullptr; |
| while (page_time_cursor_ < page_time_count_) { |
| if (row_appender.remaining() == 0) { |
| return E_OK; |
| } |
| int64_t ts = times[page_time_cursor_]; |
| if (boundary_filter && !filter->satisfy_start_end_time(ts, ts)) { |
| for (uint32_t c = 0; c < num_cols; c++) { |
| auto* col = value_columns_[c]; |
| auto& pps = col->per_page_state[current_page_plan_index_]; |
| // An empty notnull_bitmap means this column carried no data |
| // for the page (a missing / sparse aligned measurement), so |
| // every row is null; otherwise consult the per-row bit. |
| bool is_null = true; |
| if (!pps.notnull_bitmap.empty()) { |
| is_null = |
| ((pps.notnull_bitmap[page_time_cursor_ / 8] & |
| 0xFF) & |
| (null_mask_base >> (page_time_cursor_ % 8))) == 0; |
| } |
| if (!is_null) pps.predecoded_read_pos++; |
| } |
| page_time_cursor_++; |
| continue; |
| } |
| if (UNLIKELY(!row_appender.add_row())) { |
| return E_OK; |
| } |
| row_appender.append(0, reinterpret_cast<char*>(&ts), sizeof(ts)); |
| |
| for (uint32_t c = 0; c < num_cols; c++) { |
| auto* col = value_columns_[c]; |
| auto& pps = col->per_page_state[current_page_plan_index_]; |
| bool is_null = true; |
| if (!pps.notnull_bitmap.empty()) { |
| is_null = |
| ((pps.notnull_bitmap[page_time_cursor_ / 8] & 0xFF) & |
| (null_mask_base >> (page_time_cursor_ % 8))) == 0; |
| } |
| if (is_null) { |
| row_appender.append_null(c + 1); |
| continue; |
| } |
| if (col->chunk_header.data_type_ == common::STRING || |
| col->chunk_header.data_type_ == common::TEXT || |
| col->chunk_header.data_type_ == common::BLOB) { |
| const common::String& value = |
| pps.predecoded_strings[pps.predecoded_read_pos++]; |
| row_appender.append(c + 1, value.buf_, value.len_); |
| } else { |
| uint32_t elem_size = common::get_data_type_size( |
| col->chunk_header.data_type_); |
| row_appender.append( |
| c + 1, |
| pps.predecoded_values.data() + |
| static_cast<size_t>(pps.predecoded_read_pos++) * |
| elem_size, |
| elem_size); |
| } |
| } |
| page_time_cursor_++; |
| } |
| |
| if (single_thread_lazy) { |
| release_page_slot(current_page_plan_index_); |
| } |
| current_page_plan_index_++; |
| current_page_loaded_ = false; |
| } |
| return E_NO_MORE_DATA; |
| } |
| |
| int AlignedChunkReader::get_next_page_multi_serial(TsBlock* ret_tsblock, |
| Filter* filter, |
| PageArena& pa) { |
| int ret = E_OK; |
| bool pt = prev_time_page_not_finish(); |
| bool pv = prev_any_value_page_not_finish_multi(); |
| if (pt && pv) { |
| ret = |
| decode_time_value_buf_into_tsblock_multi(ret_tsblock, filter, &pa); |
| return ret; |
| } |
| if (!pt && !pv) { |
| while (IS_SUCC(ret)) { |
| if (RET_FAIL(get_cur_page_header( |
| time_chunk_meta_, time_in_stream_, cur_time_page_header_, |
| time_chunk_visit_offset_, time_chunk_header_))) { |
| break; |
| } |
| for (size_t c = 0; c < value_columns_.size() && IS_SUCC(ret); c++) { |
| auto* col = value_columns_[c]; |
| if (RET_FAIL(get_cur_page_header( |
| col->chunk_meta, col->in_stream, col->cur_page_header, |
| col->chunk_visit_offset, col->chunk_header, |
| &col->file_data_buf_size))) { |
| } |
| } |
| if (IS_FAIL(ret)) break; |
| if (cur_page_statisify_filter_multi(filter)) break; |
| if (RET_FAIL(skip_cur_page_multi())) break; |
| if (!has_more_data()) { |
| ret = E_NO_MORE_DATA; |
| break; |
| } |
| } |
| if (IS_SUCC(ret)) { |
| ret = decode_cur_time_page_data(); |
| if (IS_SUCC(ret)) ret = decode_cur_value_pages_multi(); |
| } |
| } |
| if (IS_SUCC(ret)) { |
| ret = |
| decode_time_value_buf_into_tsblock_multi(ret_tsblock, filter, &pa); |
| } |
| return ret; |
| } |
| |
| bool AlignedChunkReader::cur_page_statisify_filter_multi(Filter* filter) { |
| bool time_satisfy = filter == nullptr || |
| cur_time_page_header_.statistic_ == nullptr || |
| filter->satisfy(cur_time_page_header_.statistic_); |
| return time_satisfy; |
| } |
| |
| int AlignedChunkReader::skip_cur_page_multi() { |
| time_chunk_visit_offset_ += cur_time_page_header_.compressed_size_; |
| time_in_stream_.wrapped_buf_advance_read_pos( |
| cur_time_page_header_.compressed_size_); |
| for (auto* col : value_columns_) { |
| col->chunk_visit_offset += col->cur_page_header.compressed_size_; |
| col->in_stream.wrapped_buf_advance_read_pos( |
| col->cur_page_header.compressed_size_); |
| } |
| return E_OK; |
| } |
| |
| int AlignedChunkReader::decode_cur_value_pages_multi() { |
| int ret = E_OK; |
| // Phase 1: Serial IO — ensure each column's page data is in memory. |
| for (size_t c = 0; c < value_columns_.size() && IS_SUCC(ret); c++) { |
| ret = ensure_value_page_loaded(*value_columns_[c]); |
| } |
| if (IS_FAIL(ret)) return ret; |
| |
| // Phase 2: decompress + parse bitmap + reset decoder for each column's |
| // current page, inline. This serial path now only runs for single-column |
| // reads or when no thread pool exists — multi-column reads with a pool take |
| // the chunk-level path (decode_all_planned_pages), so there is no per-page |
| // thread-pool fan-out here anymore. predecode=false lets the scatter loop |
| // (multi_DECODE_TV_BATCH) decode inline, which has better cache locality |
| // when there is no parallelism to amortize an extra predecode buffer write. |
| for (size_t c = 0; c < value_columns_.size() && IS_SUCC(ret); c++) { |
| ret = decompress_and_parse_value_page(*value_columns_[c], false); |
| } |
| return ret; |
| } |
| |
| int AlignedChunkReader::decode_cur_value_page_data_for(ValueColumnState& col) { |
| int ret = E_OK; |
| |
| // Step 1: ensure full page data is loaded |
| if (col.in_stream.remaining_size() < col.cur_page_header.compressed_size_) { |
| if (RET_FAIL(read_from_file_and_rewrap( |
| col.in_stream, col.chunk_meta, col.chunk_visit_offset, |
| col.file_data_buf_size, |
| col.cur_page_header.compressed_size_))) { |
| return ret; |
| } |
| } |
| |
| if (col.cur_page_header.compressed_size_ == 0) { |
| col.in.wrap_from(nullptr, 0); |
| return E_OK; |
| } |
| |
| // Step 2: uncompress |
| char* compressed_buf = |
| col.in_stream.get_wrapped_buf() + col.in_stream.read_pos(); |
| uint32_t compressed_size = col.cur_page_header.compressed_size_; |
| col.in_stream.wrapped_buf_advance_read_pos(compressed_size); |
| col.chunk_visit_offset += compressed_size; |
| |
| char* uncompressed_buf = nullptr; |
| uint32_t uncompressed_size = 0; |
| if (RET_FAIL(col.compressor->reset(false))) { |
| return ret; |
| } |
| if (RET_FAIL(col.compressor->uncompress(compressed_buf, compressed_size, |
| uncompressed_buf, |
| uncompressed_size))) { |
| return ret; |
| } |
| col.uncompressed_buf = uncompressed_buf; |
| |
| if (uncompressed_size != col.cur_page_header.uncompressed_size_) { |
| return E_TSFILE_CORRUPTED; |
| } |
| |
| // Step 3: parse bitmap + value data |
| if (uncompressed_size < sizeof(uint32_t)) return E_TSFILE_CORRUPTED; |
| uint32_t offset = 0; |
| uint32_t data_num = SerializationUtil::read_ui32(uncompressed_buf); |
| offset += sizeof(uint32_t); |
| uint32_t bitmap_bytes = (data_num + 7) / 8; |
| if (uncompressed_size - offset < bitmap_bytes) return E_TSFILE_CORRUPTED; |
| col.notnull_bitmap.resize(bitmap_bytes); |
| memcpy(col.notnull_bitmap.data(), uncompressed_buf + offset, bitmap_bytes); |
| offset += bitmap_bytes; |
| col.cur_value_index = -1; |
| |
| char* value_buf = uncompressed_buf + offset; |
| uint32_t value_buf_size = uncompressed_size - offset; |
| col.decoder->reset(); |
| col.in.wrap_from(value_buf, value_buf_size); |
| return ret; |
| } |
| |
| int AlignedChunkReader::ensure_value_page_loaded(ValueColumnState& col) { |
| int ret = E_OK; |
| if (col.in_stream.remaining_size() < col.cur_page_header.compressed_size_) { |
| if (RET_FAIL(read_from_file_and_rewrap( |
| col.in_stream, col.chunk_meta, col.chunk_visit_offset, |
| col.file_data_buf_size, |
| col.cur_page_header.compressed_size_))) { |
| return ret; |
| } |
| } |
| return ret; |
| } |
| |
| int AlignedChunkReader::decompress_and_parse_value_page(ValueColumnState& col, |
| bool predecode) { |
| int ret = E_OK; |
| |
| if (col.cur_page_header.compressed_size_ == 0) { |
| col.in.wrap_from(nullptr, 0); |
| return E_OK; |
| } |
| |
| // Decompress |
| char* compressed_buf = |
| col.in_stream.get_wrapped_buf() + col.in_stream.read_pos(); |
| uint32_t compressed_size = col.cur_page_header.compressed_size_; |
| col.in_stream.wrapped_buf_advance_read_pos(compressed_size); |
| col.chunk_visit_offset += compressed_size; |
| |
| char* uncompressed_buf = nullptr; |
| uint32_t uncompressed_size = 0; |
| if (RET_FAIL(col.compressor->reset(false))) { |
| return ret; |
| } |
| if (RET_FAIL(col.compressor->uncompress(compressed_buf, compressed_size, |
| uncompressed_buf, |
| uncompressed_size))) { |
| return ret; |
| } |
| col.uncompressed_buf = uncompressed_buf; |
| |
| if (uncompressed_size != col.cur_page_header.uncompressed_size_) { |
| return E_TSFILE_CORRUPTED; |
| } |
| |
| // Parse bitmap + value data |
| if (uncompressed_size < sizeof(uint32_t)) return E_TSFILE_CORRUPTED; |
| uint32_t offset = 0; |
| uint32_t data_num = SerializationUtil::read_ui32(uncompressed_buf); |
| offset += sizeof(uint32_t); |
| uint32_t bitmap_bytes = (data_num + 7) / 8; |
| if (uncompressed_size - offset < bitmap_bytes) return E_TSFILE_CORRUPTED; |
| col.notnull_bitmap.resize(bitmap_bytes); |
| memcpy(col.notnull_bitmap.data(), uncompressed_buf + offset, bitmap_bytes); |
| offset += bitmap_bytes; |
| col.cur_value_index = -1; |
| |
| char* value_buf = uncompressed_buf + offset; |
| uint32_t value_buf_size = uncompressed_size - offset; |
| col.decoder->reset(); |
| col.in.wrap_from(value_buf, value_buf_size); |
| |
| // Pre-decode all non-null values into pending_decoded_values so the |
| // scatter loop (multi_DECODE_TV_BATCH) just memcpys instead of calling |
| // the decoder. Moves the expensive int64/double decode into the worker |
| // task so it runs in parallel. Only handles fixed-length types — strings |
| // stay on the inline-decode path. |
| col.pending_decoded = false; |
| col.pending_decoded_count = 0; |
| col.pending_decoded_cursor = 0; |
| auto dt = col.chunk_header.data_type_; |
| if (predecode && dt != common::STRING && dt != common::TEXT && |
| dt != common::BLOB) { |
| int nonnull_total = 0; |
| for (uint32_t i = 0; i < data_num; i++) { |
| if ((col.notnull_bitmap[i / 8] & (0x80 >> (i % 8))) != 0) { |
| nonnull_total++; |
| } |
| } |
| if (nonnull_total > 0) { |
| uint32_t elem_size = common::get_data_type_size(dt); |
| col.pending_decoded_values.resize( |
| static_cast<size_t>(nonnull_total) * elem_size); |
| int actual = 0; |
| int rret = common::E_OK; |
| switch (dt) { |
| case common::BOOLEAN: { |
| bool* out = reinterpret_cast<bool*>( |
| col.pending_decoded_values.data()); |
| for (int i = 0; i < nonnull_total; i++) { |
| bool v; |
| if (col.decoder->read_boolean(v, col.in) != |
| common::E_OK) { |
| rret = common::E_OUT_OF_RANGE; |
| break; |
| } |
| out[i] = v; |
| } |
| actual = nonnull_total; |
| break; |
| } |
| case common::INT32: |
| case common::DATE: |
| rret = col.decoder->read_batch_int32( |
| reinterpret_cast<int32_t*>( |
| col.pending_decoded_values.data()), |
| nonnull_total, actual, col.in); |
| break; |
| case common::INT64: |
| case common::TIMESTAMP: |
| rret = col.decoder->read_batch_int64( |
| reinterpret_cast<int64_t*>( |
| col.pending_decoded_values.data()), |
| nonnull_total, actual, col.in); |
| break; |
| case common::FLOAT: |
| rret = col.decoder->read_batch_float( |
| reinterpret_cast<float*>( |
| col.pending_decoded_values.data()), |
| nonnull_total, actual, col.in); |
| break; |
| case common::DOUBLE: |
| rret = col.decoder->read_batch_double( |
| reinterpret_cast<double*>( |
| col.pending_decoded_values.data()), |
| nonnull_total, actual, col.in); |
| break; |
| default: |
| rret = common::E_OUT_OF_RANGE; |
| } |
| if (rret == common::E_OK && actual == nonnull_total) { |
| col.pending_decoded_count = nonnull_total; |
| col.pending_decoded = true; |
| } |
| } else { |
| col.pending_decoded = true; // empty page is trivially predecoded |
| } |
| } |
| return ret; |
| } |
| |
| int AlignedChunkReader::decode_time_value_buf_into_tsblock_multi( |
| TsBlock*& ret_tsblock, Filter* filter, PageArena* pa) { |
| int ret = E_OK; |
| RowAppender row_appender(ret_tsblock); |
| ret = multi_DECODE_TV_BATCH(ret_tsblock, row_appender, filter, pa); |
| |
| // Release uncompressed buffers if pages are done |
| if (ret != E_OVERFLOW) { |
| if (time_uncompressed_buf_ != nullptr) { |
| time_compressor_->after_uncompress(time_uncompressed_buf_); |
| time_uncompressed_buf_ = nullptr; |
| } |
| for (auto* col : value_columns_) { |
| if (col->uncompressed_buf != nullptr) { |
| col->compressor->after_uncompress(col->uncompressed_buf); |
| col->uncompressed_buf = nullptr; |
| } |
| // The time stream and bitmap define the page's row/value count. |
| // Once the page is fully processed, bytes left in an all-null |
| // value stream are only encoder terminators or padding and must |
| // not make has_more_data_multi() treat the page as unfinished. |
| col->in.reset(); |
| col->notnull_bitmap.clear(); |
| col->notnull_bitmap.shrink_to_fit(); |
| } |
| if (!prev_time_page_not_finish()) { |
| time_in_.reset(); |
| } |
| } else { |
| ret = E_OK; |
| } |
| return ret; |
| } |
| |
| int AlignedChunkReader::multi_DECODE_TV_BATCH(TsBlock* ret_tsblock, |
| RowAppender& row_appender, |
| Filter* filter, PageArena* pa) { |
| int ret = E_OK; |
| const int BATCH = 129; |
| int64_t times[BATCH]; |
| const uint32_t null_mask_base = 1 << 7; |
| const uint32_t num_cols = value_columns_.size(); |
| |
| while (time_decoder_->has_remaining(time_in_)) { |
| // Cap each pass to what the appender can still hold; mirrors the fix |
| // in ChunkReader's per-type batch loops. A blanket "remaining < BATCH |
| // → E_OVERFLOW" made progress impossible whenever the caller handed |
| // us a TsBlock with capacity below BATCH (e.g. small per-block sizes |
| // in multi-chunk queries). |
| int eff_batch = |
| std::min(BATCH, static_cast<int>(row_appender.remaining())); |
| if (eff_batch <= 0) { |
| ret = E_OVERFLOW; |
| break; |
| } |
| |
| // ── Phase 1: Decode a batch of timestamps ── |
| int time_count = 0; |
| if (RET_FAIL(time_decoder_->read_batch_int64(times, eff_batch, |
| time_count, time_in_))) { |
| break; |
| } |
| if (time_count == 0) break; |
| |
| // ── Phase 2: Apply time filter ── |
| bool time_mask[BATCH]; |
| bool block_all_pass = (filter == nullptr); |
| int pass_count = time_count; |
| if (!block_all_pass) { |
| pass_count = |
| filter->satisfy_batch_time(times, time_count, time_mask); |
| } |
| |
| // ── Phase 3: Per-column null check + value decode ── |
| // For each column, compute null flags and decode non-null values. |
| // We store decoded values in column-specific buffers. |
| // Max 8 bytes per value, 129 values per batch. |
| struct ColBatch { |
| bool is_null[BATCH]; |
| int nonnull_count; |
| // Value buffer for fixed-width types — up to 129 * 8 bytes |
| char val_buf[BATCH * 8]; |
| int val_count; |
| // Variable-length values for STRING/TEXT/BLOB columns. Only |
| // populated when the column's data_type_ is variable; their |
| // bufs are owned by the caller-provided PageArena. |
| std::vector<common::String> str_vals; |
| }; |
| // One ColBatch per value column. |
| std::vector<ColBatch> col_batches(num_cols); |
| |
| for (uint32_t c = 0; c < num_cols; c++) { |
| auto* col = value_columns_[c]; |
| auto& cb = col_batches[c]; |
| cb.nonnull_count = 0; |
| cb.val_count = 0; |
| for (int i = 0; i < time_count; i++) { |
| int vi = col->cur_value_index + 1 + i; |
| if (col->notnull_bitmap.empty() || |
| ((col->notnull_bitmap[vi / 8] & 0xFF) & |
| (null_mask_base >> (vi % 8))) == 0) { |
| cb.is_null[i] = true; |
| } else { |
| cb.is_null[i] = false; |
| cb.nonnull_count++; |
| } |
| } |
| |
| // Skip values if no rows pass time filter. Skip/read errors and |
| // short reads (decoder returned fewer values than the bitmap |
| // promised) must abort; otherwise the input stream is left |
| // mid-value and later batches would decode garbage from |
| // misaligned bytes. |
| if (pass_count == 0 && cb.nonnull_count > 0) { |
| int dret = common::E_OK; |
| int sk = 0; |
| switch (col->chunk_header.data_type_) { |
| case common::BOOLEAN: { |
| bool dummy; |
| for (sk = 0; sk < cb.nonnull_count; sk++) { |
| dret = col->decoder->read_boolean(dummy, col->in); |
| if (dret != common::E_OK) break; |
| } |
| break; |
| } |
| case common::INT32: |
| case common::DATE: |
| dret = col->decoder->skip_int32(cb.nonnull_count, sk, |
| col->in); |
| break; |
| case common::INT64: |
| case common::TIMESTAMP: |
| dret = col->decoder->skip_int64(cb.nonnull_count, sk, |
| col->in); |
| break; |
| case common::FLOAT: |
| dret = col->decoder->skip_float(cb.nonnull_count, sk, |
| col->in); |
| break; |
| case common::DOUBLE: |
| dret = col->decoder->skip_double(cb.nonnull_count, sk, |
| col->in); |
| break; |
| case common::STRING: |
| case common::TEXT: |
| case common::BLOB: { |
| // The decoder has no fast skip for var-length strings; |
| // reading + discarding is the only way to advance the |
| // input stream past the row's payload. |
| common::String tmp; |
| for (sk = 0; sk < cb.nonnull_count; sk++) { |
| dret = col->decoder->read_String(tmp, *pa, col->in); |
| if (dret != common::E_OK) break; |
| } |
| break; |
| } |
| default: |
| ret = E_TSFILE_CORRUPTED; |
| break; |
| } |
| if (ret != common::E_OK) break; |
| if (dret != common::E_OK) { |
| ret = dret; |
| break; |
| } |
| if (sk != cb.nonnull_count) { |
| ret = E_TSFILE_CORRUPTED; |
| break; |
| } |
| cb.nonnull_count = 0; // bytes consumed cleanly |
| } |
| |
| // Decode non-null values. Fast path: values were predecoded |
| // into col->pending_decoded_values by the parallel worker — just |
| // memcpy the slice for this batch. Fallback: call the decoder |
| // inline (used for STRING/TEXT/BLOB and when predecode was |
| // skipped). |
| if (cb.nonnull_count > 0) { |
| if (col->pending_decoded) { |
| uint32_t elem_size = common::get_data_type_size( |
| col->chunk_header.data_type_); |
| memcpy( |
| cb.val_buf, |
| col->pending_decoded_values.data() + |
| static_cast<size_t>(col->pending_decoded_cursor) * |
| elem_size, |
| static_cast<size_t>(cb.nonnull_count) * elem_size); |
| col->pending_decoded_cursor += cb.nonnull_count; |
| cb.val_count = cb.nonnull_count; |
| } else { |
| int dret = common::E_OK; |
| switch (col->chunk_header.data_type_) { |
| case common::BOOLEAN: { |
| bool* out = reinterpret_cast<bool*>(cb.val_buf); |
| cb.val_count = 0; |
| for (int s = 0; s < cb.nonnull_count; s++) { |
| bool v; |
| dret = col->decoder->read_boolean(v, col->in); |
| if (dret != common::E_OK) break; |
| out[cb.val_count++] = v; |
| } |
| break; |
| } |
| case common::INT32: |
| case common::DATE: |
| dret = col->decoder->read_batch_int32( |
| reinterpret_cast<int32_t*>(cb.val_buf), |
| cb.nonnull_count, cb.val_count, col->in); |
| break; |
| case common::INT64: |
| case common::TIMESTAMP: |
| dret = col->decoder->read_batch_int64( |
| reinterpret_cast<int64_t*>(cb.val_buf), |
| cb.nonnull_count, cb.val_count, col->in); |
| break; |
| case common::FLOAT: |
| dret = col->decoder->read_batch_float( |
| reinterpret_cast<float*>(cb.val_buf), |
| cb.nonnull_count, cb.val_count, col->in); |
| break; |
| case common::DOUBLE: |
| dret = col->decoder->read_batch_double( |
| reinterpret_cast<double*>(cb.val_buf), |
| cb.nonnull_count, cb.val_count, col->in); |
| break; |
| case common::STRING: |
| case common::TEXT: |
| case common::BLOB: { |
| // Variable-length payload doesn't fit in |
| // cb.val_buf; pull each value into str_vals and |
| // let the scatter loop index by val_count. |
| cb.str_vals.resize(cb.nonnull_count); |
| cb.val_count = 0; |
| for (int s = 0; s < cb.nonnull_count; s++) { |
| dret = col->decoder->read_String(cb.str_vals[s], |
| *pa, col->in); |
| if (dret != common::E_OK) break; |
| cb.val_count++; |
| } |
| break; |
| } |
| default: |
| break; |
| } |
| // Any decoder error, or a short decode that produced |
| // fewer values than the bitmap promised, indicates a |
| // corrupt page; propagate immediately so the scatter |
| // loop doesn't read uninitialised cb.val_buf bytes. |
| if (dret != common::E_OK) { |
| ret = dret; |
| break; |
| } |
| if (col->chunk_header.data_type_ != common::STRING && |
| col->chunk_header.data_type_ != common::TEXT && |
| col->chunk_header.data_type_ != common::BLOB && |
| cb.val_count != cb.nonnull_count) { |
| ret = E_TSFILE_CORRUPTED; |
| break; |
| } |
| } |
| } |
| } |
| if (ret != E_OK) break; |
| |
| // ── Phase 4: Skip if no rows pass ── |
| if (pass_count == 0) { |
| for (uint32_t c = 0; c < num_cols; c++) { |
| value_columns_[c]->cur_value_index += time_count; |
| } |
| continue; |
| } |
| |
| // ── Phase 5: Scatter into TsBlock ── |
| |
| // Fast path: all rows pass filter AND all columns have no nulls |
| // → batch memcpy directly into Vector buffers. STRING/TEXT/BLOB |
| // columns have variable-width payload and live in cb.str_vals, not |
| // cb.val_buf, so they must take the slow scatter path. |
| if (pass_count == time_count) { |
| bool all_nonnull_and_fixed_size = true; |
| for (uint32_t c = 0; c < num_cols; c++) { |
| auto dt = value_columns_[c]->chunk_header.data_type_; |
| if (col_batches[c].nonnull_count != time_count || |
| dt == common::STRING || dt == common::TEXT || |
| dt == common::BLOB) { |
| all_nonnull_and_fixed_size = false; |
| break; |
| } |
| } |
| if (all_nonnull_and_fixed_size) { |
| // Batch append time column (bytes + row count); see the |
| // chunk-level bulk path above for why add_row_nums() is |
| // required alongside append_fixed_value(). |
| common::Vector* time_vec = ret_tsblock->get_vector(0); |
| time_vec->get_value_data().append_fixed_value( |
| (const char*)times, |
| static_cast<uint32_t>(time_count) * sizeof(int64_t)); |
| time_vec->add_row_nums(static_cast<uint32_t>(time_count)); |
| // Batch append each value column |
| for (uint32_t c = 0; c < num_cols; c++) { |
| auto& cb = col_batches[c]; |
| auto* col = value_columns_[c]; |
| uint32_t elem_size = common::get_data_type_size( |
| col->chunk_header.data_type_); |
| common::Vector* vec = ret_tsblock->get_vector(c + 1); |
| vec->get_value_data().append_fixed_value( |
| cb.val_buf, |
| static_cast<uint32_t>(cb.val_count) * elem_size); |
| vec->add_row_nums(static_cast<uint32_t>(cb.val_count)); |
| col->cur_value_index += time_count; |
| } |
| row_appender.add_rows(static_cast<uint32_t>(time_count)); |
| continue; |
| } |
| } |
| |
| // Slow path: per-row scatter (has filter or has nulls or strings) |
| std::vector<int> val_idx(num_cols, 0); |
| |
| for (int i = 0; i < time_count; i++) { |
| bool passes = block_all_pass || time_mask[i]; |
| |
| if (!passes) { |
| for (uint32_t c = 0; c < num_cols; c++) { |
| value_columns_[c]->cur_value_index++; |
| if (!col_batches[c].is_null[i]) val_idx[c]++; |
| } |
| continue; |
| } |
| |
| if (UNLIKELY(!row_appender.add_row())) { |
| ret = E_OVERFLOW; |
| break; |
| } |
| |
| row_appender.append(0, (char*)×[i], sizeof(int64_t)); |
| |
| for (uint32_t c = 0; c < num_cols; c++) { |
| value_columns_[c]->cur_value_index++; |
| auto& cb = col_batches[c]; |
| auto* col = value_columns_[c]; |
| |
| if (cb.is_null[i]) { |
| row_appender.append_null(c + 1); |
| } else { |
| auto dt = col->chunk_header.data_type_; |
| if (dt == common::STRING || dt == common::TEXT || |
| dt == common::BLOB) { |
| const common::String& sv = cb.str_vals[val_idx[c]]; |
| row_appender.append(c + 1, sv.buf_, sv.len_); |
| } else { |
| uint32_t elem_size = common::get_data_type_size(dt); |
| row_appender.append(c + 1, |
| cb.val_buf + val_idx[c] * elem_size, |
| elem_size); |
| } |
| val_idx[c]++; |
| } |
| } |
| } |
| if (ret != E_OK) break; |
| } |
| return ret; |
| } |
| |
| } // end namespace storage |