| // 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. |
| |
| #pragma once |
| |
| #include <benchmark/benchmark.h> |
| #include <sys/stat.h> |
| #include <unistd.h> |
| |
| #include <algorithm> |
| #include <array> |
| #include <cerrno> |
| #include <charconv> |
| #include <chrono> |
| #include <cstdint> |
| #include <cstdlib> |
| #include <cstring> |
| #include <limits> |
| #include <memory> |
| #include <string> |
| #include <string_view> |
| #include <system_error> |
| #include <utility> |
| #include <vector> |
| |
| #include "common/status.h" |
| #include "core/block/block.h" |
| #include "core/column/column_nullable.h" |
| #include "core/column/column_string.h" |
| #include "core/column/column_variant.h" |
| #include "core/column/column_vector.h" |
| #include "core/column/variant_v2/column_variant_v2.h" |
| #include "core/data_type/data_type_number.h" |
| #include "core/data_type/data_type_variant.h" |
| #include "core/data_type/data_type_variant_v2.h" |
| #include "exec/common/variant_util.h" |
| #include "exprs/function/parse/variant_string_parse.h" |
| #include "io/fs/file_writer.h" |
| #include "io/fs/local_file_system.h" |
| #include "runtime/exec_env.h" |
| #include "runtime/memory/cache_manager.h" |
| #include "storage/cache/page_cache.h" |
| #include "storage/compaction/cumulative_compaction.h" |
| #include "storage/data_dir.h" |
| #include "storage/index/index_writer.h" |
| #include "storage/olap_common.h" |
| #include "storage/options.h" |
| #include "storage/rowset/beta_rowset.h" |
| #include "storage/rowset/rowset_factory.h" |
| #include "storage/rowset/rowset_reader.h" |
| #include "storage/rowset/rowset_reader_context.h" |
| #include "storage/rowset/rowset_writer.h" |
| #include "storage/rowset/rowset_writer_context.h" |
| #include "storage/schema.h" |
| #include "storage/segment/column_meta_accessor.h" |
| #include "storage/segment/segment.h" |
| #include "storage/segment/segment_loader.h" |
| #include "storage/segment/segment_writer.h" |
| #include "storage/storage_engine.h" |
| #include "storage/tablet/tablet.h" |
| #include "storage/tablet/tablet_column_object_pool.h" |
| #include "storage/tablet/tablet_meta.h" |
| #include "storage/tablet/tablet_schema.h" |
| #include "storage/tablet/tablet_schema_cache.h" |
| |
| namespace doris::variant_segment_benchmark { |
| namespace { |
| |
| // P0 workload: 200 top-level BIGINT paths, with one hot path and 29 additional paths per row. |
| // Ingest cases parse the same canonical JSON before SegmentWriter. Read cases use the same |
| // V1-written physical segment and only switch the requested V1/V2 output representation. Input |
| // generation, buffer destruction, warmup, checksum, physical-layout validation, and route |
| // validation are paused. Scan-and-rewrite cases query key plus whole Variant from that prevalidated |
| // source, then time scan/writer initialization, read, append, and destination finalize; destination |
| // validation is paused. Ingest rotates SegmentWriter at |
| // DORIS_VARIANT_BENCHMARK_ROWS_PER_SEGMENT; read cases retain their historical single-segment |
| // semantics. |
| constexpr uint32_t DEFAULT_ROWS = 1'000'000; |
| constexpr uint32_t DEFAULT_ROWS_PER_SEGMENT = 1'000'000; |
| constexpr uint32_t BATCH_ROWS = 4'096; |
| constexpr uint32_t WHOLE_VALIDATION_STRIDE = BATCH_ROWS; |
| constexpr uint32_t COMPACTION_INPUT_ROWSETS = 10; |
| constexpr uint32_t CANDIDATE_PATHS = 200; |
| constexpr uint32_t COLD_PATHS = CANDIDATE_PATHS - 2; |
| constexpr uint32_t FIELDS_PER_ROW = 30; |
| constexpr uint32_t SPARSE_HIT_PERIOD = 17; |
| constexpr int32_t KEY_UID = 0; |
| constexpr int32_t ROOT_UID = 1; |
| constexpr uint32_t BUCKETS = 16; |
| constexpr std::string_view ROOT_NAME = "v"; |
| constexpr std::string_view HOT_PATH = "hot"; |
| constexpr std::string_view SPARSE_PATH = "sparse_target"; |
| constexpr std::string_view MISSING_PATH = "global_missing"; |
| constexpr uint64_t FNV_OFFSET = 1469598103934665603ULL; |
| constexpr uint64_t FNV_PRIME = 1099511628211ULL; |
| |
| enum class VariantVersion : uint8_t { V1, V2 }; |
| enum class VariantLayout : uint8_t { SPARSE16, DOC16, FULL }; |
| enum class ReadTarget : uint8_t { WHOLE, MATERIALIZED, SPARSE, MISSING }; |
| |
| struct LayoutConfig { |
| std::string_view name; |
| int32_t max_subcolumns; |
| bool doc_mode; |
| }; |
| |
| struct LayoutCounts { |
| uint32_t materialized = 0; |
| uint32_t sparse = 0; |
| uint32_t doc = 0; |
| }; |
| |
| void count_layout_columns(const ColumnMetaPB& meta, LayoutCounts* counts) { |
| DORIS_CHECK(counts != nullptr); |
| if (meta.has_column_path_info()) { |
| PathInData path; |
| path.from_protobuf(meta.column_path_info()); |
| const std::string relative = path.copy_pop_front().get_path(); |
| if (!relative.empty()) { |
| if (relative.find(SPARSE_COLUMN_PATH) != std::string::npos) { |
| ++counts->sparse; |
| } else if (relative.find(DOC_VALUE_COLUMN_PATH) != std::string::npos) { |
| ++counts->doc; |
| } else { |
| ++counts->materialized; |
| } |
| } |
| } |
| for (const ColumnMetaPB& child : meta.children_columns()) { |
| count_layout_columns(child, counts); |
| } |
| } |
| |
| struct WriteResult { |
| uint64_t segment_bytes = 0; |
| uint64_t index_bytes = 0; |
| int64_t init_ns = 0; |
| int64_t parse_ns = 0; |
| int64_t append_ns = 0; |
| int64_t finalize_ns = 0; |
| }; |
| |
| struct PreparedSegment { |
| TabletSchemaSPtr schema; |
| segment_v2::SegmentSharedPtr segment; |
| std::string path; |
| uint64_t segment_bytes = 0; |
| LayoutCounts counts; |
| }; |
| |
| struct PreparedScan { |
| PreparedSegment* fixture = nullptr; |
| TabletSchemaSPtr query_schema; |
| ReadSchemaSPtr scan_schema; |
| ColumnId output_column_id = 0; |
| ReadTarget target = ReadTarget::WHOLE; |
| }; |
| |
| struct ScanResult { |
| uint64_t checksum = FNV_OFFSET; |
| uint64_t output_bytes = 0; |
| uint32_t rows = 0; |
| uint32_t hits = 0; |
| OlapReaderStatistics statistics; |
| }; |
| |
| struct RewriteResult { |
| uint64_t source_segment_bytes = 0; |
| uint64_t read_output_bytes = 0; |
| uint64_t destination_segment_bytes = 0; |
| uint64_t destination_index_bytes = 0; |
| uint32_t rows = 0; |
| int64_t scan_init_ns = 0; |
| int64_t writer_init_ns = 0; |
| int64_t read_ns = 0; |
| int64_t append_ns = 0; |
| int64_t finalize_ns = 0; |
| OlapReaderStatistics statistics; |
| }; |
| |
| struct CompactionResult { |
| uint64_t input_json_bytes = 0; |
| uint64_t input_disk_bytes = 0; |
| uint64_t output_disk_bytes = 0; |
| uint32_t output_segments = 0; |
| LayoutCounts output_layout; |
| }; |
| |
| std::string_view version_name(VariantVersion version) { |
| return version == VariantVersion::V1 ? "V1" : "V2"; |
| } |
| |
| LayoutConfig layout_config(VariantLayout layout) { |
| switch (layout) { |
| case VariantLayout::SPARSE16: |
| return {.name = "Sparse16", .max_subcolumns = 1, .doc_mode = false}; |
| case VariantLayout::DOC16: |
| return {.name = "Doc16", .max_subcolumns = 1, .doc_mode = true}; |
| case VariantLayout::FULL: |
| return {.name = "Full", .max_subcolumns = 0, .doc_mode = false}; |
| } |
| __builtin_unreachable(); |
| } |
| |
| Status validate_sparse16_layout(const LayoutCounts& counts) { |
| if (counts.materialized != 1 || counts.sparse != BUCKETS || counts.doc != 0) { |
| return Status::InternalError("Sparse16 layout is materialized={}, sparse={}, doc={}", |
| counts.materialized, counts.sparse, counts.doc); |
| } |
| return Status::OK(); |
| } |
| |
| void ensure_variant_compaction_runtime() { |
| ExecEnv* env = ExecEnv::GetInstance(); |
| if (env->get_cache_manager() == nullptr) { |
| env->set_cache_manager(CacheManager::create_global_instance()); |
| } |
| if (env->get_storage_page_cache() == nullptr) { |
| constexpr size_t CACHE_CAPACITY = 512UL << 20; |
| env->set_storage_page_cache(StoragePageCache::create_global_cache(CACHE_CAPACITY, 10, 0)); |
| } |
| if (env->segment_loader() == nullptr) { |
| static const std::unique_ptr<SegmentLoader> loader = |
| std::make_unique<SegmentLoader>(512UL << 20, 4'096); |
| env->set_segment_loader(loader.get()); |
| } |
| if (env->get_tablet_schema_cache() == nullptr) { |
| env->set_tablet_schema_cache(TabletSchemaCache::create_global_schema_cache( |
| config::tablet_schema_cache_capacity)); |
| } |
| if (env->get_tablet_column_object_pool() == nullptr) { |
| env->set_tablet_column_object_pool(TabletColumnObjectPool::create_global_column_cache( |
| config::tablet_schema_cache_capacity)); |
| } |
| } |
| |
| std::string_view target_name(ReadTarget target) { |
| switch (target) { |
| case ReadTarget::WHOLE: |
| return "Whole"; |
| case ReadTarget::MATERIALIZED: |
| return "Materialized100pct"; |
| case ReadTarget::SPARSE: |
| return "Sparse6pct"; |
| case ReadTarget::MISSING: |
| return "GlobalMiss"; |
| } |
| __builtin_unreachable(); |
| } |
| |
| uint32_t configured_rows() { |
| static const uint32_t rows = [] { |
| const char* value = std::getenv("DORIS_VARIANT_BENCHMARK_ROWS"); |
| if (value == nullptr) { |
| return DEFAULT_ROWS; |
| } |
| uint64_t parsed = 0; |
| const std::string_view text(value); |
| const auto [end, error] = std::from_chars(text.data(), text.data() + text.size(), parsed); |
| DORIS_CHECK(error == std::errc {} && end == text.data() + text.size()); |
| DORIS_CHECK_GT(parsed, 0); |
| DORIS_CHECK_LE(parsed, std::numeric_limits<uint32_t>::max()); |
| return static_cast<uint32_t>(parsed); |
| }(); |
| return rows; |
| } |
| |
| uint32_t configured_rows_per_segment() { |
| static const uint32_t rows = [] { |
| const char* value = std::getenv("DORIS_VARIANT_BENCHMARK_ROWS_PER_SEGMENT"); |
| if (value == nullptr) { |
| return DEFAULT_ROWS_PER_SEGMENT; |
| } |
| uint64_t parsed = 0; |
| const std::string_view text(value); |
| const auto [end, error] = std::from_chars(text.data(), text.data() + text.size(), parsed); |
| DORIS_CHECK(error == std::errc {} && end == text.data() + text.size()); |
| DORIS_CHECK_GT(parsed, 0); |
| DORIS_CHECK_LE(parsed, std::numeric_limits<uint32_t>::max()); |
| return static_cast<uint32_t>(parsed); |
| }(); |
| return rows; |
| } |
| |
| std::string benchmark_root() { |
| const char* value = std::getenv("DORIS_VARIANT_BENCHMARK_ROOT"); |
| return value == nullptr ? "/tmp" : value; |
| } |
| |
| uint64_t update_checksum(uint64_t checksum, std::string_view value) { |
| for (const unsigned char byte : value) { |
| checksum ^= byte; |
| checksum *= FNV_PRIME; |
| } |
| checksum ^= 0xff; |
| checksum *= FNV_PRIME; |
| return checksum; |
| } |
| |
| int64_t elapsed_ns(std::chrono::steady_clock::time_point start) { |
| return std::chrono::duration_cast<std::chrono::nanoseconds>(std::chrono::steady_clock::now() - |
| start) |
| .count(); |
| } |
| |
| class VariantSegmentBenchmarkData { |
| public: |
| static VariantSegmentBenchmarkData& instance() { |
| static VariantSegmentBenchmarkData data; |
| return data; |
| } |
| |
| const Status& status() const { return _status; } |
| uint32_t rows() const { return _rows; } |
| uint32_t rows_per_segment() const { return _rows_per_segment; } |
| uint32_t expected_hits(ReadTarget target) const { |
| if (target == ReadTarget::MATERIALIZED) { |
| return _rows; |
| } |
| if (target == ReadTarget::SPARSE) { |
| return (_rows + SPARSE_HIT_PERIOD - 1) / SPARSE_HIT_PERIOD; |
| } |
| return 0; |
| } |
| uint32_t segment_count() const { |
| return static_cast<uint32_t>((static_cast<uint64_t>(_rows) + _rows_per_segment - 1) / |
| _rows_per_segment); |
| } |
| uint32_t whole_validation_sample_rows() const { |
| uint64_t samples = 0; |
| for (uint32_t segment_id = 0; segment_id < segment_count(); ++segment_id) { |
| const uint32_t rows = rows_in_segment(segment_id); |
| samples += (static_cast<uint64_t>(rows) + WHOLE_VALIDATION_STRIDE - 1) / |
| WHOLE_VALIDATION_STRIDE; |
| if ((rows - 1) % WHOLE_VALIDATION_STRIDE != 0) { |
| ++samples; |
| } |
| } |
| DORIS_CHECK_LE(samples, std::numeric_limits<uint32_t>::max()); |
| return static_cast<uint32_t>(samples); |
| } |
| uint64_t input_bytes() const { return _input_bytes; } |
| std::string measured_segment_path(VariantLayout layout, VariantVersion version, |
| std::string benchmark_name) const { |
| std::replace(benchmark_name.begin(), benchmark_name.end(), '/', '_'); |
| return _directory + "/measured_" + std::string(layout_config(layout).name) + "_" + |
| std::string(version_name(version)) + "_" + benchmark_name + ".dat"; |
| } |
| Status delete_segment_files(const std::string& base_path) const { |
| for (uint32_t segment_id = 0; segment_id < segment_count(); ++segment_id) { |
| RETURN_IF_ERROR(io::global_local_filesystem()->delete_file( |
| segment_path(base_path, segment_id))); |
| } |
| return Status::OK(); |
| } |
| ParseConfig v1_parse_config(VariantLayout layout) const { |
| const TabletSchema& schema = *_schemas[static_cast<size_t>(layout)]; |
| const int32_t root_index = schema.field_index(ROOT_UID); |
| DORIS_CHECK_GE(root_index, 0); |
| ParseConfig parse_config; |
| parse_config.deprecated_enable_flatten_nested = schema.deprecated_variant_flatten_nested(); |
| parse_config.check_duplicate_json_path = config::variant_enable_duplicate_json_path_check; |
| parse_config.parse_to = variant_util::select_storage_variant_parse_target( |
| schema.column(root_index), parse_config); |
| return parse_config; |
| } |
| Status ensure_writer_warmup(VariantLayout layout, VariantVersion version, |
| PreparedSegment** prepared) { |
| if (prepared != nullptr && segment_count() != 1) { |
| return Status::InternalError( |
| "Variant read benchmark requires one segment, but rows={} and " |
| "rows_per_segment={} produce {} segments", |
| _rows, _rows_per_segment, segment_count()); |
| } |
| const auto layout_index = static_cast<size_t>(layout); |
| const auto version_index = static_cast<size_t>(version); |
| if (_writer_attempted[layout_index][version_index]) { |
| if (!_writer_errors[layout_index][version_index].empty()) { |
| return Status::InternalError(_writer_errors[layout_index][version_index]); |
| } |
| if (prepared != nullptr) { |
| *prepared = &_writer_segments[layout_index][version_index]; |
| } |
| return Status::OK(); |
| } |
| _writer_attempted[layout_index][version_index] = true; |
| |
| PreparedSegment result; |
| const std::string path = _directory + "/warm_" + std::string(layout_config(layout).name) + |
| "_" + std::string(version_name(version)) + ".dat"; |
| RETURN_IF_ERROR(delete_segment_files(path)); |
| WriteResult write_result; |
| std::vector<uint64_t> segment_bytes; |
| Status status = |
| write_segments(layout, version, path, nullptr, &write_result, &segment_bytes); |
| if (status.ok()) { |
| LayoutCounts counts; |
| status = validate_written_segments(layout, version, path, segment_bytes, &counts, |
| segment_count() == 1 ? &result : nullptr); |
| } |
| if (!status.ok()) { |
| _writer_errors[layout_index][version_index] = status.to_string(); |
| return status; |
| } |
| if (segment_count() == 1) { |
| _writer_segments[layout_index][version_index] = std::move(result); |
| } |
| if (prepared != nullptr) { |
| *prepared = &_writer_segments[layout_index][version_index]; |
| } |
| return Status::OK(); |
| } |
| |
| Status ensure_read_validation(VariantLayout layout, ReadTarget target, |
| PreparedSegment** fixture) { |
| const auto layout_index = static_cast<size_t>(layout); |
| const auto target_index = static_cast<size_t>(target); |
| PreparedSegment* segment = nullptr; |
| RETURN_IF_ERROR(ensure_writer_warmup(layout, VariantVersion::V1, &segment)); |
| |
| if (_read_attempted[layout_index][target_index]) { |
| if (!_read_errors[layout_index][target_index].empty()) { |
| return Status::InternalError(_read_errors[layout_index][target_index]); |
| } |
| *fixture = segment; |
| return Status::OK(); |
| } |
| _read_attempted[layout_index][target_index] = true; |
| |
| PreparedScan v1_scan; |
| PreparedScan v2_scan; |
| Status status = prepare_scan(segment, target, VariantVersion::V1, &v1_scan); |
| if (status.ok()) { |
| status = prepare_scan(segment, target, VariantVersion::V2, &v2_scan); |
| } |
| ScanResult v1_result; |
| ScanResult v2_result; |
| const bool sample_whole = target == ReadTarget::WHOLE; |
| if (status.ok()) { |
| status = scan_segment(v1_scan, true, _rows, &v1_result, sample_whole); |
| } |
| if (status.ok()) { |
| status = scan_segment(v2_scan, true, _rows, &v2_result, sample_whole); |
| } |
| if (status.ok() && (v1_result.rows != v2_result.rows || v1_result.hits != v2_result.hits)) { |
| status = Status::InternalError("{} {} V1/V2 mismatch: rows {}/{}, hits {}/{}", |
| layout_config(layout).name, target_name(target), |
| v1_result.rows, v2_result.rows, v1_result.hits, |
| v2_result.hits); |
| } |
| const auto expected = expected_checksum(target); |
| if (status.ok() && !sample_whole && |
| (v1_result.checksum != expected || v2_result.checksum != expected)) { |
| status = Status::InternalError("{} {} checksum mismatch: V1 {}, V2 {}, input {}", |
| layout_config(layout).name, target_name(target), |
| v1_result.checksum, v2_result.checksum, expected); |
| } |
| if (status.ok()) { |
| status = validate_hit_count(target, v1_result.hits); |
| } |
| if (status.ok()) { |
| status = validate_route(target, v1_result.statistics); |
| } |
| if (status.ok()) { |
| status = validate_route(target, v2_result.statistics); |
| } |
| if (!status.ok()) { |
| _read_errors[layout_index][target_index] = status.to_string(); |
| return status; |
| } |
| *fixture = segment; |
| return Status::OK(); |
| } |
| |
| Status prepare_scan(PreparedSegment* fixture, ReadTarget target, VariantVersion version, |
| PreparedScan* prepared) const { |
| DORIS_CHECK(fixture != nullptr); |
| DORIS_CHECK(prepared != nullptr); |
| TabletSchemaPB schema_pb; |
| fixture->schema->to_schema_pb(&schema_pb); |
| auto query_schema = std::make_shared<TabletSchema>(); |
| query_schema->init_from_pb(schema_pb); |
| query_schema->set_storage_format(fixture->schema->storage_format()); |
| |
| const int32_t root_id = query_schema->field_index(ROOT_UID); |
| if (root_id < 0) { |
| return Status::InternalError("Variant benchmark root column is missing"); |
| } |
| const TabletColumn& root = query_schema->column(root_id); |
| auto output_id = static_cast<ColumnId>(root_id); |
| if (target == ReadTarget::WHOLE) { |
| query_schema->mutable_column(root_id).set_variant_is_v2(version == VariantVersion::V2); |
| } else { |
| std::string_view relative_path; |
| switch (target) { |
| case ReadTarget::MATERIALIZED: |
| relative_path = HOT_PATH; |
| break; |
| case ReadTarget::SPARSE: |
| relative_path = SPARSE_PATH; |
| break; |
| case ReadTarget::MISSING: |
| relative_path = MISSING_PATH; |
| break; |
| case ReadTarget::WHOLE: |
| __builtin_unreachable(); |
| } |
| const std::string full_path = root.name_lower_case() + "." + std::string(relative_path); |
| DataTypePtr path_type = std::make_shared<DataTypeVariant>( |
| root.variant_max_subcolumns_count(), root.variant_enable_doc_mode()); |
| if (version == VariantVersion::V2) { |
| path_type = std::make_shared<DataTypeVariantV2>(root.variant_max_subcolumns_count(), |
| root.variant_enable_doc_mode()); |
| } |
| TabletColumn path_column = variant_util::get_column_by_type( |
| path_type, full_path, |
| variant_util::ExtraInfo {.parent_unique_id = root.unique_id(), |
| .path_info = PathInData(full_path)}); |
| path_column.set_is_nullable(true); |
| variant_util::inherit_column_attributes(root, path_column); |
| query_schema->append_column(path_column, TabletSchema::ColumnType::VARIANT); |
| output_id = static_cast<ColumnId>(query_schema->num_columns() - 1); |
| } |
| |
| prepared->fixture = fixture; |
| prepared->query_schema = std::move(query_schema); |
| prepared->scan_schema = std::make_shared<ReadSchema>(project_columns_by_ordinal( |
| prepared->query_schema->columns(), std::vector<ColumnId> {output_id})); |
| prepared->output_column_id = output_id; |
| prepared->target = target; |
| return Status::OK(); |
| } |
| |
| Status prepare_rewrite_scan(PreparedSegment* fixture, VariantVersion version, |
| PreparedScan* prepared) const { |
| RETURN_IF_ERROR(prepare_scan(fixture, ReadTarget::WHOLE, version, prepared)); |
| const int32_t key_id = prepared->query_schema->field_index(KEY_UID); |
| const int32_t root_id = prepared->query_schema->field_index(ROOT_UID); |
| if (key_id < 0 || root_id < 0) { |
| return Status::InternalError("Variant rewrite benchmark columns are missing"); |
| } |
| prepared->scan_schema = std::make_shared<ReadSchema>( |
| project_columns_by_ordinal(prepared->query_schema->columns(), |
| std::vector<ColumnId> {static_cast<ColumnId>(key_id), |
| static_cast<ColumnId>(root_id)})); |
| return Status::OK(); |
| } |
| |
| Status rewrite_segment(const PreparedScan& prepared, VariantLayout layout, |
| VariantVersion version, const std::string& destination_path, |
| RewriteResult* result) const { |
| DORIS_CHECK(prepared.fixture != nullptr); |
| DORIS_CHECK(result != nullptr); |
| if (segment_count() != 1) { |
| return Status::InternalError( |
| "Variant rewrite benchmark requires one source segment, got {}", |
| segment_count()); |
| } |
| |
| const auto scan_init_start = std::chrono::steady_clock::now(); |
| StorageReadOptions read_options; |
| read_options.stats = &result->statistics; |
| read_options.tablet_schema = prepared.query_schema; |
| read_options.io_ctx.reader_type = ReaderType::READER_QUERY; |
| read_options.use_page_cache = true; |
| read_options.block_row_max = BATCH_ROWS; |
| read_options.preferred_block_size_bytes = 0; |
| RowwiseIteratorUPtr iterator; |
| RETURN_IF_ERROR(prepared.fixture->segment->new_iterator(prepared.scan_schema, read_options, |
| &iterator)); |
| result->scan_init_ns += elapsed_ns(scan_init_start); |
| |
| const TabletSchemaSPtr& destination_schema = _schemas[static_cast<size_t>(layout)]; |
| const auto writer_init_start = std::chrono::steady_clock::now(); |
| io::FileWriterPtr file_writer; |
| RETURN_IF_ERROR(io::global_local_filesystem()->create_file(destination_path, &file_writer)); |
| RowsetWriterContext rowset_context; |
| rowset_context.write_type = DataWriteType::TYPE_DIRECT; |
| rowset_context.tablet_schema = destination_schema; |
| rowset_context.tablet_path = _directory; |
| |
| segment_v2::SegmentWriterOptions writer_options; |
| writer_options.num_rows_per_block = BATCH_ROWS; |
| writer_options.max_rows_per_segment = _rows_per_segment; |
| writer_options.compression_type = CompressionTypePB::LZ4; |
| writer_options.rowset_ctx = &rowset_context; |
| writer_options.write_type = DataWriteType::TYPE_DIRECT; |
| segment_v2::SegmentWriter writer(file_writer.get(), 0, destination_schema, nullptr, nullptr, |
| writer_options, nullptr); |
| RETURN_IF_ERROR(writer.init()); |
| result->writer_init_ns += elapsed_ns(writer_init_start); |
| |
| Block block = prepared.scan_schema->create_read_block(); |
| bool checked_representation = false; |
| while (true) { |
| const auto read_start = std::chrono::steady_clock::now(); |
| Status status = iterator->next_batch(&block); |
| result->read_ns += elapsed_ns(read_start); |
| if (status.is<ErrorCode::END_OF_FILE>()) { |
| break; |
| } |
| RETURN_IF_ERROR(status); |
| if (!checked_representation) { |
| const IColumn& root = *block.get_by_position(1).column; |
| const bool is_v2 = check_and_get_column<ColumnVariantV2>(root) != nullptr; |
| const bool is_v1 = check_and_get_column<ColumnVariant>(root) != nullptr; |
| if ((version == VariantVersion::V2 && !is_v2) || |
| (version == VariantVersion::V1 && (!is_v1 || is_v2))) { |
| return Status::InternalError("Variant rewrite query returned the wrong column"); |
| } |
| checked_representation = true; |
| } |
| const uint32_t batch_rows = static_cast<uint32_t>(block.rows()); |
| result->rows += batch_rows; |
| result->read_output_bytes += block.bytes(); |
| const auto append_start = std::chrono::steady_clock::now(); |
| RETURN_IF_ERROR(writer.append_block(&block, 0, batch_rows)); |
| result->append_ns += elapsed_ns(append_start); |
| block.clear_column_data(); |
| } |
| if (result->rows != _rows) { |
| return Status::InternalError("Variant rewrite read {} rows, expected {}", result->rows, |
| _rows); |
| } |
| |
| const auto finalize_start = std::chrono::steady_clock::now(); |
| RETURN_IF_ERROR(writer.finalize(&result->destination_segment_bytes, |
| &result->destination_index_bytes)); |
| result->finalize_ns += elapsed_ns(finalize_start); |
| result->source_segment_bytes = prepared.fixture->segment_bytes; |
| return Status::OK(); |
| } |
| |
| Status write_segments(VariantLayout layout, VariantVersion version, |
| const std::string& base_path, benchmark::State* state, |
| WriteResult* result, std::vector<uint64_t>* segment_bytes) const { |
| DORIS_CHECK(result != nullptr); |
| DORIS_CHECK(segment_bytes != nullptr); |
| const TabletSchemaSPtr& schema = _schemas[static_cast<size_t>(layout)]; |
| const auto key_type = std::make_shared<DataTypeInt64>(); |
| segment_bytes->clear(); |
| segment_bytes->reserve(segment_count()); |
| |
| for (uint32_t segment_id = 0; segment_id < segment_count(); ++segment_id) { |
| const uint32_t segment_begin = segment_id * _rows_per_segment; |
| const uint32_t segment_rows = rows_in_segment(segment_id); |
| const auto init_start = std::chrono::steady_clock::now(); |
| { |
| io::FileWriterPtr file_writer; |
| RETURN_IF_ERROR(io::global_local_filesystem()->create_file( |
| segment_path(base_path, segment_id), &file_writer)); |
| RowsetWriterContext rowset_context; |
| rowset_context.write_type = DataWriteType::TYPE_DIRECT; |
| rowset_context.tablet_schema = schema; |
| rowset_context.tablet_path = _directory; |
| |
| segment_v2::SegmentWriterOptions options; |
| options.num_rows_per_block = BATCH_ROWS; |
| options.max_rows_per_segment = _rows_per_segment; |
| options.compression_type = CompressionTypePB::LZ4; |
| options.rowset_ctx = &rowset_context; |
| options.write_type = DataWriteType::TYPE_DIRECT; |
| |
| segment_v2::SegmentWriter writer(file_writer.get(), segment_id, schema, nullptr, |
| nullptr, options, nullptr); |
| RETURN_IF_ERROR(writer.init()); |
| result->init_ns += elapsed_ns(init_start); |
| |
| for (uint32_t offset = 0; offset < segment_rows; offset += BATCH_ROWS) { |
| const uint32_t batch_rows = std::min(BATCH_ROWS, segment_rows - offset); |
| if (state != nullptr) { |
| state->PauseTiming(); |
| } |
| { |
| ColumnString::MutablePtr raw_json; |
| ColumnPtr key_batch; |
| build_input_batch(segment_begin + offset, batch_rows, &raw_json, |
| &key_batch); |
| if (state != nullptr) { |
| state->ResumeTiming(); |
| } |
| |
| ColumnPtr variant_column; |
| DataTypePtr variant_type; |
| const auto parse_start = std::chrono::steady_clock::now(); |
| RETURN_IF_ERROR(parse_batch(layout, version, *raw_json, batch_rows, |
| &variant_column, &variant_type)); |
| result->parse_ns += elapsed_ns(parse_start); |
| |
| Block block; |
| block.insert({std::move(key_batch), key_type, "k"}); |
| block.insert({std::move(variant_column), std::move(variant_type), |
| std::string(ROOT_NAME)}); |
| const auto append_start = std::chrono::steady_clock::now(); |
| RETURN_IF_ERROR(writer.append_block(&block, 0, batch_rows)); |
| result->append_ns += elapsed_ns(append_start); |
| if (state != nullptr) { |
| state->PauseTiming(); |
| } |
| } |
| if (state != nullptr) { |
| state->ResumeTiming(); |
| } |
| } |
| |
| uint64_t bytes = 0; |
| uint64_t index_bytes = 0; |
| const auto finalize_start = std::chrono::steady_clock::now(); |
| RETURN_IF_ERROR(writer.finalize(&bytes, &index_bytes)); |
| result->finalize_ns += elapsed_ns(finalize_start); |
| result->segment_bytes += bytes; |
| result->index_bytes += index_bytes; |
| if (state != nullptr) { |
| state->PauseTiming(); |
| } |
| segment_bytes->push_back(bytes); |
| } |
| if (state != nullptr) { |
| state->ResumeTiming(); |
| } |
| } |
| return Status::OK(); |
| } |
| |
| Status validate_written_segments(VariantLayout layout, VariantVersion version, |
| const std::string& base_path, |
| const std::vector<uint64_t>& segment_bytes, |
| LayoutCounts* counts, PreparedSegment* retained) const { |
| DORIS_CHECK(counts != nullptr); |
| if (segment_bytes.size() != segment_count()) { |
| return Status::InternalError("Variant benchmark wrote {} segments, expected {}", |
| segment_bytes.size(), segment_count()); |
| } |
| if (retained != nullptr && segment_count() != 1) { |
| return Status::InternalError("Only a single segment can be retained for read cases"); |
| } |
| |
| *counts = LayoutCounts {}; |
| uint64_t total_rows = 0; |
| for (uint32_t segment_id = 0; segment_id < segment_count(); ++segment_id) { |
| PreparedSegment measured; |
| measured.schema = _schemas[static_cast<size_t>(layout)]; |
| measured.path = segment_path(base_path, segment_id); |
| measured.segment_bytes = segment_bytes[segment_id]; |
| const uint32_t expected_rows = rows_in_segment(segment_id); |
| RETURN_IF_ERROR( |
| open_and_validate_segment(layout, segment_id, expected_rows, &measured)); |
| |
| PreparedScan scan; |
| RETURN_IF_ERROR(prepare_scan(&measured, ReadTarget::WHOLE, version, &scan)); |
| ScanResult scan_result; |
| const uint32_t global_row_offset = segment_id * _rows_per_segment; |
| RETURN_IF_ERROR( |
| scan_segment(scan, true, expected_rows, &scan_result, true, global_row_offset)); |
| RETURN_IF_ERROR(validate_route(ReadTarget::WHOLE, scan_result.statistics)); |
| total_rows += scan_result.rows; |
| |
| if (segment_id == 0) { |
| *counts = measured.counts; |
| } else if (measured.counts.materialized != counts->materialized || |
| measured.counts.sparse != counts->sparse || |
| measured.counts.doc != counts->doc) { |
| return Status::InternalError( |
| "{} segment {} layout differs: materialized={}, sparse={}, doc={}", |
| layout_config(layout).name, segment_id, measured.counts.materialized, |
| measured.counts.sparse, measured.counts.doc); |
| } |
| |
| if (retained != nullptr) { |
| *retained = std::move(measured); |
| } else { |
| measured.segment.reset(); |
| RETURN_IF_ERROR(io::global_local_filesystem()->delete_file(measured.path)); |
| } |
| } |
| if (total_rows != _rows) { |
| return Status::InternalError( |
| "Variant benchmark read {} rows from {} segments, expected {}", total_rows, |
| segment_count(), _rows); |
| } |
| return Status::OK(); |
| } |
| |
| Status scan_segment(const PreparedScan& prepared, bool checksum, uint32_t expected_rows, |
| ScanResult* result, bool sample_whole = false, |
| uint32_t global_row_offset = 0) const { |
| DORIS_CHECK(prepared.fixture != nullptr); |
| DORIS_CHECK(result != nullptr); |
| DORIS_CHECK(!sample_whole || (checksum && prepared.target == ReadTarget::WHOLE)); |
| StorageReadOptions options; |
| options.stats = &result->statistics; |
| options.tablet_schema = prepared.query_schema; |
| options.io_ctx.reader_type = ReaderType::READER_QUERY; |
| options.use_page_cache = true; |
| options.block_row_max = BATCH_ROWS; |
| options.preferred_block_size_bytes = 0; |
| |
| RowwiseIteratorUPtr iterator; |
| RETURN_IF_ERROR( |
| prepared.fixture->segment->new_iterator(prepared.scan_schema, options, &iterator)); |
| Block block = prepared.scan_schema->create_read_block(); |
| while (true) { |
| Status status = iterator->next_batch(&block); |
| if (status.is<ErrorCode::END_OF_FILE>()) { |
| break; |
| } |
| RETURN_IF_ERROR(status); |
| const auto& output = block.get_by_position(0); |
| const uint32_t block_begin = result->rows; |
| result->rows += static_cast<uint32_t>(block.rows()); |
| result->output_bytes += block.bytes(); |
| if (checksum && prepared.target != ReadTarget::WHOLE) { |
| for (size_t row = 0; row < block.rows(); ++row) { |
| result->hits += !output.column->is_null_at(row); |
| } |
| } |
| if (checksum) { |
| if (sample_whole) { |
| for (size_t row = 0; row < block.rows(); ++row) { |
| const uint32_t local_row = block_begin + static_cast<uint32_t>(row); |
| if (local_row % WHOLE_VALIDATION_STRIDE != 0 && |
| local_row + 1 != expected_rows) { |
| continue; |
| } |
| |
| const std::string actual = output.type->to_string(*output.column, row); |
| const uint32_t global_row = global_row_offset + local_row; |
| const std::string expected = make_json(global_row); |
| if (actual != expected) { |
| return Status::InternalError( |
| "Variant whole-column sample differs at global row {}", |
| global_row); |
| } |
| } |
| } else { |
| for (size_t row = 0; row < block.rows(); ++row) { |
| result->checksum = update_checksum( |
| result->checksum, output.type->to_string(*output.column, row)); |
| } |
| } |
| } else { |
| benchmark::DoNotOptimize(result->output_bytes); |
| } |
| block.clear_column_data(); |
| } |
| if (result->rows != expected_rows) { |
| return Status::InternalError("Variant benchmark read {} rows, expected {}", |
| result->rows, expected_rows); |
| } |
| return Status::OK(); |
| } |
| |
| private: |
| VariantSegmentBenchmarkData() |
| : _rows(configured_rows()), |
| _rows_per_segment(std::min(configured_rows_per_segment(), _rows)), |
| _directory(benchmark_root() + "/doris_variant_segment_benchmark_" + |
| std::to_string(getpid())) { |
| _status = initialize(); |
| } |
| |
| ~VariantSegmentBenchmarkData() { |
| for (auto& layouts : _writer_segments) { |
| for (auto& segment : layouts) { |
| segment.segment.reset(); |
| } |
| } |
| WARN_IF_ERROR(io::global_local_filesystem()->delete_directory(_directory), |
| "Failed to clean Variant segment benchmark directory"); |
| } |
| |
| Status initialize() { |
| if (ExecEnv::GetInstance()->get_cache_manager() == nullptr) { |
| ExecEnv::GetInstance()->set_cache_manager(CacheManager::create_global_instance()); |
| } |
| if (ExecEnv::GetInstance()->get_storage_page_cache() == nullptr) { |
| constexpr size_t CACHE_CAPACITY = 512UL << 20; |
| ExecEnv::GetInstance()->set_storage_page_cache( |
| StoragePageCache::create_global_cache(CACHE_CAPACITY, 10, 0)); |
| } |
| RETURN_IF_ERROR(io::global_local_filesystem()->delete_directory(_directory)); |
| RETURN_IF_ERROR(io::global_local_filesystem()->create_directory(_directory)); |
| _schemas[static_cast<size_t>(VariantLayout::SPARSE16)] = |
| make_schema(VariantLayout::SPARSE16); |
| _schemas[static_cast<size_t>(VariantLayout::DOC16)] = make_schema(VariantLayout::DOC16); |
| _schemas[static_cast<size_t>(VariantLayout::FULL)] = make_schema(VariantLayout::FULL); |
| build_input_oracle(); |
| return Status::OK(); |
| } |
| |
| uint64_t expected_checksum(ReadTarget target) const { |
| return _expected_checksums[static_cast<size_t>(target)]; |
| } |
| |
| uint32_t rows_in_segment(uint32_t segment_id) const { |
| const uint64_t begin = static_cast<uint64_t>(segment_id) * _rows_per_segment; |
| return static_cast<uint32_t>( |
| std::min<uint64_t>(_rows_per_segment, static_cast<uint64_t>(_rows) - begin)); |
| } |
| |
| std::string segment_path(const std::string& base_path, uint32_t segment_id) const { |
| if (segment_count() == 1) { |
| return base_path; |
| } |
| return base_path + "." + std::to_string(segment_id); |
| } |
| |
| public: |
| static TabletSchemaSPtr make_schema(VariantLayout layout) { |
| const LayoutConfig config = layout_config(layout); |
| TabletSchemaPB schema_pb; |
| schema_pb.set_keys_type(KeysType::DUP_KEYS); |
| schema_pb.set_num_short_key_columns(1); |
| |
| auto* key = schema_pb.add_column(); |
| key->set_unique_id(KEY_UID); |
| key->set_name("k"); |
| key->set_type("BIGINT"); |
| key->set_is_key(true); |
| key->set_is_nullable(false); |
| |
| auto* variant = schema_pb.add_column(); |
| variant->set_unique_id(ROOT_UID); |
| variant->set_name(std::string(ROOT_NAME)); |
| variant->set_type("VARIANT"); |
| variant->set_is_key(false); |
| variant->set_is_nullable(false); |
| variant->set_variant_max_subcolumns_count(config.max_subcolumns); |
| variant->set_variant_max_sparse_column_statistics_size(10'000); |
| variant->set_variant_sparse_hash_shard_count(BUCKETS); |
| variant->set_variant_enable_doc_mode(config.doc_mode); |
| variant->set_variant_doc_materialization_min_rows(std::numeric_limits<int64_t>::max()); |
| variant->set_variant_doc_hash_shard_count(BUCKETS); |
| |
| auto schema = std::make_shared<TabletSchema>(); |
| schema->init_from_pb(schema_pb); |
| schema->set_storage_format(TabletStorageFormatPB::TABLET_STORAGE_FORMAT_V3); |
| return schema; |
| } |
| |
| static std::string make_json(uint32_t row) { |
| static const std::array<std::string, COLD_PATHS> cold_keys = [] { |
| std::array<std::string, COLD_PATHS> keys; |
| for (uint32_t cold = 0; cold < COLD_PATHS; ++cold) { |
| std::string suffix = std::to_string(cold); |
| suffix.insert(0, 3 - suffix.size(), '0'); |
| keys[cold] = "cold_" + suffix; |
| } |
| return keys; |
| }(); |
| std::array<uint16_t, FIELDS_PER_ROW - 1> cold_ids {}; |
| const bool include_sparse = row % SPARSE_HIT_PERIOD == 0; |
| const size_t cold_count = FIELDS_PER_ROW - 1 - include_sparse; |
| const uint32_t start = (static_cast<uint64_t>(row) * 37) % COLD_PATHS; |
| for (size_t index = 0; index < cold_count; ++index) { |
| cold_ids[index] = static_cast<uint16_t>((start + index) % COLD_PATHS); |
| } |
| std::sort(cold_ids.begin(), cold_ids.begin() + cold_count); |
| |
| std::string json; |
| json.reserve(640); |
| json.push_back('{'); |
| bool first = true; |
| const auto append_field = [&](std::string_view key, uint64_t value) { |
| if (!first) { |
| json.push_back(','); |
| } |
| first = false; |
| json.push_back('"'); |
| json.append(key); |
| json += "\":"; |
| json += std::to_string(value); |
| }; |
| for (size_t index = 0; index < cold_count; ++index) { |
| const uint32_t cold = cold_ids[index]; |
| append_field(cold_keys[cold], static_cast<uint64_t>(row) * COLD_PATHS + cold); |
| } |
| append_field(HOT_PATH, row); |
| if (include_sparse) { |
| append_field(SPARSE_PATH, static_cast<uint64_t>(row) * 17 + 3); |
| } |
| json.push_back('}'); |
| DORIS_CHECK_EQ(std::count(json.begin(), json.end(), ':'), FIELDS_PER_ROW); |
| return json; |
| } |
| |
| private: |
| static void build_input_batch(uint32_t begin, uint32_t rows, ColumnString::MutablePtr* raw_json, |
| ColumnPtr* keys) { |
| DORIS_CHECK(raw_json != nullptr); |
| DORIS_CHECK(keys != nullptr); |
| auto json_batch = ColumnString::create(); |
| json_batch->reserve(rows); |
| auto key_batch = ColumnInt64::create(); |
| key_batch->reserve(rows); |
| for (uint32_t offset = 0; offset < rows; ++offset) { |
| const uint32_t row = begin + offset; |
| const std::string json = make_json(row); |
| json_batch->insert_data(json.data(), json.size()); |
| key_batch->insert_value(row); |
| } |
| *raw_json = std::move(json_batch); |
| *keys = std::move(key_batch); |
| } |
| |
| void build_input_oracle() { |
| _segment_expected_checksums.resize(segment_count()); |
| for (auto& checksums : _segment_expected_checksums) { |
| checksums.fill(FNV_OFFSET); |
| } |
| for (uint32_t segment_id = 0; segment_id < segment_count(); ++segment_id) { |
| const uint32_t segment_begin = segment_id * _rows_per_segment; |
| const uint32_t segment_rows = rows_in_segment(segment_id); |
| for (uint32_t offset = 0; offset < segment_rows; ++offset) { |
| const uint32_t row = segment_begin + offset; |
| const std::string json = make_json(row); |
| const bool include_sparse = row % SPARSE_HIT_PERIOD == 0; |
| const std::string materialized = std::to_string(row); |
| const std::string sparse = |
| include_sparse ? std::to_string(static_cast<uint64_t>(row) * 17 + 3) |
| : "NULL"; |
| _input_bytes += json.size(); |
| const std::array<std::string_view, 4> values {json, materialized, sparse, "NULL"}; |
| for (size_t target = 0; target < values.size(); ++target) { |
| _expected_checksums[target] = |
| update_checksum(_expected_checksums[target], values[target]); |
| _segment_expected_checksums[segment_id][target] = update_checksum( |
| _segment_expected_checksums[segment_id][target], values[target]); |
| } |
| } |
| } |
| } |
| |
| Status parse_batch(VariantLayout layout, VariantVersion version, const ColumnString& raw_json, |
| uint32_t rows, ColumnPtr* column, DataTypePtr* type) const { |
| DORIS_CHECK(column != nullptr); |
| DORIS_CHECK(type != nullptr); |
| const LayoutConfig config = layout_config(layout); |
| if (version == VariantVersion::V1) { |
| auto values = ColumnVariant::create(config.max_subcolumns, config.doc_mode); |
| const ParseConfig parse_config = v1_parse_config(layout); |
| JsonParser parser; |
| RETURN_IF_CATCH_EXCEPTION({ |
| for (size_t row = 0; row < rows; ++row) { |
| variant_util::parse_json_to_variant(*values, raw_json.get_data_at(row), &parser, |
| parse_config); |
| } |
| values->finalize(); |
| }); |
| *column = std::move(values); |
| *type = std::make_shared<DataTypeVariant>(config.max_subcolumns, config.doc_mode); |
| } else { |
| auto values = ColumnVariantV2::create(); |
| RETURN_IF_CATCH_EXCEPTION({ |
| JsonStringToVariantEncoder encoder(JsonToVariantOptions::current_config()); |
| for (size_t row = 0; row < rows; ++row) { |
| encoder.add_json(raw_json.get_data_at(row)); |
| } |
| VariantBatchBuilder encoded = encoder.finish_batch(); |
| values->insert_encoded_batch(encoded); |
| }); |
| *column = std::move(values); |
| *type = std::make_shared<DataTypeVariantV2>(config.max_subcolumns, config.doc_mode); |
| } |
| DORIS_CHECK_EQ((*column)->size(), rows); |
| return Status::OK(); |
| } |
| |
| Status open_and_validate_segment(VariantLayout layout, uint32_t segment_id, |
| uint32_t expected_rows, PreparedSegment* prepared) const { |
| DORIS_CHECK(prepared != nullptr); |
| RowsetId rowset_id; |
| rowset_id.init(10'000 + static_cast<int64_t>(layout)); |
| RETURN_IF_ERROR(segment_v2::Segment::open(io::global_local_filesystem(), prepared->path, |
| 20'000 + static_cast<int64_t>(layout), segment_id, |
| rowset_id, prepared->schema, |
| io::FileReaderOptions {}, &prepared->segment)); |
| if (prepared->segment->num_rows() != expected_rows) { |
| return Status::InternalError("{} segment has {} rows, expected {}", |
| layout_config(layout).name, prepared->segment->num_rows(), |
| expected_rows); |
| } |
| |
| std::shared_ptr<SegmentFooterPB> footer; |
| OlapReaderStatistics statistics; |
| io::IOContext io_context; |
| io_context.reader_type = ReaderType::READER_QUERY; |
| RETURN_IF_ERROR(prepared->segment->_get_segment_footer(footer, &statistics, &io_context)); |
| segment_v2::ColumnMetaAccessor accessor; |
| RETURN_IF_ERROR(accessor.init(*footer, prepared->segment->_file_reader)); |
| RETURN_IF_ERROR(accessor.traverse_metas( |
| *footer, |
| [&](const ColumnMetaPB& meta) { count_layout_columns(meta, &prepared->counts); }, |
| &statistics, &io_context)); |
| |
| const LayoutCounts& counts = prepared->counts; |
| switch (layout) { |
| case VariantLayout::SPARSE16: |
| if (counts.materialized != 1 || counts.sparse != BUCKETS || counts.doc != 0) { |
| return Status::InternalError( |
| "Sparse16 layout is materialized={}, sparse={}, doc={}", |
| counts.materialized, counts.sparse, counts.doc); |
| } |
| break; |
| case VariantLayout::DOC16: |
| if (counts.materialized != 0 || counts.sparse != 0 || counts.doc != BUCKETS) { |
| return Status::InternalError("Doc16 layout is materialized={}, sparse={}, doc={}", |
| counts.materialized, counts.sparse, counts.doc); |
| } |
| break; |
| case VariantLayout::FULL: |
| if (counts.materialized != CANDIDATE_PATHS || counts.sparse != BUCKETS || |
| counts.doc != 0) { |
| return Status::InternalError("Full layout is materialized={}, sparse={}, doc={}", |
| counts.materialized, counts.sparse, counts.doc); |
| } |
| break; |
| } |
| return Status::OK(); |
| } |
| |
| Status validate_hit_count(ReadTarget target, uint32_t hits) const { |
| if (target == ReadTarget::WHOLE) { |
| return Status::OK(); |
| } |
| const uint32_t expected = expected_hits(target); |
| if (hits != expected) { |
| return Status::InternalError("{} hit count is {}, expected {}", target_name(target), |
| hits, expected); |
| } |
| return Status::OK(); |
| } |
| |
| static Status validate_route(ReadTarget target, const OlapReaderStatistics& statistics) { |
| switch (target) { |
| case ReadTarget::WHOLE: |
| if (statistics.variant_subtree_hierarchical_iter_count <= 0) { |
| return Status::InternalError("Whole-column scan did not use HIERARCHICAL"); |
| } |
| break; |
| case ReadTarget::MATERIALIZED: |
| if (statistics.variant_subtree_leaf_iter_count <= 0) { |
| return Status::InternalError("Materialized scan did not use LEAF"); |
| } |
| break; |
| case ReadTarget::SPARSE: |
| if (statistics.variant_subtree_sparse_iter_count <= 0) { |
| return Status::InternalError("Sparse scan did not use BINARY_EXTRACT"); |
| } |
| break; |
| case ReadTarget::MISSING: |
| if (statistics.variant_subtree_default_iter_count <= 0) { |
| return Status::InternalError("Global-miss scan did not use DEFAULT_FILL"); |
| } |
| break; |
| } |
| return Status::OK(); |
| } |
| |
| uint32_t _rows; |
| uint32_t _rows_per_segment; |
| std::string _directory; |
| Status _status = Status::OK(); |
| uint64_t _input_bytes = 0; |
| std::array<uint64_t, 4> _expected_checksums {FNV_OFFSET, FNV_OFFSET, FNV_OFFSET, FNV_OFFSET}; |
| std::vector<std::array<uint64_t, 4>> _segment_expected_checksums; |
| std::array<TabletSchemaSPtr, 3> _schemas; |
| std::array<std::array<PreparedSegment, 2>, 3> _writer_segments; |
| std::array<std::array<bool, 2>, 3> _writer_attempted {}; |
| std::array<std::array<std::string, 2>, 3> _writer_errors; |
| std::array<std::array<bool, 4>, 3> _read_attempted {}; |
| std::array<std::array<std::string, 4>, 3> _read_errors; |
| }; |
| |
| uint64_t next_compaction_fixture_id() { |
| static uint64_t next_id = 0; |
| return ++next_id; |
| } |
| |
| // Measures the production cumulative-compaction entry point. Input rowsets are |
| // persisted by the V2 writer, while the current compactor intentionally reads |
| // and writes ColumnVariant; there is no V1-input parser in this fixture. |
| class VariantCompactionBenchmarkFixture { |
| public: |
| VariantCompactionBenchmarkFixture() |
| : _total_rows(configured_rows()), |
| _rows_per_rowset(_total_rows / COMPACTION_INPUT_ROWSETS), |
| _fixture_id(next_compaction_fixture_id()), |
| _directory(benchmark_root() + "/doris_variant_compaction_benchmark_" + |
| std::to_string(getpid()) + "_" + std::to_string(_fixture_id)), |
| _tmp_directory(_directory + "/tmp"), |
| _previous_ordered_compaction(config::enable_ordered_data_compaction), |
| _previous_compaction_checksum(config::enable_compaction_checksum), |
| _previous_vertical_compaction(config::enable_vertical_compaction), |
| _previous_vertical_variant_compaction( |
| config::enable_vertical_compact_variant_subcolumns) {} |
| |
| ~VariantCompactionBenchmarkFixture() { |
| _compaction.reset(); |
| _input_rowsets.clear(); |
| _tablet.reset(); |
| _schema.reset(); |
| _data_dir.reset(); |
| _engine = nullptr; |
| if (_installed_runtime) { |
| ExecEnv* env = ExecEnv::GetInstance(); |
| env->set_storage_engine(std::move(_previous_storage_engine)); |
| env->set_tmp_file_dir(std::move(_previous_tmp_file_dirs)); |
| } |
| WARN_IF_ERROR(io::global_local_filesystem()->delete_directory(_directory), |
| "Failed to clean Variant compaction benchmark directory"); |
| config::enable_ordered_data_compaction = _previous_ordered_compaction; |
| config::enable_compaction_checksum = _previous_compaction_checksum; |
| config::enable_vertical_compaction = _previous_vertical_compaction; |
| config::enable_vertical_compact_variant_subcolumns = _previous_vertical_variant_compaction; |
| } |
| |
| uint32_t total_rows() const { return _total_rows; } |
| uint32_t rows_per_rowset() const { return _rows_per_rowset; } |
| uint32_t input_segments() const { return _input_segments; } |
| CumulativeCompaction* compaction() const { return _compaction.get(); } |
| |
| Status prepare() { |
| if (config::variant_storage_parse_mode != 0) { |
| return Status::InvalidArgument( |
| "Variant cumulative compaction benchmark requires " |
| "variant_storage_parse_mode=0, actual={}", |
| config::variant_storage_parse_mode); |
| } |
| if (_total_rows % COMPACTION_INPUT_ROWSETS != 0) { |
| return Status::InvalidArgument("Variant compaction rows {} must be divisible by {}", |
| _total_rows, COMPACTION_INPUT_ROWSETS); |
| } |
| |
| ensure_variant_compaction_runtime(); |
| config::enable_ordered_data_compaction = false; |
| config::enable_compaction_checksum = false; |
| config::enable_vertical_compaction = true; |
| config::enable_vertical_compact_variant_subcolumns = true; |
| RETURN_IF_ERROR(io::global_local_filesystem()->delete_directory(_directory)); |
| RETURN_IF_ERROR(io::global_local_filesystem()->create_directory(_directory)); |
| RETURN_IF_ERROR(io::global_local_filesystem()->create_directory(_tmp_directory)); |
| |
| ExecEnv* env = ExecEnv::GetInstance(); |
| _previous_storage_engine = std::move(env->_storage_engine); |
| _previous_tmp_file_dirs = std::move(env->_tmp_file_dirs); |
| _installed_runtime = true; |
| |
| std::vector<StorePath> tmp_paths; |
| tmp_paths.emplace_back(_tmp_directory, 100ULL << 30); |
| auto tmp_file_dirs = std::make_unique<segment_v2::TmpFileDirs>(tmp_paths); |
| RETURN_IF_ERROR(tmp_file_dirs->init()); |
| env->set_tmp_file_dir(std::move(tmp_file_dirs)); |
| |
| EngineOptions engine_options; |
| auto engine = std::make_unique<StorageEngine>(engine_options); |
| _engine = engine.get(); |
| _data_dir = std::make_unique<DataDir>(*_engine, _directory); |
| RETURN_IF_ERROR(_data_dir->init(true)); |
| env->set_storage_engine(std::move(engine)); |
| |
| _schema = VariantSegmentBenchmarkData::make_schema(VariantLayout::SPARSE16); |
| auto tablet_meta = std::make_shared<TabletMeta>(_schema); |
| const int64_t tablet_id = 100'000 + static_cast<int64_t>(_fixture_id); |
| tablet_meta->_tablet_id = tablet_id; |
| tablet_meta->set_tablet_uid(TabletUid(tablet_id, tablet_id + 1)); |
| _tablet = std::make_shared<Tablet>(*_engine, tablet_meta, _data_dir.get()); |
| RETURN_IF_ERROR(_tablet->init()); |
| RETURN_IF_ERROR(io::global_local_filesystem()->delete_directory(_tablet->tablet_path())); |
| RETURN_IF_ERROR(io::global_local_filesystem()->create_directory(_tablet->tablet_path())); |
| |
| build_oracle(); |
| _input_rowsets.reserve(COMPACTION_INPUT_ROWSETS); |
| for (uint32_t index = 0; index < COMPACTION_INPUT_ROWSETS; ++index) { |
| RowsetSharedPtr rowset; |
| RETURN_IF_ERROR(write_input_rowset(index, &rowset)); |
| LayoutCounts input_layout; |
| RETURN_IF_ERROR(validate_rowset_layout(rowset, _rows_per_rowset, &input_layout)); |
| _input_segments += cast_set<uint32_t>(rowset->num_segments()); |
| RETURN_IF_ERROR(record_input_inodes(rowset)); |
| RETURN_IF_ERROR(_tablet->add_rowset(rowset)); |
| _input_disk_bytes += rowset->total_disk_size(); |
| _input_rowsets.emplace_back(std::move(rowset)); |
| } |
| |
| _compaction = std::make_unique<CumulativeCompaction>(*_engine, _tablet); |
| _compaction->_input_rowsets = _input_rowsets; |
| return Status::OK(); |
| } |
| |
| Status validate(CompactionResult* result) { |
| DORIS_CHECK(result != nullptr); |
| if (_compaction == nullptr || _compaction->_output_rowset == nullptr) { |
| return Status::InternalError("Variant compaction did not produce an output rowset"); |
| } |
| if (_compaction->_is_ordered_data_compaction) { |
| return Status::InternalError("Variant compaction used ordered link-file compaction"); |
| } |
| |
| const RowsetSharedPtr& output = _compaction->_output_rowset; |
| if (output->start_version() != 0 || output->end_version() != COMPACTION_INPUT_ROWSETS - 1) { |
| return Status::InternalError("Compaction output version is [{},{}], expected [0,{}]", |
| output->start_version(), output->end_version(), |
| COMPACTION_INPUT_ROWSETS - 1); |
| } |
| if (output->num_rows() != _total_rows) { |
| return Status::InternalError("Compaction output has {} rows, expected {}", |
| output->num_rows(), _total_rows); |
| } |
| const int32_t root_index = output->tablet_schema()->field_index(ROOT_UID); |
| if (root_index < 0 || output->tablet_schema()->column(root_index).variant_is_v2()) { |
| return Status::InternalError( |
| "ProductionCurrent compaction output unexpectedly uses Variant V2 schema"); |
| } |
| RETURN_IF_ERROR(validate_physical_rewrite(output)); |
| RETURN_IF_ERROR(validate_rowset_layout(output, _total_rows, &result->output_layout)); |
| RETURN_IF_ERROR(validate_output_oracle(output)); |
| result->input_json_bytes = _input_json_bytes; |
| result->input_disk_bytes = _input_disk_bytes; |
| result->output_disk_bytes = output->total_disk_size(); |
| result->output_segments = cast_set<uint32_t>(output->num_segments()); |
| return Status::OK(); |
| } |
| |
| private: |
| void build_oracle() { |
| for (uint32_t row = 0; row < _total_rows; ++row) { |
| const std::string json = VariantSegmentBenchmarkData::make_json(row); |
| const std::string key = std::to_string(row); |
| _expected_whole_checksum = update_checksum(_expected_whole_checksum, json); |
| _expected_key_checksum = update_checksum(_expected_key_checksum, key); |
| _expected_hot_checksum = update_checksum(_expected_hot_checksum, key); |
| const bool has_sparse = row % SPARSE_HIT_PERIOD == 0; |
| _expected_sparse_checksum = update_checksum( |
| _expected_sparse_checksum, |
| has_sparse ? std::to_string(static_cast<uint64_t>(row) * 17 + 3) : "NULL"); |
| _expected_sparse_hits += has_sparse; |
| } |
| } |
| |
| Status make_v2_input_block(uint32_t rowset_index, uint32_t first_local_row, uint32_t rows, |
| Block* block) { |
| auto keys = ColumnInt64::create(); |
| auto raw_json = ColumnString::create(); |
| keys->reserve(rows); |
| raw_json->reserve(rows); |
| for (uint32_t local = 0; local < rows; ++local) { |
| const uint32_t global_row = |
| (first_local_row + local) * COMPACTION_INPUT_ROWSETS + rowset_index; |
| const std::string json = VariantSegmentBenchmarkData::make_json(global_row); |
| keys->insert_value(global_row); |
| raw_json->insert_data(json.data(), json.size()); |
| _input_json_bytes += json.size(); |
| } |
| |
| auto values = ColumnVariantV2::create(); |
| RETURN_IF_CATCH_EXCEPTION({ |
| JsonStringToVariantEncoder encoder(JsonToVariantOptions::current_config()); |
| for (uint32_t row = 0; row < rows; ++row) { |
| encoder.add_json(raw_json->get_data_at(row)); |
| } |
| VariantBatchBuilder encoded = encoder.finish_batch(); |
| values->insert_encoded_batch(encoded); |
| }); |
| block->insert({std::move(keys), std::make_shared<DataTypeInt64>(), "k"}); |
| block->insert({std::move(values), std::make_shared<DataTypeVariantV2>(1, false), |
| std::string(ROOT_NAME)}); |
| return Status::OK(); |
| } |
| |
| Status write_input_rowset(uint32_t rowset_index, RowsetSharedPtr* rowset) { |
| RowsetWriterContext context; |
| RowsetId rowset_id; |
| rowset_id.init(static_cast<int64_t>(_fixture_id) * 100 + rowset_index + 1); |
| context.rowset_id = rowset_id; |
| context.rowset_type = BETA_ROWSET; |
| context.data_dir = _data_dir.get(); |
| context.rowset_state = VISIBLE; |
| context.tablet_schema = _schema; |
| context.tablet_path = _tablet->tablet_path(); |
| context.tablet_id = _tablet->tablet_id(); |
| context.tablet_uid = _tablet->tablet_uid(); |
| context.tablet = _tablet; |
| context.version = Version(rowset_index, rowset_index); |
| context.segments_overlap = NONOVERLAPPING; |
| context.max_rows_per_segment = _rows_per_rowset; |
| context.write_type = DataWriteType::TYPE_DIRECT; |
| |
| auto writer_result = RowsetFactory::create_rowset_writer(*_engine, context, false); |
| if (!writer_result.has_value()) { |
| return writer_result.error(); |
| } |
| auto writer = std::move(writer_result).value(); |
| for (uint32_t first = 0; first < _rows_per_rowset; first += BATCH_ROWS) { |
| Block block; |
| RETURN_IF_ERROR(make_v2_input_block( |
| rowset_index, first, std::min(BATCH_ROWS, _rows_per_rowset - first), &block)); |
| RETURN_IF_ERROR(writer->add_block(&block)); |
| } |
| RETURN_IF_ERROR(writer->flush()); |
| RETURN_IF_ERROR(writer->build(*rowset)); |
| return Status::OK(); |
| } |
| |
| Status validate_rowset_layout(const RowsetSharedPtr& rowset, uint32_t expected_rows, |
| LayoutCounts* result) const { |
| auto beta_rowset = std::static_pointer_cast<BetaRowset>(rowset); |
| std::vector<segment_v2::SegmentSharedPtr> segments; |
| RETURN_IF_ERROR(beta_rowset->load_segments(&segments)); |
| if (segments.empty()) { |
| return Status::InternalError("Rowset has no segments"); |
| } |
| uint64_t rows = 0; |
| bool first = true; |
| for (const auto& segment : segments) { |
| LayoutCounts counts; |
| RETURN_IF_ERROR(segment->traverse_column_meta_pbs( |
| [&](const ColumnMetaPB& meta) { count_layout_columns(meta, &counts); })); |
| RETURN_IF_ERROR(validate_sparse16_layout(counts)); |
| if (first) { |
| *result = counts; |
| first = false; |
| } else if (counts.materialized != result->materialized || |
| counts.sparse != result->sparse || counts.doc != result->doc) { |
| return Status::InternalError("Compaction output segment layouts differ"); |
| } |
| rows += segment->num_rows(); |
| } |
| if (rows != expected_rows) { |
| return Status::InternalError("Rowset segments contain {} rows, expected {}", rows, |
| expected_rows); |
| } |
| return Status::OK(); |
| } |
| |
| Status record_input_inodes(const RowsetSharedPtr& rowset) { |
| for (uint32_t segment = 0; segment < rowset->num_segments(); ++segment) { |
| struct stat file_stat {}; |
| const std::string path = local_segment_path(_tablet->tablet_path(), |
| rowset->rowset_id().to_string(), segment); |
| if (::stat(path.c_str(), &file_stat) != 0) { |
| return Status::IOError("stat {} failed: {}", path, std::strerror(errno)); |
| } |
| _input_inodes.push_back(file_stat.st_ino); |
| } |
| return Status::OK(); |
| } |
| |
| Status validate_physical_rewrite(const RowsetSharedPtr& output) const { |
| for (uint32_t segment = 0; segment < output->num_segments(); ++segment) { |
| struct stat file_stat {}; |
| const std::string path = local_segment_path(_tablet->tablet_path(), |
| output->rowset_id().to_string(), segment); |
| if (::stat(path.c_str(), &file_stat) != 0) { |
| return Status::IOError("stat {} failed: {}", path, std::strerror(errno)); |
| } |
| if (std::find(_input_inodes.begin(), _input_inodes.end(), file_stat.st_ino) != |
| _input_inodes.end()) { |
| return Status::InternalError("Compaction output segment {} reused an input inode", |
| segment); |
| } |
| } |
| return Status::OK(); |
| } |
| |
| Status validate_output_oracle(const RowsetSharedPtr& output) const { |
| TabletSchemaPB schema_pb; |
| _schema->to_schema_pb(&schema_pb); |
| auto query_schema = std::make_shared<TabletSchema>(); |
| query_schema->init_from_pb(schema_pb); |
| query_schema->set_storage_format(_schema->storage_format()); |
| const int32_t root_index = query_schema->field_index(ROOT_UID); |
| if (root_index < 0) { |
| return Status::InternalError("Variant root is missing from compaction read schema"); |
| } |
| query_schema->mutable_column(root_index).set_variant_is_v2(false); |
| const TabletColumn root = query_schema->column(root_index); |
| const auto append_path = [&](std::string_view path) { |
| const std::string full_path = root.name_lower_case() + "." + std::string(path); |
| TabletColumn path_column = variant_util::get_column_by_type( |
| std::make_shared<DataTypeVariant>(1, false), full_path, |
| variant_util::ExtraInfo {.parent_unique_id = root.unique_id(), |
| .path_info = PathInData(full_path)}); |
| path_column.set_is_nullable(true); |
| variant_util::inherit_column_attributes(root, path_column); |
| query_schema->append_column(path_column, TabletSchema::ColumnType::VARIANT); |
| return static_cast<uint32_t>(query_schema->num_columns() - 1); |
| }; |
| const uint32_t hot_id = append_path(HOT_PATH); |
| const uint32_t sparse_id = append_path(SPARSE_PATH); |
| auto read_schema = std::make_shared<ReadSchema>(project_columns_by_ordinal( |
| query_schema->columns(), |
| std::vector<ColumnId> {0, static_cast<ColumnId>(root_index), hot_id, sparse_id})); |
| |
| RowsetReaderSharedPtr reader; |
| RETURN_IF_ERROR(output->create_reader(&reader)); |
| OlapReaderStatistics statistics; |
| RowsetReaderContext context; |
| context.reader_type = ReaderType::READER_QUERY; |
| context.tablet_schema = query_schema; |
| context.need_ordered_result = true; |
| context.read_schema = read_schema; |
| context.stats = &statistics; |
| RETURN_IF_ERROR(reader->init(&context)); |
| |
| uint64_t whole_checksum = FNV_OFFSET; |
| uint64_t key_checksum = FNV_OFFSET; |
| uint64_t hot_checksum = FNV_OFFSET; |
| uint64_t sparse_checksum = FNV_OFFSET; |
| uint32_t rows = 0; |
| uint32_t hot_hits = 0; |
| uint32_t sparse_hits = 0; |
| bool saw_current_compactor = false; |
| while (true) { |
| Block block = read_schema->create_read_block(); |
| Status status = reader->next_batch(&block); |
| if (status.is<ErrorCode::END_OF_FILE>()) { |
| break; |
| } |
| RETURN_IF_ERROR(status); |
| const auto& key = block.get_by_position(0); |
| const auto& whole = block.get_by_position(1); |
| const auto& hot = block.get_by_position(2); |
| const auto& sparse = block.get_by_position(3); |
| if (block.rows() > 0 && !saw_current_compactor) { |
| const auto& nested = |
| assert_cast<const ColumnNullable&>(*hot.column).get_nested_column(); |
| if (check_and_get_column<ColumnVariant>(nested) == nullptr || |
| check_and_get_column<ColumnVariantV2>(nested) != nullptr) { |
| return Status::InternalError( |
| "ProductionCurrent compaction did not return ColumnVariant"); |
| } |
| saw_current_compactor = true; |
| } |
| for (size_t row = 0; row < block.rows(); ++row) { |
| whole_checksum = |
| update_checksum(whole_checksum, whole.type->to_string(*whole.column, row)); |
| key_checksum = update_checksum(key_checksum, key.type->to_string(*key.column, row)); |
| hot_checksum = update_checksum(hot_checksum, hot.type->to_string(*hot.column, row)); |
| sparse_checksum = update_checksum(sparse_checksum, |
| sparse.type->to_string(*sparse.column, row)); |
| hot_hits += !hot.column->is_null_at(row); |
| sparse_hits += !sparse.column->is_null_at(row); |
| } |
| rows += static_cast<uint32_t>(block.rows()); |
| } |
| if (!saw_current_compactor || rows != _total_rows || hot_hits != _total_rows || |
| sparse_hits != _expected_sparse_hits || whole_checksum != _expected_whole_checksum || |
| key_checksum != _expected_key_checksum || hot_checksum != _expected_hot_checksum || |
| sparse_checksum != _expected_sparse_checksum) { |
| return Status::InternalError( |
| "Compaction oracle mismatch: rows={}/{}, hot_hits={}/{}, sparse_hits={}/{}, " |
| "whole_checksum={}/{}, key_checksum={}/{}, hot_checksum={}/{}, " |
| "sparse_checksum={}/{}", |
| rows, _total_rows, hot_hits, _total_rows, sparse_hits, _expected_sparse_hits, |
| whole_checksum, _expected_whole_checksum, key_checksum, _expected_key_checksum, |
| hot_checksum, _expected_hot_checksum, sparse_checksum, |
| _expected_sparse_checksum); |
| } |
| return Status::OK(); |
| } |
| |
| uint32_t _total_rows; |
| uint32_t _rows_per_rowset; |
| uint32_t _input_segments = 0; |
| uint64_t _fixture_id; |
| std::string _directory; |
| std::string _tmp_directory; |
| bool _previous_ordered_compaction; |
| bool _previous_compaction_checksum; |
| bool _previous_vertical_compaction; |
| bool _previous_vertical_variant_compaction; |
| bool _installed_runtime = false; |
| std::unique_ptr<BaseStorageEngine> _previous_storage_engine; |
| std::unique_ptr<segment_v2::TmpFileDirs> _previous_tmp_file_dirs; |
| StorageEngine* _engine = nullptr; |
| std::unique_ptr<DataDir> _data_dir; |
| TabletSchemaSPtr _schema; |
| TabletSharedPtr _tablet; |
| std::vector<RowsetSharedPtr> _input_rowsets; |
| std::vector<ino_t> _input_inodes; |
| std::unique_ptr<CumulativeCompaction> _compaction; |
| uint64_t _input_json_bytes = 0; |
| uint64_t _input_disk_bytes = 0; |
| uint64_t _expected_whole_checksum = FNV_OFFSET; |
| uint64_t _expected_key_checksum = FNV_OFFSET; |
| uint64_t _expected_hot_checksum = FNV_OFFSET; |
| uint64_t _expected_sparse_checksum = FNV_OFFSET; |
| uint32_t _expected_sparse_hits = 0; |
| }; |
| |
| bool benchmark_status(benchmark::State& state, const Status& status) { |
| if (status.ok()) { |
| return true; |
| } |
| const std::string message = status.to_string(); |
| state.SkipWithError(message); |
| return false; |
| } |
| |
| void add_common_counters(benchmark::State& state, const VariantSegmentBenchmarkData& data) { |
| state.counters["batch_rows"] = |
| benchmark::Counter(BATCH_ROWS, benchmark::Counter::kIsIterationInvariant); |
| state.counters["rows_per_run"] = |
| benchmark::Counter(data.rows(), benchmark::Counter::kIsIterationInvariant); |
| state.counters["rows_per_segment"] = |
| benchmark::Counter(data.rows_per_segment(), benchmark::Counter::kIsIterationInvariant); |
| state.counters["segment_count"] = |
| benchmark::Counter(data.segment_count(), benchmark::Counter::kIsIterationInvariant); |
| state.counters["input_bytes_per_row"] = |
| benchmark::Counter(static_cast<double>(data.input_bytes()) / data.rows(), |
| benchmark::Counter::kIsIterationInvariant); |
| } |
| |
| void BM_VariantIngestToSegment(benchmark::State& state, VariantLayout layout, |
| VariantVersion version) { |
| VariantSegmentBenchmarkData* data = nullptr; |
| std::string path; |
| WriteResult result; |
| std::vector<uint64_t> segment_bytes; |
| LayoutCounts measured_counts; |
| bool completed = false; |
| for (auto _ : state) { |
| benchmark::DoNotOptimize(_); |
| state.PauseTiming(); |
| data = &VariantSegmentBenchmarkData::instance(); |
| Status status = data->status(); |
| if (status.ok()) { |
| status = data->ensure_writer_warmup(layout, version, nullptr); |
| } |
| if (status.ok()) { |
| path = data->measured_segment_path(layout, version, state.name()); |
| status = data->delete_segment_files(path); |
| } |
| state.ResumeTiming(); |
| if (!benchmark_status(state, status)) { |
| break; |
| } |
| result = WriteResult {}; |
| status = data->write_segments(layout, version, path, &state, &result, &segment_bytes); |
| if (!benchmark_status(state, status)) { |
| break; |
| } |
| benchmark::DoNotOptimize(result.segment_bytes); |
| state.PauseTiming(); |
| status = data->validate_written_segments(layout, version, path, segment_bytes, |
| &measured_counts, nullptr); |
| state.ResumeTiming(); |
| if (!benchmark_status(state, status)) { |
| break; |
| } |
| completed = true; |
| } |
| if (!completed) { |
| return; |
| } |
| add_common_counters(state, *data); |
| state.counters["whole_validation_sample_rows"] = benchmark::Counter( |
| data->whole_validation_sample_rows(), benchmark::Counter::kIsIterationInvariant); |
| state.counters["whole_validation_sample_stride"] = |
| benchmark::Counter(WHOLE_VALIDATION_STRIDE, benchmark::Counter::kIsIterationInvariant); |
| state.counters["whole_validation_row_count_full"] = |
| benchmark::Counter(1, benchmark::Counter::kIsIterationInvariant); |
| state.counters["whole_validation_footer_layout_full"] = |
| benchmark::Counter(1, benchmark::Counter::kIsIterationInvariant); |
| state.counters["whole_validation_route_full_scan"] = |
| benchmark::Counter(1, benchmark::Counter::kIsIterationInvariant); |
| state.counters["whole_validation_canonical_sampled"] = |
| benchmark::Counter(1, benchmark::Counter::kIsIterationInvariant); |
| state.SetItemsProcessed(static_cast<int64_t>(data->rows()) * state.iterations()); |
| state.SetBytesProcessed(static_cast<int64_t>(data->input_bytes()) * state.iterations()); |
| state.counters["segment_bytes_per_row"] = |
| benchmark::Counter(static_cast<double>(result.segment_bytes) / data->rows(), |
| benchmark::Counter::kIsIterationInvariant); |
| state.counters["index_bytes"] = benchmark::Counter(static_cast<double>(result.index_bytes), |
| benchmark::Counter::kIsIterationInvariant); |
| state.counters["init_ms"] = result.init_ns / 1e6; |
| state.counters["parse_ms"] = result.parse_ns / 1e6; |
| state.counters["append_ms"] = result.append_ns / 1e6; |
| state.counters["finalize_ms"] = result.finalize_ns / 1e6; |
| state.counters["materialized_columns"] = measured_counts.materialized; |
| state.counters["sparse_columns"] = measured_counts.sparse; |
| state.counters["doc_columns"] = measured_counts.doc; |
| const ParseConfig parse_config = data->v1_parse_config(layout); |
| state.counters["variant_storage_parse_mode"] = config::variant_storage_parse_mode; |
| state.counters["v1_parse_to_doc_value"] = |
| parse_config.parse_to == ParseConfig::ParseTo::OnlyDocValueColumn; |
| } |
| |
| void BM_VariantRead(benchmark::State& state, VariantLayout layout, ReadTarget target, |
| VariantVersion version) { |
| VariantSegmentBenchmarkData* data = nullptr; |
| PreparedSegment* fixture = nullptr; |
| PreparedScan prepared; |
| ScanResult result; |
| bool completed = false; |
| for (auto _ : state) { |
| benchmark::DoNotOptimize(_); |
| state.PauseTiming(); |
| data = &VariantSegmentBenchmarkData::instance(); |
| Status status = data->status(); |
| if (status.ok()) { |
| status = data->ensure_read_validation(layout, target, &fixture); |
| } |
| if (status.ok()) { |
| status = data->prepare_scan(fixture, target, version, &prepared); |
| } |
| state.ResumeTiming(); |
| if (!benchmark_status(state, status)) { |
| break; |
| } |
| result = ScanResult {}; |
| status = data->scan_segment(prepared, false, data->rows(), &result); |
| if (!benchmark_status(state, status)) { |
| break; |
| } |
| completed = true; |
| } |
| if (!completed) { |
| return; |
| } |
| add_common_counters(state, *data); |
| state.counters["prevalidation_whole_sample_rows_per_representation"] = benchmark::Counter( |
| target == ReadTarget::WHOLE ? data->whole_validation_sample_rows() : 0, |
| benchmark::Counter::kIsIterationInvariant); |
| state.counters["prevalidation_whole_sample_stride"] = |
| target == ReadTarget::WHOLE ? WHOLE_VALIDATION_STRIDE : 0; |
| state.counters["prevalidation_exact_oracle_full_rows_per_representation"] = |
| benchmark::Counter(target == ReadTarget::WHOLE ? 0 : data->rows(), |
| benchmark::Counter::kIsIterationInvariant); |
| state.counters["prevalidation_representation_scans"] = 2; |
| state.counters["prevalidation_row_count_full"] = 1; |
| state.counters["prevalidation_footer_layout_full"] = 1; |
| state.counters["prevalidation_route_full_scan"] = 1; |
| state.counters["prevalidation_canonical_sampled"] = target == ReadTarget::WHOLE ? 1 : 0; |
| state.counters["prevalidation_checksum_full"] = target == ReadTarget::WHOLE ? 0 : 1; |
| state.SetItemsProcessed(static_cast<int64_t>(result.rows)); |
| if (target == ReadTarget::WHOLE) { |
| state.SetBytesProcessed(static_cast<int64_t>(data->input_bytes())); |
| } |
| state.counters["output_bytes_per_row"] = |
| benchmark::Counter(static_cast<double>(result.output_bytes) / result.rows, |
| benchmark::Counter::kIsIterationInvariant); |
| state.counters["segment_bytes_per_row"] = |
| benchmark::Counter(static_cast<double>(fixture->segment_bytes) / data->rows(), |
| benchmark::Counter::kIsIterationInvariant); |
| const uint32_t expected_hits = data->expected_hits(target); |
| state.counters["hit_count"] = expected_hits; |
| state.counters["hit_rate"] = static_cast<double>(expected_hits) / result.rows; |
| state.counters["variant_storage_parse_mode"] = config::variant_storage_parse_mode; |
| state.counters["route_leaf"] = result.statistics.variant_subtree_leaf_iter_count; |
| state.counters["route_binary_extract"] = result.statistics.variant_subtree_sparse_iter_count; |
| state.counters["route_default_fill"] = result.statistics.variant_subtree_default_iter_count; |
| state.counters["route_hierarchical"] = |
| result.statistics.variant_subtree_hierarchical_iter_count; |
| state.counters["materialized_columns"] = fixture->counts.materialized; |
| state.counters["sparse_columns"] = fixture->counts.sparse; |
| state.counters["doc_columns"] = fixture->counts.doc; |
| } |
| |
| void BM_VariantScanAndRewriteSegment(benchmark::State& state, VariantLayout layout, |
| VariantVersion version) { |
| VariantSegmentBenchmarkData* data = nullptr; |
| PreparedSegment* source = nullptr; |
| PreparedScan prepared; |
| RewriteResult result; |
| LayoutCounts destination_counts; |
| std::string destination_path; |
| bool completed = false; |
| for (auto _ : state) { |
| benchmark::DoNotOptimize(_); |
| state.PauseTiming(); |
| data = &VariantSegmentBenchmarkData::instance(); |
| Status status = data->status(); |
| if (status.ok()) { |
| status = data->ensure_read_validation(layout, ReadTarget::WHOLE, &source); |
| } |
| if (status.ok()) { |
| status = data->prepare_rewrite_scan(source, version, &prepared); |
| } |
| if (status.ok()) { |
| destination_path = data->measured_segment_path(layout, version, state.name()); |
| status = data->delete_segment_files(destination_path); |
| } |
| state.ResumeTiming(); |
| if (!benchmark_status(state, status)) { |
| break; |
| } |
| |
| result = RewriteResult {}; |
| status = data->rewrite_segment(prepared, layout, version, destination_path, &result); |
| if (!benchmark_status(state, status)) { |
| break; |
| } |
| |
| state.PauseTiming(); |
| std::vector<uint64_t> destination_bytes {result.destination_segment_bytes}; |
| status = data->validate_written_segments(layout, version, destination_path, |
| destination_bytes, &destination_counts, nullptr); |
| state.ResumeTiming(); |
| if (!benchmark_status(state, status)) { |
| break; |
| } |
| completed = true; |
| } |
| if (!completed) { |
| return; |
| } |
| |
| add_common_counters(state, *data); |
| state.SetItemsProcessed(static_cast<int64_t>(result.rows) * state.iterations()); |
| state.SetBytesProcessed(static_cast<int64_t>(result.source_segment_bytes) * state.iterations()); |
| state.counters["scan_init_ms"] = result.scan_init_ns / 1e6; |
| state.counters["writer_init_ms"] = result.writer_init_ns / 1e6; |
| state.counters["read_ms"] = result.read_ns / 1e6; |
| state.counters["append_ms"] = result.append_ns / 1e6; |
| state.counters["finalize_ms"] = result.finalize_ns / 1e6; |
| state.counters["stage_sum_ms"] = (result.scan_init_ns + result.writer_init_ns + result.read_ns + |
| result.append_ns + result.finalize_ns) / |
| 1e6; |
| state.counters["read_output_bytes_per_row"] = |
| benchmark::Counter(static_cast<double>(result.read_output_bytes) / result.rows, |
| benchmark::Counter::kIsIterationInvariant); |
| state.counters["source_segment_bytes_per_row"] = |
| benchmark::Counter(static_cast<double>(result.source_segment_bytes) / result.rows, |
| benchmark::Counter::kIsIterationInvariant); |
| state.counters["destination_segment_bytes_per_row"] = |
| benchmark::Counter(static_cast<double>(result.destination_segment_bytes) / result.rows, |
| benchmark::Counter::kIsIterationInvariant); |
| state.counters["destination_index_bytes"] = result.destination_index_bytes; |
| state.counters["source_writer_variant_v1"] = 1; |
| state.counters["query_output_variant_v2"] = version == VariantVersion::V2; |
| state.counters["destination_input_variant_v2"] = version == VariantVersion::V2; |
| state.counters["destination_validation_row_count_full"] = 1; |
| state.counters["destination_validation_footer_layout_full"] = 1; |
| state.counters["destination_validation_canonical_sampled"] = 1; |
| state.counters["destination_validation_route_full_scan"] = 1; |
| state.counters["source_prevalidated"] = 1; |
| state.counters["source_page_cache"] = 1; |
| state.counters["variant_storage_parse_mode"] = config::variant_storage_parse_mode; |
| state.counters["route_hierarchical"] = |
| result.statistics.variant_subtree_hierarchical_iter_count; |
| state.counters["materialized_columns"] = destination_counts.materialized; |
| state.counters["sparse_columns"] = destination_counts.sparse; |
| state.counters["doc_columns"] = destination_counts.doc; |
| } |
| |
| void BM_VariantCumulativeCompaction(benchmark::State& state) { |
| CompactionResult result; |
| uint32_t total_rows = 0; |
| uint32_t rows_per_rowset = 0; |
| uint32_t input_segments = 0; |
| bool completed = false; |
| for (auto _ : state) { |
| benchmark::DoNotOptimize(_); |
| state.PauseTiming(); |
| auto fixture = std::make_unique<VariantCompactionBenchmarkFixture>(); |
| Status status = fixture->prepare(); |
| if (status.ok()) { |
| total_rows = fixture->total_rows(); |
| rows_per_rowset = fixture->rows_per_rowset(); |
| input_segments = fixture->input_segments(); |
| } |
| CumulativeCompaction* compaction = status.ok() ? fixture->compaction() : nullptr; |
| state.ResumeTiming(); |
| if (!benchmark_status(state, status)) { |
| break; |
| } |
| |
| status = compaction->execute_compact(); |
| |
| state.PauseTiming(); |
| if (status.ok()) { |
| status = fixture->validate(&result); |
| } |
| const bool ok = benchmark_status(state, status); |
| fixture.reset(); |
| state.ResumeTiming(); |
| if (!ok) { |
| break; |
| } |
| completed = true; |
| } |
| if (!completed) { |
| return; |
| } |
| |
| state.SetItemsProcessed(static_cast<int64_t>(total_rows) * state.iterations()); |
| state.SetBytesProcessed(static_cast<int64_t>(result.input_disk_bytes) * state.iterations()); |
| state.counters["rows_per_run"] = |
| benchmark::Counter(total_rows, benchmark::Counter::kIsIterationInvariant); |
| state.counters["input_rowsets"] = |
| benchmark::Counter(COMPACTION_INPUT_ROWSETS, benchmark::Counter::kIsIterationInvariant); |
| state.counters["input_segments"] = |
| benchmark::Counter(input_segments, benchmark::Counter::kIsIterationInvariant); |
| state.counters["rows_per_input_rowset"] = |
| benchmark::Counter(rows_per_rowset, benchmark::Counter::kIsIterationInvariant); |
| state.counters["merge_ways"] = |
| benchmark::Counter(COMPACTION_INPUT_ROWSETS, benchmark::Counter::kIsIterationInvariant); |
| state.counters["input_key_ranges_overlap"] = 1; |
| state.counters["input_json_bytes_per_row"] = |
| benchmark::Counter(static_cast<double>(result.input_json_bytes) / total_rows, |
| benchmark::Counter::kIsIterationInvariant); |
| state.counters["input_disk_bytes_per_row"] = |
| benchmark::Counter(static_cast<double>(result.input_disk_bytes) / total_rows, |
| benchmark::Counter::kIsIterationInvariant); |
| state.counters["output_disk_bytes_per_row"] = |
| benchmark::Counter(static_cast<double>(result.output_disk_bytes) / total_rows, |
| benchmark::Counter::kIsIterationInvariant); |
| state.counters["output_segments"] = result.output_segments; |
| state.counters["materialized_columns"] = result.output_layout.materialized; |
| state.counters["sparse_columns"] = result.output_layout.sparse; |
| state.counters["doc_columns"] = result.output_layout.doc; |
| state.counters["input_writer_variant_v2"] = 1; |
| state.counters["persisted_compaction_variant_v2"] = 0; |
| state.counters["ordered_link_fast_path"] = 0; |
| state.counters["vertical_compaction"] = 1; |
| state.counters["vertical_variant_subcolumns"] = 1; |
| state.counters["compaction_batch_size"] = config::compaction_batch_size; |
| state.counters["vertical_columns_per_group"] = |
| config::vertical_compaction_num_columns_per_group; |
| state.counters["fresh_input_fixture"] = 1; |
| state.counters["input_footers_prevalidated"] = 1; |
| state.counters["variant_storage_parse_mode"] = config::variant_storage_parse_mode; |
| } |
| |
| constexpr std::array<VariantVersion, 10> ABBA_ORDER { |
| VariantVersion::V1, VariantVersion::V2, VariantVersion::V2, VariantVersion::V1, |
| VariantVersion::V2, VariantVersion::V1, VariantVersion::V1, VariantVersion::V2, |
| VariantVersion::V1, VariantVersion::V2, |
| }; |
| |
| template <typename Register> |
| void register_abba_pair(std::string_view prefix, bool reverse, Register&& register_one) { |
| std::array<int, 2> sample {}; |
| for (VariantVersion ordered : ABBA_ORDER) { |
| VariantVersion version = ordered; |
| if (reverse) { |
| version = ordered == VariantVersion::V1 ? VariantVersion::V2 : VariantVersion::V1; |
| } |
| const auto version_index = static_cast<size_t>(version); |
| const std::string name = std::string(prefix) + "/sample" + |
| std::to_string(++sample[version_index]) + "_" + |
| std::string(version_name(version)); |
| register_one(name, version)->Unit(benchmark::kMillisecond)->Iterations(1)->UseRealTime(); |
| } |
| } |
| |
| bool register_variant_segment_benchmarks() { |
| const std::array<VariantLayout, 3> layouts {VariantLayout::SPARSE16, VariantLayout::DOC16, |
| VariantLayout::FULL}; |
| size_t pair_index = 0; |
| for (VariantLayout layout : layouts) { |
| const std::string prefix = |
| "BM_VariantIngestToSegment/" + std::string(layout_config(layout).name); |
| register_abba_pair(prefix, pair_index++ % 2 != 0, |
| [layout](const std::string& name, VariantVersion version) { |
| return benchmark::RegisterBenchmark( |
| name, [layout, version](benchmark::State& state) { |
| BM_VariantIngestToSegment(state, layout, version); |
| }); |
| }); |
| } |
| for (VariantLayout layout : layouts) { |
| const std::string prefix = |
| "BM_VariantReadWholeColumn/" + std::string(layout_config(layout).name); |
| register_abba_pair(prefix, pair_index++ % 2 != 0, |
| [layout](const std::string& name, VariantVersion version) { |
| return benchmark::RegisterBenchmark( |
| name, [layout, version](benchmark::State& state) { |
| BM_VariantRead(state, layout, ReadTarget::WHOLE, |
| version); |
| }); |
| }); |
| } |
| const std::array<ReadTarget, 3> exact_targets {ReadTarget::MATERIALIZED, ReadTarget::SPARSE, |
| ReadTarget::MISSING}; |
| for (ReadTarget target : exact_targets) { |
| const std::string prefix = |
| "BM_VariantReadExactPath/Sparse16/" + std::string(target_name(target)); |
| register_abba_pair(prefix, pair_index++ % 2 != 0, |
| [target](const std::string& name, VariantVersion version) { |
| return benchmark::RegisterBenchmark( |
| name, [target, version](benchmark::State& state) { |
| BM_VariantRead(state, VariantLayout::SPARSE16, target, |
| version); |
| }); |
| }); |
| } |
| for (VariantLayout layout : layouts) { |
| const std::string prefix = "BM_VariantScanAndRewriteSegment/" + |
| std::string(layout_config(layout).name) + "/V1WrittenSource"; |
| register_abba_pair(prefix, pair_index++ % 2 != 0, |
| [layout](const std::string& name, VariantVersion version) { |
| return benchmark::RegisterBenchmark( |
| name, [layout, version](benchmark::State& state) { |
| BM_VariantScanAndRewriteSegment(state, layout, version); |
| }); |
| }); |
| } |
| constexpr uint32_t COMPACTION_SAMPLES = 5; |
| for (uint32_t sample = 1; sample <= COMPACTION_SAMPLES; ++sample) { |
| const std::string name = |
| "BM_VariantCumulativeCompaction/Overlap10Way/FreshInput/" |
| "Sparse16/InputWrittenByV2/ProductionCurrent/sample" + |
| std::to_string(sample); |
| benchmark::RegisterBenchmark(name, BM_VariantCumulativeCompaction) |
| ->Unit(benchmark::kMillisecond) |
| ->Iterations(1) |
| ->UseRealTime(); |
| } |
| return true; |
| } |
| |
| inline const bool VARIANT_SEGMENT_BENCHMARKS_REGISTERED = register_variant_segment_benchmarks(); |
| |
| } // namespace |
| } // namespace doris::variant_segment_benchmark |