blob: d7de5a6b9bc41b7ae82db11ef38793c919323109 [file]
/*
* Parallel READ throughput benchmark.
*
* Measures the throughput of TsFileReader table-model batch reads with and
* without the column-parallel decode path.
*
* For each (compression, n_field_cols, thread_count) combination the benchmark
* runs two modes:
*
* serial — parallel_read_enabled = false
* parallel — parallel_read_enabled = true, read_thread_count = thread_count
*
* Encoding and compression are controlled via g_config_value_ (the ColumnSchema
* parameters are ignored by the writer — the global config is authoritative).
*
* Build (requires ENABLE_THREADS=ON):
* cmake -DENABLE_THREADS=ON -DBUILD_TEST=OFF -DCMAKE_BUILD_TYPE=Release ..
* cmake --build . --target read_thread_bench
*
* Run:
* ./read_thread_bench [total_rows] [batch_size] [csv_path]
*
* Defaults: total_rows=5000000, batch_size=65536,
* csv_path=read_thread_bench.csv
*/
#include <fcntl.h>
#include <unistd.h>
#include <chrono>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <fstream>
#include <iomanip>
#include <iostream>
#include <string>
#include <vector>
#include "common/global.h"
#include "common/schema.h"
#include "common/tablet.h"
#include "common/tsblock/tsblock.h"
#include "file/write_file.h"
#include "reader/result_set.h"
#include "reader/tsfile_reader.h"
#include "writer/tsfile_table_writer.h"
using namespace common;
using namespace storage;
static const char* kTable = "bench";
static const char* kTagName = "dev";
static const char* kTagVal = "d0";
// ---------------------------------------------------------------------------
// Write a test TsFile with the given number of FIELD columns
// ---------------------------------------------------------------------------
static void write_test_file(const std::string& path, int n_field_cols,
int64_t total_rows, int batch_size) {
std::vector<ColumnSchema> cols;
cols.emplace_back(kTagName, STRING, ColumnCategory::TAG);
for (int i = 0; i < n_field_cols; i++) {
cols.emplace_back("f" + std::to_string(i), INT64,
ColumnCategory::FIELD);
}
auto schema = std::make_shared<TableSchema>(std::string(kTable), cols);
WriteFile file;
int ret = file.create(path.c_str(), O_WRONLY | O_CREAT | O_TRUNC, 0666);
if (ret != 0) {
std::cerr << "create file failed: " << ret << "\n";
std::exit(1);
}
TsFileTableWriter writer(&file, schema.get(),
static_cast<uint64_t>(4) * 1024 * 1024 * 1024ULL);
std::vector<std::string> col_names;
std::vector<TSDataType> col_types;
std::vector<ColumnCategory> col_cats;
col_names.push_back(kTagName);
col_types.push_back(STRING);
col_cats.push_back(ColumnCategory::TAG);
for (int i = 0; i < n_field_cols; i++) {
col_names.push_back("f" + std::to_string(i));
col_types.push_back(INT64);
col_cats.push_back(ColumnCategory::FIELD);
}
Tablet tablet(kTable, col_names, col_types, col_cats, batch_size);
std::vector<int64_t> ts_arr(batch_size);
std::vector<int64_t> val_arr(batch_size);
// Pre-fill random values so decompression has real work to do
uint64_t rng = 0xdeadbeefcafe1234ULL;
for (int i = 0; i < batch_size; i++) {
rng ^= rng << 13;
rng ^= rng >> 7;
rng ^= rng << 17;
val_arr[i] = static_cast<int64_t>(rng);
}
int64_t rows_written = 0;
while (rows_written < total_rows) {
int cur_batch = static_cast<int>(
std::min((int64_t)batch_size, total_rows - rows_written));
for (int i = 0; i < cur_batch; i++) {
ts_arr[i] = rows_written + i;
}
tablet.set_timestamps(ts_arr.data(), cur_batch);
tablet.set_column_string_repeated(
0, kTagVal, static_cast<uint32_t>(strlen(kTagVal)), cur_batch);
for (int c = 1; c <= n_field_cols; c++) {
tablet.set_column_values(c, val_arr.data(), nullptr, cur_batch);
}
ret = writer.write_table(tablet);
if (ret != E_OK) {
std::cerr << "write_table failed: " << ret << "\n";
std::exit(1);
}
rows_written += cur_batch;
}
writer.flush();
writer.close();
}
// ---------------------------------------------------------------------------
// Result record
// ---------------------------------------------------------------------------
struct ReadResult {
std::string compression;
int n_field_cols;
int thread_count;
bool parallel;
int64_t total_rows;
int batch_size;
double wall_ms;
double rows_per_sec;
double values_per_sec; // rows * n_field_cols / sec
};
// ---------------------------------------------------------------------------
// Run one read configuration
// ---------------------------------------------------------------------------
static ReadResult run_read(const std::string& comp_name,
const std::string& path, int n_field_cols,
int thread_count, bool parallel, int64_t total_rows,
int batch_size) {
// Configure threading
g_config_value_.parallel_read_enabled_ = parallel;
g_config_value_.read_thread_count_ = thread_count;
// Build column name list for query
std::vector<std::string> col_names;
col_names.push_back(kTagName);
for (int i = 0; i < n_field_cols; i++) {
col_names.push_back("f" + std::to_string(i));
}
// Warm up: one read to populate OS page cache
{
TsFileReader reader;
reader.open(path);
ResultSet* rs = nullptr;
reader.query(kTable, col_names, 0, total_rows + 1, rs, batch_size);
if (rs) {
auto* trs = dynamic_cast<TableResultSet*>(rs);
TsBlock* blk = nullptr;
while (trs->get_next_tsblock(blk) == E_OK) {
}
reader.destroy_query_data_set(rs);
}
reader.close();
}
// Timed run
auto t0 = std::chrono::steady_clock::now();
TsFileReader reader;
reader.open(path);
ResultSet* rs = nullptr;
int ret =
reader.query(kTable, col_names, 0, total_rows + 1, rs, batch_size);
if (ret != E_OK || rs == nullptr) {
std::cerr << "query failed: " << ret << "\n";
std::exit(1);
}
auto* trs = dynamic_cast<TableResultSet*>(rs);
int64_t rows_read = 0;
TsBlock* blk = nullptr;
while (trs->get_next_tsblock(blk) == E_OK) {
rows_read += blk->get_row_count();
}
reader.destroy_query_data_set(rs);
reader.close();
auto t1 = std::chrono::steady_clock::now();
double wall_ms = std::chrono::duration<double, std::milli>(t1 - t0).count();
if (rows_read != total_rows) {
std::cerr << "WARNING: expected " << total_rows << " rows, got "
<< rows_read << "\n";
}
double sec = wall_ms / 1000.0;
ReadResult r;
r.compression = comp_name;
r.n_field_cols = n_field_cols;
r.thread_count = thread_count;
r.parallel = parallel;
r.total_rows = rows_read;
r.batch_size = batch_size;
r.wall_ms = wall_ms;
r.rows_per_sec = rows_read / sec;
r.values_per_sec = (double)rows_read * n_field_cols / sec;
return r;
}
// ---------------------------------------------------------------------------
// main
// ---------------------------------------------------------------------------
int main(int argc, char* argv[]) {
int64_t total_rows = 5000000;
int batch_size = 65536;
std::string csv_path = "read_thread_bench.csv";
if (argc > 1) total_rows = std::atoll(argv[1]);
if (argc > 2) batch_size = std::atoi(argv[2]);
if (argc > 3) csv_path = argv[3];
std::cout << "=== Parallel Read Benchmark ===\n"
<< " total_rows : " << total_rows << "\n"
<< " batch_size : " << batch_size << "\n"
<< " csv_output : " << csv_path << "\n\n";
libtsfile_init();
#ifndef ENABLE_THREADS
std::cout << "[WARNING] Built without ENABLE_THREADS — "
"parallel mode will fall back to serial.\n\n";
#endif
const std::vector<int> col_counts = {4, 8, 16};
const std::vector<int> thread_counts = {2, 4, 8};
const std::string tmp_path = "/tmp/_read_thread_bench.tsfile";
std::ofstream csv(csv_path);
if (!csv.is_open()) {
std::cerr << "cannot open csv: " << csv_path << "\n";
return 1;
}
csv << "n_field_cols,mode,thread_count,total_rows,batch_size,"
"wall_ms,rows_per_sec,values_per_sec\n";
std::cout << std::left << std::setw(8) << "cols" << std::setw(10) << "mode"
<< std::setw(9) << "threads" << std::setw(12) << "wall_ms"
<< std::setw(16) << "rows/sec" << std::setw(16) << "values/sec"
<< std::setw(10) << "speedup"
<< "\n"
<< std::string(81, '-') << "\n";
// Fixed: TS_2DIFF encoding + LZ4 compression
g_config_value_.int64_encoding_type_ = TSEncoding::TS_2DIFF;
g_config_value_.default_compression_type_ = LZ4;
for (int n_cols : col_counts) {
// Write test file for this column count
std::cout << "[Writing LZ4 " << n_cols << "-col file, " << total_rows
<< " rows...]\n";
write_test_file(tmp_path, n_cols, total_rows, 8192);
// Serial baseline
ReadResult serial_r =
run_read("LZ4", tmp_path, n_cols, 1, false, total_rows, batch_size);
double serial_ms = serial_r.wall_ms;
std::cout << std::left << std::setw(8) << n_cols << std::setw(10)
<< "serial" << std::setw(9) << 1 << std::setw(12)
<< std::fixed << std::setprecision(1) << serial_r.wall_ms
<< std::setw(16) << std::fixed << std::setprecision(0)
<< serial_r.rows_per_sec << std::setw(16) << std::fixed
<< std::setprecision(0) << serial_r.values_per_sec
<< std::setw(10) << "1.00x"
<< "\n";
csv << n_cols << ",serial,1," << total_rows << "," << batch_size << ","
<< serial_r.wall_ms << "," << serial_r.rows_per_sec << ","
<< serial_r.values_per_sec << "\n";
// Parallel with varying thread counts
for (int t : thread_counts) {
ReadResult r = run_read("LZ4", tmp_path, n_cols, t, true,
total_rows, batch_size);
double speedup = serial_ms / r.wall_ms;
std::ostringstream sp;
sp << std::fixed << std::setprecision(2) << speedup << "x";
std::cout << std::left << std::setw(8) << n_cols << std::setw(10)
<< "parallel" << std::setw(9) << t << std::setw(12)
<< std::fixed << std::setprecision(1) << r.wall_ms
<< std::setw(16) << std::fixed << std::setprecision(0)
<< r.rows_per_sec << std::setw(16) << std::fixed
<< std::setprecision(0) << r.values_per_sec
<< std::setw(10) << sp.str() << "\n";
csv << n_cols << ",parallel," << t << "," << total_rows << ","
<< batch_size << "," << r.wall_ms << "," << r.rows_per_sec
<< "," << r.values_per_sec << "\n";
}
std::cout << "\n";
csv.flush();
}
::unlink(tmp_path.c_str());
std::cout << "Done. Results written to " << csv_path << "\n";
libtsfile_destroy();
return 0;
}