| /* |
| * 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. |
| * |
| * Performance comparison (4 groups): |
| * - Tree model: query single time series / multi time series |
| * - Table model: query single time series / multi time series |
| * |
| * Dataset: |
| * - points per time series = 50000 (a time series = device_id + measurement_id) |
| * - offset default = 25000, limit default = 1000 |
| * - random "none" (NULL / missing values) to make the data non-dense |
| * |
| * Run (suite is DISABLED by default; omit DISABLED_ in filter when using |
| * --gtest_also_run_disabled_tests): |
| * TsFile_Test --gtest_also_run_disabled_tests |
| * --gtest_filter=DISABLED_QueryByRowPerformance* |
| * |
| * Dynamic offset (optional): |
| * QUERY_BY_ROW_PERF_OFFSET=<absolute offset> |
| * QUERY_BY_ROW_PERF_OFFSET_RATIO=<percent [0..100] of num_rows> |
| * |
| * Optional output aggregation: |
| * QUERY_BY_ROW_PERF_RESULT=/path/to/result.md (append) |
| * |
| * More iterations for stabler avg (default 5): |
| * QUERY_BY_ROW_PERF_ITERS=30 |
| */ |
| |
| #include <gtest/gtest.h> |
| |
| #include <algorithm> |
| #include <chrono> |
| #include <cstdlib> |
| #include <fstream> |
| #include <iostream> |
| #include <random> |
| #include <sstream> |
| #include <string> |
| #include <vector> |
| |
| #include "common/global.h" |
| #include "common/record.h" |
| #include "common/schema.h" |
| #include "common/tablet.h" |
| #include "file/write_file.h" |
| #include "reader/tsfile_reader.h" |
| #include "reader/tsfile_tree_reader.h" |
| #include "utils/util_define.h" |
| #include "writer/tsfile_table_writer.h" |
| #include "writer/tsfile_tree_writer.h" |
| |
| using namespace storage; |
| using namespace common; |
| |
| static bool get_env_int(const char* name, int& out) { |
| const char* v = std::getenv(name); |
| if (v == nullptr || v[0] == '\0') return false; |
| char* end = nullptr; |
| long x = std::strtol(v, &end, 10); |
| if (end == v) return false; |
| out = static_cast<int>(x); |
| return true; |
| } |
| |
| /** Average latency iterations per (offset,limit) case; clamp [1,200]. */ |
| static int query_by_row_perf_iters() { |
| int n = 100; |
| if (get_env_int("QUERY_BY_ROW_PERF_ITERS", n)) { |
| if (n < 1) n = 1; |
| if (n > 200) n = 200; |
| } |
| return n; |
| } |
| |
| MAYBE_UNUSED static int compute_offset_with_env(int num_rows, |
| int default_offset) { |
| int offset = default_offset; |
| int abs = 0; |
| if (get_env_int("QUERY_BY_ROW_PERF_OFFSET", abs)) { |
| offset = abs; |
| } else { |
| int ratio = 0; |
| if (get_env_int("QUERY_BY_ROW_PERF_OFFSET_RATIO", ratio)) { |
| if (ratio < 0) ratio = 0; |
| if (ratio > 100) ratio = 100; |
| offset = |
| static_cast<int>(static_cast<int64_t>(num_rows) * ratio / 100); |
| } |
| } |
| |
| if (num_rows <= 0) return 0; |
| if (offset < 0) offset = 0; |
| if (offset >= num_rows) offset = num_rows - 1; |
| return offset; |
| } |
| |
| static void write_result_if_needed(const std::string& md) { |
| const char* result_path = std::getenv("QUERY_BY_ROW_PERF_RESULT"); |
| if (result_path == nullptr || result_path[0] == '\0') return; |
| std::ofstream f(result_path, std::ios::app); |
| if (f) f << md << "\n"; |
| } |
| |
| // Entire suite skipped in default runs |
| class DISABLED_QueryByRowPerformanceTest : public ::testing::Test { |
| protected: |
| void SetUp() override { |
| libtsfile_init(); |
| file_name_ = std::string("query_by_row_perf_") + |
| generate_random_string(8) + std::string(".tsfile"); |
| remove(file_name_.c_str()); |
| int flags = O_WRONLY | O_CREAT | O_TRUNC; |
| #ifdef _WIN32 |
| flags |= O_BINARY; |
| #endif |
| mode_t mode = 0666; |
| write_file_.create(file_name_, flags, mode); |
| } |
| |
| void TearDown() override { |
| remove(file_name_.c_str()); |
| libtsfile_destroy(); |
| } |
| |
| static std::string generate_random_string(int length) { |
| std::random_device rd; |
| std::mt19937 gen(rd()); |
| std::uniform_int_distribution<> dis(0, 61); |
| const std::string chars = |
| "0123456789" |
| "abcdefghijklmnopqrstuvwxyz" |
| "ABCDEFGHIJKLMNOPQRSTUVWXYZ"; |
| std::string result; |
| for (int i = 0; i < length; ++i) { |
| result += chars[dis(gen)]; |
| } |
| return result; |
| } |
| |
| void write_tree_multi_device_file( |
| int num_rows_total, int device_count, |
| const std::vector<std::string>& measurement_ids, double none_prob_s1, |
| double none_prob_s2, uint32_t seed) { |
| TsFileTreeWriter writer(&write_file_); |
| |
| // Register all selected measurements (only those in measurement_ids). |
| std::vector<std::string> device_ids; |
| for (int d = 0; d < device_count; ++d) { |
| device_ids.push_back("d" + std::to_string(d)); |
| } |
| for (auto& device_id : device_ids) { |
| for (const auto& measurement_id : measurement_ids) { |
| auto* schema = |
| new MeasurementSchema(measurement_id, TSDataType::INT64); |
| std::string device_id_nc = |
| device_id; // register_timeseries needs std::string& |
| ASSERT_EQ(E_OK, |
| writer.register_timeseries(device_id_nc, schema)); |
| delete schema; |
| } |
| } |
| |
| std::mt19937 rng(seed); |
| std::uniform_real_distribution<double> u01(0.0, 1.0); |
| |
| // Ensure every (device_id + measurement_id) time series has exactly |
| // num_rows_total points at timestamps [0, num_rows_total). |
| for (int64_t timestamp = 0; timestamp < num_rows_total; ++timestamp) { |
| for (const auto& device_id : device_ids) { |
| TsRecord record(timestamp, device_id, |
| static_cast<int32_t>(measurement_ids.size())); |
| for (size_t m = 0; m < measurement_ids.size(); ++m) { |
| const auto& measurement_id = measurement_ids[m]; |
| bool make_null = false; |
| if (measurement_id == "s1") { |
| make_null = (u01(rng) < none_prob_s1); |
| } else if (measurement_id == "s2") { |
| make_null = (u01(rng) < none_prob_s2); |
| } |
| |
| if (make_null) { |
| // DataPoint(measurement_name) creates a NULL point. |
| record.points_.emplace_back(DataPoint(measurement_id)); |
| } else { |
| // Make values unique per (timestamp, measurement) to |
| // prevent constant folding. |
| int64_t value = |
| timestamp * 100 + static_cast<int64_t>(m); |
| record.add_point(measurement_id, value); |
| } |
| } |
| |
| ASSERT_EQ(E_OK, writer.write(record)); |
| } |
| } |
| writer.flush(); |
| writer.close(); |
| } |
| |
| void write_table_multi_device_file(int num_rows_total, int device_count, |
| double none_prob_s1, double none_prob_s2, |
| uint32_t seed) { |
| // Schema always contains both s1 and s2; single-sequence tests will |
| // only query s1. |
| std::vector<ColumnSchema> col_schemas = { |
| ColumnSchema("id1", TSDataType::STRING, |
| CompressionType::UNCOMPRESSED, TSEncoding::PLAIN, |
| ColumnCategory::TAG), |
| ColumnSchema("s1", TSDataType::INT64, CompressionType::UNCOMPRESSED, |
| TSEncoding::PLAIN, ColumnCategory::FIELD), |
| ColumnSchema("s2", TSDataType::INT64, CompressionType::UNCOMPRESSED, |
| TSEncoding::PLAIN, ColumnCategory::FIELD), |
| }; |
| auto* schema = new TableSchema("t1", col_schemas); |
| auto* writer = new TsFileTableWriter(&write_file_, schema); |
| |
| // Each device has num_rows_total points, and timestamps are aligned |
| // across devices. |
| const int num_total_rows = num_rows_total * device_count; |
| Tablet tablet( |
| "t1", {"id1", "s1", "s2"}, |
| {TSDataType::STRING, TSDataType::INT64, TSDataType::INT64}, |
| {ColumnCategory::TAG, ColumnCategory::FIELD, ColumnCategory::FIELD}, |
| num_total_rows); |
| std::mt19937 rng(seed); |
| std::uniform_real_distribution<double> u01(0.0, 1.0); |
| |
| for (int device_index = 0; device_index < device_count; |
| ++device_index) { |
| for (int row_in_device = 0; row_in_device < num_rows_total; |
| ++row_in_device) { |
| const int row = device_index * num_rows_total + row_in_device; |
| const int64_t timestamp = static_cast<int64_t>(row_in_device); |
| |
| tablet.add_timestamp(row, timestamp); |
| tablet.add_value(row, "id1", |
| "device_" + std::to_string(device_index)); |
| |
| if (u01(rng) >= none_prob_s1) { |
| tablet.add_value(row, "s1", timestamp * 10); |
| } |
| if (u01(rng) >= none_prob_s2) { |
| tablet.add_value(row, "s2", timestamp * 100); |
| } |
| } |
| } |
| |
| ASSERT_EQ(writer->write_table(tablet), E_OK); |
| ASSERT_EQ(writer->flush(), E_OK); |
| ASSERT_EQ(writer->close(), E_OK); |
| delete writer; |
| delete schema; |
| } |
| |
| std::string file_name_; |
| WriteFile write_file_; |
| }; |
| |
| static const int kNumRowsTotal = 50000; // points per time series |
| static const int kDeviceCount = |
| 1; // keep "offset/limit" aligned to a single time series |
| |
| struct OffsetLimitCase { |
| int offset; |
| int limit; |
| const char* label; |
| }; |
| |
| // Multiple (offset, limit) groups for performance analysis. |
| // Notes: |
| // - The dataset has offsets in [0, kNumRowsTotal-1]. (50000, 1000) means |
| // "offset beyond end" and should return empty results for that series. |
| static const OffsetLimitCase kOffsetLimitCases[] = { |
| {0, 1000, "(0,1000)"}, {12500, 1000, "(12500,1000)"}, |
| {25000, 1000, "(25000,1000)"}, {37500, 1000, "(37500,1000)"}, |
| {50000, 1000, "(50000,1000)"}, {25000, 10, "(25000,10)"}, |
| {25000, 100, "(25000,100)"}, {25000, 1000, "(25000,1000)"}, |
| {25000, 10000, "(25000,10000)"}, |
| }; |
| |
| static const int kOffsetLimitCaseCount = |
| static_cast<int>(sizeof(kOffsetLimitCases) / sizeof(kOffsetLimitCases[0])); |
| |
| // Keep dataset fully dense (no NULL) to maximize offset/limit pushdown effects. |
| static const double kNoneProbSingle = 0.0; // s1 in single-sequence |
| static const double kNoneProbS2 = 0.0; // s2 in multi-sequence |
| static const double kNoneProbMultiS1 = 0.0; // s1 in multi-sequence |
| |
| template <typename RunByRowFn, typename RunManualFn> |
| static void compute_avg_times(RunByRowFn&& run_by_row, RunManualFn&& run_manual, |
| int iters, double& avg_by_row, double& avg_manual, |
| int& valid_iters) { |
| double sum_by_row = 0.0; |
| double sum_manual = 0.0; |
| valid_iters = 0; |
| for (int i = 0; i < iters; i++) { |
| const double t1 = run_by_row(); |
| const double t2 = run_manual(); |
| if (t1 > 0 && t2 > 0) { |
| sum_by_row += t1; |
| sum_manual += t2; |
| valid_iters++; |
| } |
| } |
| avg_by_row = (valid_iters > 0) ? (sum_by_row / valid_iters) : -1.0; |
| avg_manual = (valid_iters > 0) ? (sum_manual / valid_iters) : -1.0; |
| } |
| |
| TEST_F(DISABLED_QueryByRowPerformanceTest, TreeModel_SingleSequence) { |
| const std::vector<std::string> measurement_ids = {"s1"}; |
| write_tree_multi_device_file(kNumRowsTotal, kDeviceCount, measurement_ids, |
| kNoneProbSingle, /*none_prob_s2=*/0.0, 123); |
| std::vector<std::string> devices{"d0"}; |
| |
| const int perf_iters = query_by_row_perf_iters(); |
| std::ostringstream out; |
| out << "# QueryByRow (Tree, single) vs Manual Next – Performance Result\n\n" |
| << "Avg iterations per cell: **" << perf_iters |
| << "** (`QUERY_BY_ROW_PERF_ITERS`)\n\n" |
| << "| Case | Offset | Limit | queryByRow(avg ms) | Manual(avg ms) | " |
| "Speedup |\n" |
| << "|------|--------|-------|-------------------|--------------|-------" |
| "--|\n"; |
| |
| double best_speedup = 0.0; |
| for (int c = 0; c < kOffsetLimitCaseCount; ++c) { |
| const OffsetLimitCase& cs = kOffsetLimitCases[c]; |
| const int offset = cs.offset; |
| const int limit = cs.limit; |
| |
| auto run_query_by_row = [this, &devices, &measurement_ids, offset, |
| limit]() { |
| TsFileTreeReader reader; |
| if (reader.open(file_name_) != E_OK) return -1.0; |
| ResultSet* rs = nullptr; |
| if (reader.queryByRow(devices, measurement_ids, offset, limit, |
| rs) != E_OK) { |
| reader.close(); |
| return -1.0; |
| } |
| auto start = std::chrono::steady_clock::now(); |
| bool has_next = false; |
| while (IS_SUCC(rs->next(has_next)) && has_next) { |
| (void)rs->get_row_record()->get_timestamp(); |
| } |
| auto end = std::chrono::steady_clock::now(); |
| reader.destroy_query_data_set(rs); |
| reader.close(); |
| return std::chrono::duration<double, std::milli>(end - start) |
| .count(); |
| }; |
| |
| auto run_manual_next = [this, &devices, &measurement_ids, offset, |
| limit]() { |
| TsFileTreeReader reader; |
| if (reader.open(file_name_) != E_OK) return -1.0; |
| ResultSet* rs = nullptr; |
| if (reader.query(devices, measurement_ids, INT64_MIN, INT64_MAX, |
| rs) != E_OK) { |
| reader.close(); |
| return -1.0; |
| } |
| auto start = std::chrono::steady_clock::now(); |
| bool has_next = false; |
| int skipped = 0; |
| int taken = 0; |
| while (IS_SUCC(rs->next(has_next)) && has_next) { |
| if (skipped < offset) { |
| skipped++; |
| continue; |
| } |
| if (taken >= limit) break; |
| (void)rs->get_row_record()->get_timestamp(); |
| taken++; |
| } |
| auto end = std::chrono::steady_clock::now(); |
| reader.destroy_query_data_set(rs); |
| reader.close(); |
| return std::chrono::duration<double, std::milli>(end - start) |
| .count(); |
| }; |
| |
| double avg_by_row = -1.0; |
| double avg_manual = -1.0; |
| int valid_iters = 0; |
| compute_avg_times(run_query_by_row, run_manual_next, perf_iters, |
| avg_by_row, avg_manual, valid_iters); |
| ASSERT_GT(valid_iters, 0); |
| ASSERT_GT(avg_manual, 0.0); |
| |
| const double speedup = |
| (avg_by_row > 0.0) ? (avg_manual / avg_by_row) : 0.0; |
| best_speedup = std::max(best_speedup, speedup); |
| |
| out << "| " << cs.label << " | " << offset << " | " << limit << " | " |
| << avg_by_row << " | " << avg_manual << " | " << speedup << "x |\n"; |
| } |
| |
| out << "\n"; |
| std::cout << "\n" << out.str() << "\n"; |
| write_result_if_needed(out.str()); |
| EXPECT_GT(best_speedup, 1.0); |
| } |
| |
| TEST_F(DISABLED_QueryByRowPerformanceTest, TreeModel_MultiSequence) { |
| const std::vector<std::string> measurement_ids = {"s1", "s2"}; |
| write_tree_multi_device_file(kNumRowsTotal, kDeviceCount, measurement_ids, |
| kNoneProbMultiS1, kNoneProbS2, 456); |
| std::vector<std::string> devices; |
| for (int d = 0; d < kDeviceCount; ++d) |
| devices.push_back("d" + std::to_string(d)); |
| |
| const int perf_iters = query_by_row_perf_iters(); |
| std::ostringstream out; |
| out << "# QueryByRow (Tree, multi) vs Manual Next – Performance Result\n\n" |
| << "Avg iterations per cell: **" << perf_iters |
| << "** (`QUERY_BY_ROW_PERF_ITERS`)\n\n" |
| << "| Case | Offset | Limit | queryByRow(avg ms) | Manual(avg ms) | " |
| "Speedup |\n" |
| << "|------|--------|-------|-------------------|--------------|-------" |
| "--|\n"; |
| |
| double best_speedup = 0.0; |
| for (int c = 0; c < kOffsetLimitCaseCount; ++c) { |
| const OffsetLimitCase& cs = kOffsetLimitCases[c]; |
| const int offset = cs.offset; |
| const int limit = cs.limit; |
| |
| auto run_query_by_row = [this, &devices, &measurement_ids, offset, |
| limit]() { |
| TsFileTreeReader reader; |
| if (reader.open(file_name_) != E_OK) return -1.0; |
| ResultSet* rs = nullptr; |
| if (reader.queryByRow(devices, measurement_ids, offset, limit, |
| rs) != E_OK) { |
| reader.close(); |
| return -1.0; |
| } |
| auto start = std::chrono::steady_clock::now(); |
| bool has_next = false; |
| while (IS_SUCC(rs->next(has_next)) && has_next) { |
| (void)rs->get_row_record()->get_timestamp(); |
| } |
| auto end = std::chrono::steady_clock::now(); |
| reader.destroy_query_data_set(rs); |
| reader.close(); |
| return std::chrono::duration<double, std::milli>(end - start) |
| .count(); |
| }; |
| |
| auto run_manual_next = [this, &devices, &measurement_ids, offset, |
| limit]() { |
| TsFileTreeReader reader; |
| if (reader.open(file_name_) != E_OK) return -1.0; |
| ResultSet* rs = nullptr; |
| if (reader.query(devices, measurement_ids, INT64_MIN, INT64_MAX, |
| rs) != E_OK) { |
| reader.close(); |
| return -1.0; |
| } |
| auto start = std::chrono::steady_clock::now(); |
| bool has_next = false; |
| int skipped = 0; |
| int taken = 0; |
| while (IS_SUCC(rs->next(has_next)) && has_next) { |
| if (skipped < offset) { |
| skipped++; |
| continue; |
| } |
| if (taken >= limit) break; |
| (void)rs->get_row_record()->get_timestamp(); |
| taken++; |
| } |
| auto end = std::chrono::steady_clock::now(); |
| reader.destroy_query_data_set(rs); |
| reader.close(); |
| return std::chrono::duration<double, std::milli>(end - start) |
| .count(); |
| }; |
| |
| double avg_by_row = -1.0; |
| double avg_manual = -1.0; |
| int valid_iters = 0; |
| compute_avg_times(run_query_by_row, run_manual_next, perf_iters, |
| avg_by_row, avg_manual, valid_iters); |
| ASSERT_GT(valid_iters, 0); |
| ASSERT_GT(avg_manual, 0.0); |
| |
| const double speedup = |
| (avg_by_row > 0.0) ? (avg_manual / avg_by_row) : 0.0; |
| best_speedup = std::max(best_speedup, speedup); |
| |
| out << "| " << cs.label << " | " << offset << " | " << limit << " | " |
| << avg_by_row << " | " << avg_manual << " | " << speedup << "x |\n"; |
| } |
| |
| out << "\n"; |
| std::cout << "\n" << out.str() << "\n"; |
| write_result_if_needed(out.str()); |
| EXPECT_GT(best_speedup, 1.0); |
| } |
| |
| TEST_F(DISABLED_QueryByRowPerformanceTest, TableModel_SingleSequence) { |
| write_table_multi_device_file(kNumRowsTotal, kDeviceCount, kNoneProbSingle, |
| 0.0, 789); |
| const std::vector<std::string> cols = {"id1", "s1"}; |
| |
| const int perf_iters = query_by_row_perf_iters(); |
| std::ostringstream out; |
| out << "# QueryByRow (Table, single) vs Manual Next – Performance " |
| "Result\n\n" |
| << "Avg iterations per cell: **" << perf_iters |
| << "** (`QUERY_BY_ROW_PERF_ITERS`)\n\n" |
| << "| Case | Offset | Limit | queryByRow(avg ms) | Manual(avg ms) | " |
| "Speedup |\n" |
| << "|------|--------|-------|-------------------|--------------|-------" |
| "--|\n"; |
| |
| double best_speedup = 0.0; |
| for (int c = 0; c < kOffsetLimitCaseCount; ++c) { |
| const OffsetLimitCase& cs = kOffsetLimitCases[c]; |
| const int offset = cs.offset; |
| const int limit = cs.limit; |
| |
| auto run_query_by_row = [this, cols, offset, limit]() { |
| TsFileReader reader; |
| if (reader.open(file_name_) != E_OK) return -1.0; |
| ResultSet* rs = nullptr; |
| if (reader.queryByRow("t1", cols, offset, limit, rs) != E_OK) { |
| reader.close(); |
| return -1.0; |
| } |
| auto start = std::chrono::steady_clock::now(); |
| bool has_next = false; |
| while (IS_SUCC(rs->next(has_next)) && has_next) { |
| if (!rs->is_null("s1")) { |
| (void)rs->get_value<int64_t>("s1"); |
| } |
| } |
| auto end = std::chrono::steady_clock::now(); |
| reader.destroy_query_data_set(rs); |
| reader.close(); |
| return std::chrono::duration<double, std::milli>(end - start) |
| .count(); |
| }; |
| |
| auto run_manual_next = [this, cols, offset, limit]() { |
| TsFileReader reader; |
| if (reader.open(file_name_) != E_OK) return -1.0; |
| ResultSet* rs = nullptr; |
| if (reader.query("t1", cols, INT64_MIN, INT64_MAX, rs) != E_OK) { |
| reader.close(); |
| return -1.0; |
| } |
| auto start = std::chrono::steady_clock::now(); |
| bool has_next = false; |
| int skipped = 0; |
| int taken = 0; |
| while (IS_SUCC(rs->next(has_next)) && has_next) { |
| if (skipped < offset) { |
| skipped++; |
| continue; |
| } |
| if (taken >= limit) break; |
| if (!rs->is_null("s1")) { |
| (void)rs->get_value<int64_t>("s1"); |
| } |
| taken++; |
| } |
| auto end = std::chrono::steady_clock::now(); |
| reader.destroy_query_data_set(rs); |
| reader.close(); |
| return std::chrono::duration<double, std::milli>(end - start) |
| .count(); |
| }; |
| |
| double avg_by_row = -1.0; |
| double avg_manual = -1.0; |
| int valid_iters = 0; |
| compute_avg_times(run_query_by_row, run_manual_next, perf_iters, |
| avg_by_row, avg_manual, valid_iters); |
| ASSERT_GT(valid_iters, 0); |
| ASSERT_GT(avg_manual, 0.0); |
| |
| const double speedup = |
| (avg_by_row > 0.0) ? (avg_manual / avg_by_row) : 0.0; |
| best_speedup = std::max(best_speedup, speedup); |
| |
| out << "| " << cs.label << " | " << offset << " | " << limit << " | " |
| << avg_by_row << " | " << avg_manual << " | " << speedup << "x |\n"; |
| } |
| |
| out << "\n"; |
| std::cout << "\n" << out.str() << "\n"; |
| write_result_if_needed(out.str()); |
| EXPECT_GT(best_speedup, 1.0); |
| } |
| |
| TEST_F(DISABLED_QueryByRowPerformanceTest, TableModel_MultiSequence) { |
| write_table_multi_device_file(kNumRowsTotal, kDeviceCount, kNoneProbMultiS1, |
| kNoneProbS2, 101); |
| const std::vector<std::string> cols = {"id1", "s1", "s2"}; |
| |
| const int perf_iters = query_by_row_perf_iters(); |
| std::ostringstream out; |
| out << "# QueryByRow (Table, multi) vs Manual Next – Performance Result\n\n" |
| << "Avg iterations per cell: **" << perf_iters |
| << "** (`QUERY_BY_ROW_PERF_ITERS`)\n\n" |
| << "| Case | Offset | Limit | queryByRow(avg ms) | Manual(avg ms) | " |
| "Speedup |\n" |
| << "|------|--------|-------|-------------------|--------------|-------" |
| "--|\n"; |
| |
| double best_speedup = 0.0; |
| for (int c = 0; c < kOffsetLimitCaseCount; ++c) { |
| const OffsetLimitCase& cs = kOffsetLimitCases[c]; |
| const int offset = cs.offset; |
| const int limit = cs.limit; |
| |
| auto run_query_by_row = [this, cols, offset, limit]() { |
| TsFileReader reader; |
| if (reader.open(file_name_) != E_OK) return -1.0; |
| ResultSet* rs = nullptr; |
| if (reader.queryByRow("t1", cols, offset, limit, rs) != E_OK) { |
| reader.close(); |
| return -1.0; |
| } |
| auto start = std::chrono::steady_clock::now(); |
| bool has_next = false; |
| while (IS_SUCC(rs->next(has_next)) && has_next) { |
| if (!rs->is_null("s1")) (void)rs->get_value<int64_t>("s1"); |
| (void)rs->is_null("s2"); |
| } |
| auto end = std::chrono::steady_clock::now(); |
| reader.destroy_query_data_set(rs); |
| reader.close(); |
| return std::chrono::duration<double, std::milli>(end - start) |
| .count(); |
| }; |
| |
| auto run_manual_next = [this, cols, offset, limit]() { |
| TsFileReader reader; |
| if (reader.open(file_name_) != E_OK) return -1.0; |
| ResultSet* rs = nullptr; |
| if (reader.query("t1", cols, INT64_MIN, INT64_MAX, rs) != E_OK) { |
| reader.close(); |
| return -1.0; |
| } |
| auto start = std::chrono::steady_clock::now(); |
| bool has_next = false; |
| int skipped = 0; |
| int taken = 0; |
| while (IS_SUCC(rs->next(has_next)) && has_next) { |
| if (skipped < offset) { |
| skipped++; |
| continue; |
| } |
| if (taken >= limit) break; |
| if (!rs->is_null("s1")) (void)rs->get_value<int64_t>("s1"); |
| (void)rs->is_null("s2"); |
| taken++; |
| } |
| auto end = std::chrono::steady_clock::now(); |
| reader.destroy_query_data_set(rs); |
| reader.close(); |
| return std::chrono::duration<double, std::milli>(end - start) |
| .count(); |
| }; |
| |
| double avg_by_row = -1.0; |
| double avg_manual = -1.0; |
| int valid_iters = 0; |
| compute_avg_times(run_query_by_row, run_manual_next, perf_iters, |
| avg_by_row, avg_manual, valid_iters); |
| ASSERT_GT(valid_iters, 0); |
| ASSERT_GT(avg_manual, 0.0); |
| |
| const double speedup = |
| (avg_by_row > 0.0) ? (avg_manual / avg_by_row) : 0.0; |
| best_speedup = std::max(best_speedup, speedup); |
| |
| out << "| " << cs.label << " | " << offset << " | " << limit << " | " |
| << avg_by_row << " | " << avg_manual << " | " << speedup << "x |\n"; |
| } |
| |
| out << "\n"; |
| std::cout << "\n" << out.str() << "\n"; |
| write_result_if_needed(out.str()); |
| EXPECT_GT(best_speedup, 1.0); |
| } |