1use std::collections::{HashMap, HashSet};
16use std::fmt;
17use std::path::PathBuf;
18use std::sync::Arc;
19use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
20
21use clap::Parser;
22use colored::Colorize;
23use common_base::Plugins;
24use common_error::ext::{BoxedError, PlainError};
25use common_error::status_code::StatusCode;
26use common_meta::cache::{new_schema_cache, new_table_schema_cache};
27use common_meta::key::SchemaMetadataManager;
28use common_meta::kv_backend::memory::MemoryKvBackend;
29use common_wal::config::DatanodeWalConfig;
30use datafusion::execution::SessionStateBuilder;
31use datafusion::logical_expr::{BinaryExpr, Expr as DfExpr, ExprSchemable, Operator};
32use datafusion_common::tree_node::{Transformed, TreeNodeRewriter};
33use datafusion_common::{DFSchemaRef, ScalarValue, ToDFSchema};
34use datafusion_physical_plan::metrics::ExecutionPlanMetricsSet;
35use datafusion_physical_plan::{DisplayAs, DisplayFormatType};
36use datatypes::arrow::compute;
37use futures::StreamExt;
38use futures::stream::FuturesUnordered;
39use log_store::kafka::log_store::KafkaLogStore;
40use log_store::noop::log_store::NoopLogStore;
41use log_store::raft_engine::log_store::RaftEngineLogStore;
42use mito2::config::MitoConfig;
43use mito2::engine::MitoEngine;
44use mito2::sst::file_ref::FileReferenceManager;
45use moka::future::CacheBuilder;
46use object_store::manager::ObjectStoreManager;
47use object_store::util::normalize_dir;
48use query::optimizer::parallelize_scan::ParallelizeScan;
49use serde::{Deserialize, Serialize};
50use snafu::{OptionExt, ResultExt};
51use sqlparser::ast::ExprWithAlias as SqlExprWithAlias;
52use sqlparser::dialect::GenericDialect;
53use sqlparser::parser::Parser as SqlParser;
54use store_api::metadata::RegionMetadata;
55use store_api::path_utils::WAL_DIR;
56use store_api::region_engine::{PrepareRequest, QueryScanContext, RegionEngine};
57use store_api::region_request::{RegionOpenRequest, RegionRequest};
58use store_api::storage::{RegionId, ScanRequest, TimeSeriesDistribution, TimeSeriesRowSelector};
59use tokio::fs;
60
61use crate::datanode::tool_util::{
62 build_object_store, format_bytes, parse_config, parse_path_type, parse_region_id,
63};
64use crate::error;
65
66struct VerboseScannerDisplay<'a, T: ?Sized>(&'a T);
68
69impl<T: DisplayAs + ?Sized> fmt::Display for VerboseScannerDisplay<'_, T> {
70 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
71 self.0.fmt_as(DisplayFormatType::Verbose, f)
72 }
73}
74
75struct PartitionScanStats {
76 partition: usize,
77 rows: u64,
78 batches: u64,
79 array_mem_size: u64,
80 estimated_size: u64,
81 first_batch_elapsed: Option<Duration>,
82 elapsed: Duration,
83}
84
85#[derive(Debug, Parser)]
87pub struct ScanbenchCommand {
88 #[clap(long, value_name = "FILE")]
90 config: PathBuf,
91
92 #[clap(long)]
94 region_id: String,
95
96 #[clap(long)]
98 table_dir: String,
99
100 #[clap(long, default_value = "seq")]
102 scanner: String,
103
104 #[clap(long, value_name = "FILE", conflicts_with = "scan_configs")]
106 scan_config: Option<PathBuf>,
107
108 #[clap(long, value_name = "FILE", conflicts_with = "scan_config")]
110 scan_configs: Option<PathBuf>,
111
112 #[clap(long, default_value = "1")]
114 parallelism: usize,
115
116 #[clap(long, default_value = "1")]
118 iterations: usize,
119
120 #[clap(long, default_value = "bare")]
122 path_type: String,
123
124 #[clap(short, long, default_value_t = false)]
126 verbose: bool,
127
128 #[clap(long, value_name = "FILE")]
130 pprof_file: Option<PathBuf>,
131
132 #[clap(long, value_name = "FILE")]
134 result_file: Option<PathBuf>,
135
136 #[clap(long, default_value_t = false)]
138 enable_wal: bool,
139
140 #[clap(long, default_value_t = false)]
142 pprof_after_warmup: bool,
143}
144
145#[derive(Debug, Clone, Deserialize, Default)]
147struct ScanConfig {
148 name: Option<String>,
149 projection: Option<Vec<usize>>,
150 projection_names: Option<Vec<String>>,
151 filters: Option<Vec<String>>,
152 series_row_selector: Option<String>,
153}
154
155struct ScanConfigSet {
156 configs: Vec<ScanConfig>,
157 is_suite: bool,
158}
159
160impl ScanConfigSet {
161 fn run_count(&self, iterations: usize) -> usize {
162 if self.is_suite {
163 self.configs.len()
164 } else {
165 iterations
166 }
167 }
168
169 fn query_index(&self, iteration: usize) -> usize {
170 if self.is_suite { iteration } else { 0 }
171 }
172}
173
174struct ResolvedScanConfig {
175 name: String,
176 projection: Option<Vec<usize>>,
177 filters: Vec<DfExpr>,
178 series_row_selector: Option<TimeSeriesRowSelector>,
179}
180
181struct QueryRunSummary {
182 name: String,
183 runs: u64,
184 total_rows: u64,
185 total_elapsed: Duration,
186}
187
188impl QueryRunSummary {
189 fn new(name: String) -> Self {
190 Self {
191 name,
192 runs: 0,
193 total_rows: 0,
194 total_elapsed: Duration::ZERO,
195 }
196 }
197
198 fn record(&mut self, rows: u64, elapsed: Duration) {
199 self.runs += 1;
200 self.total_rows += rows;
201 self.total_elapsed += elapsed;
202 }
203
204 fn mean_rows(&self) -> u64 {
205 self.total_rows.checked_div(self.runs).unwrap_or_default()
206 }
207
208 fn mean_elapsed(&self) -> Duration {
209 self.total_elapsed
210 .checked_div(self.runs as u32)
211 .unwrap_or_default()
212 }
213}
214
215const RESULT_FORMAT_VERSION: u32 = 1;
216
217#[derive(Debug, Serialize)]
218struct ScanbenchResult {
219 format_version: u32,
220 started_at_unix_ms: u64,
221 benchmark: BenchmarkMetadata,
222 runs: Vec<ScanRunResult>,
223 summary: BenchmarkResultSummary,
224}
225
226#[derive(Debug, Serialize)]
227struct BenchmarkMetadata {
228 scanner: String,
229 region_id: String,
230 region_id_u64: u64,
231 table_dir: String,
232 path_type: String,
233 parallelism: usize,
234 enable_wal: bool,
235 config_mode: String,
236 run_count: usize,
237}
238
239#[derive(Debug, Serialize)]
240struct NormalizedScanConfig {
241 name: String,
242 projection: Option<Vec<usize>>,
243 filters: Vec<String>,
244 series_row_selector: Option<String>,
245}
246
247#[derive(Debug, Serialize)]
248struct ScanRunResult {
249 iteration: usize,
250 query_index: usize,
251 name: String,
252 config: NormalizedScanConfig,
253 rows: u64,
254 batches: u64,
255 setup_elapsed_ns: u64,
256 scan_elapsed_ns: u64,
257 elapsed_ns: u64,
258 array_mem_size_bytes: u64,
259 estimated_size_bytes: u64,
260 partitions: Vec<PartitionResult>,
261 scanner_explain: String,
262}
263
264#[derive(Debug, Serialize)]
265struct PartitionResult {
266 partition: usize,
267 rows: u64,
268 batches: u64,
269 array_mem_size_bytes: u64,
270 estimated_size_bytes: u64,
271 first_batch_elapsed_ns: Option<u64>,
272 elapsed_ns: u64,
273}
274
275#[derive(Debug, Serialize)]
276struct BenchmarkResultSummary {
277 runs: u64,
278 total_rows: u64,
279 total_elapsed_ns: u64,
280 mean_rows: u64,
281 mean_elapsed_ns: u64,
282 queries: Vec<QueryResultSummary>,
283}
284
285#[derive(Debug, Serialize)]
286struct QueryResultSummary {
287 query_index: usize,
288 name: String,
289 runs: u64,
290 total_rows: u64,
291 total_elapsed_ns: u64,
292 mean_rows: u64,
293 mean_elapsed_ns: u64,
294}
295
296fn duration_ns(duration: Duration) -> u64 {
297 duration.as_nanos().try_into().unwrap_or(u64::MAX)
298}
299
300fn started_at_unix_ms() -> u64 {
301 SystemTime::now()
302 .duration_since(UNIX_EPOCH)
303 .unwrap_or_default()
304 .as_millis()
305 .try_into()
306 .unwrap_or(u64::MAX)
307}
308
309fn result_summary(
310 total_rows: u64,
311 total_elapsed: Duration,
312 summaries: &[QueryRunSummary],
313) -> BenchmarkResultSummary {
314 let runs = summaries.iter().map(|summary| summary.runs).sum::<u64>();
315 BenchmarkResultSummary {
316 runs,
317 total_rows,
318 total_elapsed_ns: duration_ns(total_elapsed),
319 mean_rows: total_rows.checked_div(runs).unwrap_or_default(),
320 mean_elapsed_ns: duration_ns(total_elapsed.checked_div(runs as u32).unwrap_or_default()),
321 queries: summaries
322 .iter()
323 .enumerate()
324 .map(|(index, summary)| QueryResultSummary {
325 query_index: index + 1,
326 name: summary.name.clone(),
327 runs: summary.runs,
328 total_rows: summary.total_rows,
329 total_elapsed_ns: duration_ns(summary.total_elapsed),
330 mean_rows: summary.mean_rows(),
331 mean_elapsed_ns: duration_ns(summary.mean_elapsed()),
332 })
333 .collect(),
334 }
335}
336
337async fn write_result_file(path: &PathBuf, result: &ScanbenchResult) -> error::Result<()> {
338 let content = serde_json::to_vec_pretty(result).context(error::SerdeJsonSnafu)?;
339 fs::write(path, content).await.context(error::FileIoSnafu)
340}
341
342fn validate_scan_config_suite(
343 mut configs: Vec<ScanConfig>,
344 iterations: usize,
345) -> error::Result<Vec<ScanConfig>> {
346 if iterations != 1 {
347 return Err(error::IllegalConfigSnafu {
348 msg: format!(
349 "--iterations cannot be used with --scan-configs (got {iterations}); the array length defines the run count"
350 ),
351 }
352 .build());
353 }
354 if configs.is_empty() {
355 return Err(error::IllegalConfigSnafu {
356 msg: "--scan-configs file must contain at least one scan config".to_string(),
357 }
358 .build());
359 }
360
361 let mut names = HashSet::with_capacity(configs.len());
362 for (index, config) in configs.iter_mut().enumerate() {
363 let name = match config.name.take() {
364 Some(name) => {
365 let name = name.trim();
366 if name.is_empty() {
367 return Err(error::IllegalConfigSnafu {
368 msg: format!("scan config at index {index} has an empty name"),
369 }
370 .build());
371 }
372 name.to_string()
373 }
374 None => format!("query-{:03}", index + 1),
375 };
376 if !names.insert(name.clone()) {
377 return Err(error::IllegalConfigSnafu {
378 msg: format!("duplicate scan config name '{name}' at index {index}"),
379 }
380 .build());
381 }
382 config.name = Some(name);
383 }
384
385 Ok(configs)
386}
387
388fn resolve_series_row_selector(
389 scan_config: &ScanConfig,
390) -> error::Result<Option<TimeSeriesRowSelector>> {
391 match scan_config.series_row_selector.as_deref() {
392 Some("last_row") => Ok(Some(TimeSeriesRowSelector::LastRow { after_merge: false })),
393 Some(other) => Err(error::IllegalConfigSnafu {
394 msg: format!("Unknown series_row_selector '{other}'"),
395 }
396 .build()),
397 None => Ok(None),
398 }
399}
400
401fn resolve_scan_configs(
402 config_set: &ScanConfigSet,
403 metadata: &RegionMetadata,
404) -> error::Result<Vec<ResolvedScanConfig>> {
405 config_set
406 .configs
407 .iter()
408 .enumerate()
409 .map(|(index, config)| {
410 let name = config
411 .name
412 .clone()
413 .unwrap_or_else(|| format!("query-{:03}", index + 1));
414 let projection = resolve_projection(config, Some(metadata)).map_err(|err| {
415 error::IllegalConfigSnafu {
416 msg: format!("invalid scan config at index {index} ('{name}'): {err}"),
417 }
418 .build()
419 })?;
420 let filters = resolve_filters(config, metadata).map_err(|err| {
421 error::IllegalConfigSnafu {
422 msg: format!("invalid scan config at index {index} ('{name}'): {err}"),
423 }
424 .build()
425 })?;
426 let series_row_selector = resolve_series_row_selector(config).map_err(|err| {
427 error::IllegalConfigSnafu {
428 msg: format!("invalid scan config at index {index} ('{name}'): {err}"),
429 }
430 .build()
431 })?;
432 Ok(ResolvedScanConfig {
433 name,
434 projection,
435 filters,
436 series_row_selector,
437 })
438 })
439 .collect()
440}
441
442fn resolve_projection(
443 scan_config: &ScanConfig,
444 metadata: Option<&RegionMetadata>,
445) -> error::Result<Option<Vec<usize>>> {
446 if scan_config.projection.is_some() && scan_config.projection_names.is_some() {
447 return Err(error::IllegalConfigSnafu {
448 msg: "scan config cannot contain both 'projection' and 'projection_names'".to_string(),
449 }
450 .build());
451 }
452
453 if let Some(projection) = &scan_config.projection {
454 return Ok(Some(projection.clone()));
455 }
456
457 if let Some(projection_names) = &scan_config.projection_names {
458 let metadata = metadata.context(error::IllegalConfigSnafu {
459 msg: "Missing region metadata while resolving 'projection_names'".to_string(),
460 })?;
461 let available_columns = metadata
462 .column_metadatas
463 .iter()
464 .map(|column| column.column_schema.name.as_str())
465 .collect::<Vec<_>>()
466 .join(", ");
467 let projection = projection_names
468 .iter()
469 .map(|name| {
470 metadata
471 .column_index_by_name(name)
472 .with_context(|| error::IllegalConfigSnafu {
473 msg: format!(
474 "Unknown column '{}' in projection_names, available columns: [{}]",
475 name, available_columns
476 ),
477 })
478 })
479 .collect::<error::Result<Vec<_>>>()?;
480 return Ok(Some(projection));
481 }
482
483 Ok(None)
484}
485
486struct LiteralTypeCaster {
488 schema: DFSchemaRef,
489}
490
491impl TreeNodeRewriter for LiteralTypeCaster {
492 type Node = DfExpr;
493
494 fn f_up(&mut self, expr: DfExpr) -> datafusion_common::Result<Transformed<DfExpr>> {
495 let DfExpr::BinaryExpr(BinaryExpr { left, op, right }) = &expr else {
496 return Ok(Transformed::no(expr));
497 };
498
499 if !matches!(
500 op,
501 Operator::Eq
502 | Operator::NotEq
503 | Operator::Lt
504 | Operator::LtEq
505 | Operator::Gt
506 | Operator::GtEq
507 ) {
508 return Ok(Transformed::no(expr));
509 }
510
511 let (col_expr, lit_expr, col_left) = match (left.as_ref(), right.as_ref()) {
512 (col @ DfExpr::Column(_), lit @ DfExpr::Literal(_, _)) => (col, lit, true),
513 (lit @ DfExpr::Literal(_, _), col @ DfExpr::Column(_)) => (col, lit, false),
514 _ => return Ok(Transformed::no(expr)),
515 };
516
517 let col_type = col_expr.get_type(self.schema.as_ref())?;
518 let DfExpr::Literal(scalar, _) = lit_expr else {
519 unreachable!()
520 };
521
522 if scalar.data_type() == col_type {
523 return Ok(Transformed::no(expr));
524 }
525
526 let lit_array = scalar.to_array()?;
527 let casted = compute::cast(lit_array.as_ref(), &col_type).map_err(|e| {
528 datafusion_common::DataFusionError::Internal(format!(
529 "Failed to cast literal {:?} to {:?}: {}",
530 scalar, col_type, e
531 ))
532 })?;
533 let casted_scalar = ScalarValue::try_from_array(&casted, 0)?;
534
535 let new_lit = DfExpr::Literal(casted_scalar, None);
536 let (new_left, new_right) = if col_left {
537 (left.clone(), Box::new(new_lit))
538 } else {
539 (Box::new(new_lit), right.clone())
540 };
541
542 Ok(Transformed::yes(DfExpr::BinaryExpr(BinaryExpr {
543 left: new_left,
544 op: *op,
545 right: new_right,
546 })))
547 }
548}
549
550fn convert_literal_types(
551 exprs: Vec<DfExpr>,
552 schema: &DFSchemaRef,
553) -> datafusion_common::Result<Vec<DfExpr>> {
554 use datafusion_common::tree_node::TreeNode;
555
556 let mut caster = LiteralTypeCaster {
557 schema: schema.clone(),
558 };
559 exprs
560 .into_iter()
561 .map(|e| e.rewrite(&mut caster).map(|x| x.data))
562 .collect()
563}
564
565fn resolve_filters(
566 scan_config: &ScanConfig,
567 metadata: &RegionMetadata,
568) -> error::Result<Vec<DfExpr>> {
569 let Some(filters) = &scan_config.filters else {
570 return Ok(Vec::new());
571 };
572
573 let df_schema = metadata
574 .schema
575 .arrow_schema()
576 .clone()
577 .to_dfschema()
578 .map_err(|e| {
579 error::IllegalConfigSnafu {
580 msg: format!("Failed to convert region schema to DataFusion schema: {e}"),
581 }
582 .build()
583 })?;
584
585 let state = SessionStateBuilder::new()
586 .with_config(Default::default())
587 .with_runtime_env(Default::default())
588 .with_default_features()
589 .build();
590
591 let exprs: Vec<DfExpr> = filters
592 .iter()
593 .enumerate()
594 .map(|(idx, filter)| {
595 let mut parser = SqlParser::new(&GenericDialect {})
596 .try_with_sql(filter)
597 .map_err(|e| {
598 error::IllegalConfigSnafu {
599 msg: format!("Invalid filter at index {idx} ('{filter}'): {e}"),
600 }
601 .build()
602 })?;
603
604 let sql_expr = parser.parse_expr().map_err(|e| {
605 error::IllegalConfigSnafu {
606 msg: format!("Invalid filter at index {idx} ('{filter}'): {e}"),
607 }
608 .build()
609 })?;
610
611 state
612 .create_logical_expr_from_sql_expr(
613 SqlExprWithAlias {
614 expr: sql_expr,
615 alias: None,
616 },
617 &df_schema,
618 )
619 .map_err(|e| {
620 error::IllegalConfigSnafu {
621 msg: format!(
622 "Failed to convert filter at index {idx} ('{filter}') to logical expr: {e}"
623 ),
624 }
625 .build()
626 })
627 })
628 .collect::<error::Result<Vec<_>>>()?;
629
630 let df_schema_ref = Arc::new(df_schema);
631 convert_literal_types(exprs, &df_schema_ref).map_err(|e| {
632 error::IllegalConfigSnafu {
633 msg: format!("Failed to convert filter expression types: {e}"),
634 }
635 .build()
636 })
637}
638
639fn noop_partition_expr_fetcher() -> mito2::region::opener::PartitionExprFetcherRef {
640 struct NoopPartitionExprFetcher;
641
642 #[async_trait::async_trait]
643 impl mito2::region::opener::PartitionExprFetcher for NoopPartitionExprFetcher {
644 async fn fetch_expr(&self, _region_id: RegionId) -> Option<String> {
645 None
646 }
647 }
648
649 Arc::new(NoopPartitionExprFetcher)
650}
651
652struct EngineComponents {
653 data_home: String,
654 mito_config: MitoConfig,
655 object_store_manager: Arc<ObjectStoreManager>,
656 schema_metadata_manager: Arc<SchemaMetadataManager>,
657 file_ref_manager: Arc<FileReferenceManager>,
658 partition_expr_fetcher: mito2::region::opener::PartitionExprFetcherRef,
659}
660
661impl EngineComponents {
662 async fn build<S: store_api::logstore::LogStore>(
663 self,
664 log_store: Arc<S>,
665 ) -> error::Result<MitoEngine> {
666 MitoEngine::new(
667 &self.data_home,
668 self.mito_config,
669 log_store,
670 self.object_store_manager,
671 self.schema_metadata_manager,
672 self.file_ref_manager,
673 self.partition_expr_fetcher,
674 Plugins::default(),
675 )
676 .await
677 .map_err(BoxedError::new)
678 .context(error::BuildCliSnafu)
679 }
680}
681
682fn mock_schema_metadata_manager() -> Arc<SchemaMetadataManager> {
683 let kv_backend = Arc::new(MemoryKvBackend::new());
684 let table_schema_cache = Arc::new(new_table_schema_cache(
685 "table_schema_name_cache".to_string(),
686 CacheBuilder::default().build(),
687 kv_backend.clone(),
688 ));
689 let schema_cache = Arc::new(new_schema_cache(
690 "schema_cache".to_string(),
691 CacheBuilder::default().build(),
692 kv_backend.clone(),
693 ));
694 Arc::new(SchemaMetadataManager::new(table_schema_cache, schema_cache))
695}
696
697impl ScanbenchCommand {
698 async fn load_scan_config_set(&self) -> error::Result<ScanConfigSet> {
699 match (&self.scan_config, &self.scan_configs) {
700 (Some(_), Some(_)) => Err(error::IllegalConfigSnafu {
701 msg: "--scan-config and --scan-configs are mutually exclusive".to_string(),
702 }
703 .build()),
704 (Some(path), None) => {
705 let content = tokio::fs::read_to_string(path)
706 .await
707 .context(error::FileIoSnafu)?;
708 let config =
709 serde_json::from_str::<ScanConfig>(&content).context(error::SerdeJsonSnafu)?;
710 Ok(ScanConfigSet {
711 configs: vec![config],
712 is_suite: false,
713 })
714 }
715 (None, Some(path)) => {
716 if self.iterations != 1 {
717 return Err(error::IllegalConfigSnafu {
718 msg: format!(
719 "--iterations cannot be used with --scan-configs (got {}); the array length defines the run count",
720 self.iterations
721 ),
722 }
723 .build());
724 }
725 let content = tokio::fs::read_to_string(path)
726 .await
727 .context(error::FileIoSnafu)?;
728 let configs = serde_json::from_str::<Vec<ScanConfig>>(&content)
729 .context(error::SerdeJsonSnafu)?;
730 Ok(ScanConfigSet {
731 configs: validate_scan_config_suite(configs, self.iterations)?,
732 is_suite: true,
733 })
734 }
735 (None, None) => Ok(ScanConfigSet {
736 configs: vec![ScanConfig::default()],
737 is_suite: false,
738 }),
739 }
740 }
741
742 pub async fn run(&self) -> error::Result<()> {
743 if self.verbose {
744 common_telemetry::init_default_ut_logging();
745 }
746
747 println!("{}", "Starting scanbench...".cyan().bold());
748 let benchmark_started_at_unix_ms = started_at_unix_ms();
749
750 let scan_config_set = self.load_scan_config_set().await?;
751
752 let region_id = parse_region_id(&self.region_id)?;
753 let path_type = parse_path_type(&self.path_type)?;
754 println!(
755 "{} Region ID: {} (u64: {})",
756 "✓".green(),
757 self.region_id,
758 region_id.as_u64()
759 );
760
761 let (store_cfg, mito_config, wal_config) = parse_config(&self.config)?;
763 println!("{} Config parsed", "✓".green());
764
765 let object_store = build_object_store(&store_cfg).await?;
766 println!("{} Object store initialized", "✓".green());
767
768 let object_store_manager =
769 Arc::new(ObjectStoreManager::new("default", object_store.clone()));
770
771 let schema_metadata_manager = mock_schema_metadata_manager();
773 let file_ref_manager = Arc::new(FileReferenceManager::new(None));
774 let partition_expr_fetcher = noop_partition_expr_fetcher();
775
776 let components = EngineComponents {
778 data_home: store_cfg.data_home.clone(),
779 mito_config,
780 object_store_manager,
781 schema_metadata_manager,
782 file_ref_manager,
783 partition_expr_fetcher,
784 };
785
786 let engine = match &wal_config {
787 DatanodeWalConfig::RaftEngine(raft_engine_config) if self.enable_wal => {
788 let data_home = normalize_dir(&store_cfg.data_home);
789 let wal_dir = match &raft_engine_config.dir {
790 Some(dir) => dir.clone(),
791 None => format!("{}{WAL_DIR}", data_home),
792 };
793 fs::create_dir_all(&wal_dir).await.map_err(|e| {
794 error::IllegalConfigSnafu {
795 msg: format!("failed to create WAL directory {}: {e}", wal_dir),
796 }
797 .build()
798 })?;
799 let log_store = Arc::new(
800 RaftEngineLogStore::try_new(wal_dir, raft_engine_config)
801 .await
802 .map_err(BoxedError::new)
803 .context(error::BuildCliSnafu)?,
804 );
805 println!("{} Using RaftEngine WAL", "✓".green());
806 components.build(log_store).await?
807 }
808 DatanodeWalConfig::Kafka(kafka_config) if self.enable_wal => {
809 let log_store = Arc::new(
810 KafkaLogStore::try_new(kafka_config, None)
811 .await
812 .map_err(BoxedError::new)
813 .context(error::BuildCliSnafu)?,
814 );
815 println!("{} Using Kafka WAL", "✓".green());
816 components.build(log_store).await?
817 }
818 _ => {
819 let log_store = Arc::new(NoopLogStore);
820 println!(
821 "{} Using NoopLogStore (enable_wal={})",
822 "✓".green(),
823 self.enable_wal
824 );
825 components.build(log_store).await?
826 }
827 };
828
829 let open_request = RegionOpenRequest {
831 engine: "mito".to_string(),
832 table_dir: self.table_dir.clone(),
833 path_type,
834 options: HashMap::default(),
835 skip_wal_replay: !self.enable_wal,
836 checkpoint: None,
837 requirements: Default::default(),
838 };
839
840 engine
841 .handle_request(region_id, RegionRequest::Open(open_request))
842 .await
843 .map_err(BoxedError::new)
844 .context(error::BuildCliSnafu)?;
845 println!("{} Region opened", "✓".green());
846
847 let metadata = engine
848 .get_metadata(region_id)
849 .await
850 .map_err(BoxedError::new)
851 .context(error::BuildCliSnafu)?;
852 let scan_configs = resolve_scan_configs(&scan_config_set, &metadata)?;
853
854 let distribution = match self.scanner.as_str() {
856 "seq" => None,
857 "unordered" => Some(TimeSeriesDistribution::TimeWindowed),
858 "series" => Some(TimeSeriesDistribution::PerSeries),
859 other => {
860 return Err(error::IllegalConfigSnafu {
861 msg: format!(
862 "Unknown scanner type '{}', expected: seq, unordered, series",
863 other
864 ),
865 }
866 .build());
867 }
868 };
869
870 let run_count = scan_config_set.run_count(self.iterations);
871 if scan_config_set.is_suite {
872 println!(
873 "{} Scanner: {}, Parallelism: {}, Queries: {}",
874 "ℹ".blue(),
875 self.scanner,
876 self.parallelism,
877 run_count,
878 );
879 } else {
880 println!(
881 "{} Scanner: {}, Parallelism: {}, Iterations: {}",
882 "ℹ".blue(),
883 self.scanner,
884 self.parallelism,
885 run_count,
886 );
887 }
888
889 #[cfg(unix)]
891 let mut profiler_guard = if self.pprof_file.is_some() && !self.pprof_after_warmup {
892 println!("{} Starting profiling...", "⚡".yellow());
893 Some(
894 pprof::ProfilerGuardBuilder::default()
895 .frequency(99)
896 .blocklist(&["libc", "libgcc", "pthread", "vdso"])
897 .build()
898 .map_err(|e| {
899 BoxedError::new(PlainError::new(
900 format!("Failed to start profiler: {e}"),
901 StatusCode::Unexpected,
902 ))
903 })
904 .context(error::BuildCliSnafu)?,
905 )
906 } else {
907 None
908 };
909
910 #[cfg(not(unix))]
911 if self.pprof_file.is_some() {
912 eprintln!(
913 "{}: Profiling is not supported on this platform",
914 "Warning".yellow()
915 );
916 }
917
918 let mut total_rows_all = 0u64;
919 let mut total_elapsed_all = std::time::Duration::ZERO;
920 let mut run_results = Vec::with_capacity(if self.result_file.is_some() {
921 run_count
922 } else {
923 0
924 });
925 let mut query_summaries = scan_configs
926 .iter()
927 .map(|config| QueryRunSummary::new(config.name.clone()))
928 .collect::<Vec<_>>();
929 let collect_scanner_explain = self.verbose || self.result_file.is_some();
930
931 for iteration in 0..run_count {
932 let query_index = scan_config_set.query_index(iteration);
933 let scan_config = &scan_configs[query_index];
934 let request = ScanRequest {
935 projection: scan_config.projection.clone(),
936 filters: scan_config.filters.clone(),
937 series_row_selector: scan_config.series_row_selector,
938 distribution,
939 ..Default::default()
940 };
941
942 let start = Instant::now();
943
944 let mut scanner = engine
946 .handle_query(region_id, request)
947 .await
948 .map_err(BoxedError::new)
949 .context(error::BuildCliSnafu)?;
950
951 let original_partitions = scanner.properties().partitions.clone();
953 let total_ranges: usize = original_partitions.iter().map(|p| p.len()).sum();
954
955 if self.verbose {
956 println!(
957 " {} Original partitions: {}, total ranges: {}",
958 "ℹ".blue(),
959 original_partitions.len(),
960 total_ranges
961 );
962 }
963
964 if self.parallelism > 1 {
965 let all_ranges: Vec<_> = original_partitions.into_iter().flatten().collect();
967
968 let mut partitions =
970 ParallelizeScan::assign_partition_range(all_ranges, self.parallelism);
971
972 for partition in &mut partitions {
974 partition.sort_by_key(|a| a.start);
975 }
976
977 scanner
978 .prepare(
979 PrepareRequest::default()
980 .with_ranges(partitions)
981 .with_target_partitions(self.parallelism),
982 )
983 .map_err(BoxedError::new)
984 .context(error::BuildCliSnafu)?;
985 }
986
987 let num_partitions = scanner.properties().partitions.len();
989 let ctx = QueryScanContext {
990 explain_verbose: collect_scanner_explain,
991 };
992 let metrics_set = ExecutionPlanMetricsSet::new();
993
994 let mut scan_futures = FuturesUnordered::new();
995 let setup_elapsed = start.elapsed();
996 let scan_start = Instant::now();
997
998 for partition_idx in 0..num_partitions {
999 let mut stream = scanner
1000 .scan_partition(&ctx, &metrics_set, partition_idx)
1001 .map_err(BoxedError::new)
1002 .context(error::BuildCliSnafu)?;
1003
1004 scan_futures.push(tokio::spawn(async move {
1005 let partition_start = Instant::now();
1006 let mut rows = 0u64;
1007 let mut batches = 0u64;
1008 let mut array_mem_size = 0u64;
1009 let mut estimated_size = 0u64;
1010 let mut first_batch_elapsed = None;
1011 while let Some(batch_result) = stream.next().await {
1012 match batch_result {
1013 Ok(batch) => {
1014 if first_batch_elapsed.is_none() {
1015 first_batch_elapsed = Some(partition_start.elapsed());
1016 }
1017 batches += 1;
1018 rows += batch.num_rows() as u64;
1019 let df_batch = batch.df_record_batch();
1020 array_mem_size += df_batch.get_array_memory_size() as u64;
1021 estimated_size +=
1022 mito2::memtable::record_batch_estimated_size(df_batch) as u64;
1023 }
1024 Err(e) => {
1025 return Err(BoxedError::new(e));
1026 }
1027 }
1028 }
1029 Ok::<PartitionScanStats, BoxedError>(PartitionScanStats {
1030 partition: partition_idx,
1031 rows,
1032 batches,
1033 array_mem_size,
1034 estimated_size,
1035 first_batch_elapsed,
1036 elapsed: partition_start.elapsed(),
1037 })
1038 }));
1039 }
1040
1041 let mut total_rows = 0u64;
1042 let mut total_batches = 0u64;
1043 let mut total_array_mem_size = 0u64;
1044 let mut total_estimated_size = 0u64;
1045 let mut partition_stats = Vec::with_capacity(num_partitions);
1046 while let Some(task) = scan_futures.next().await {
1047 let result = task
1048 .map_err(|e| {
1049 BoxedError::new(PlainError::new(
1050 format!("scan task failed: {e}"),
1051 StatusCode::Unexpected,
1052 ))
1053 })
1054 .context(error::BuildCliSnafu)?;
1055 let stats = result.context(error::BuildCliSnafu)?;
1056 total_rows += stats.rows;
1057 total_batches += stats.batches;
1058 total_array_mem_size += stats.array_mem_size;
1059 total_estimated_size += stats.estimated_size;
1060 partition_stats.push(stats);
1061 }
1062 let scan_elapsed = scan_start.elapsed();
1063
1064 let elapsed = start.elapsed();
1065 total_rows_all += total_rows;
1066 total_elapsed_all += elapsed;
1067 query_summaries[query_index].record(total_rows, elapsed);
1068
1069 let query_display = if scan_config_set.is_suite {
1070 format!(
1071 " [query {}/{}: {}]",
1072 query_index + 1,
1073 scan_configs.len(),
1074 scan_config.name
1075 )
1076 } else {
1077 String::new()
1078 };
1079
1080 println!(
1081 " [iter {}]{} {} rows in {:?} ({} partitions), array_mem_size: {}, estimated_size: {}",
1082 iteration + 1,
1083 query_display,
1084 total_rows.to_string().cyan(),
1085 elapsed,
1086 num_partitions,
1087 format_bytes(total_array_mem_size),
1088 format_bytes(total_estimated_size),
1089 );
1090
1091 if collect_scanner_explain {
1092 partition_stats.sort_unstable_by_key(|stats| stats.partition);
1093 }
1094
1095 if self.verbose {
1096 for stats in &partition_stats {
1097 let first_batch = stats
1098 .first_batch_elapsed
1099 .map(|elapsed| format!("{elapsed:?}"))
1100 .unwrap_or_else(|| "n/a".to_string());
1101 println!(
1102 " partition {}: rows={}, batches={}, first_batch={}, elapsed={:?}, array_mem_size={}, estimated_size={}",
1103 stats.partition,
1104 stats.rows,
1105 stats.batches,
1106 first_batch,
1107 stats.elapsed,
1108 format_bytes(stats.array_mem_size),
1109 format_bytes(stats.estimated_size),
1110 );
1111 }
1112 if !partition_stats.is_empty() {
1113 let total_partition_elapsed = partition_stats
1114 .iter()
1115 .map(|stats| stats.elapsed)
1116 .sum::<Duration>();
1117 let mean_partition_elapsed =
1118 total_partition_elapsed / partition_stats.len() as u32;
1119 let max_partition_elapsed = partition_stats
1120 .iter()
1121 .map(|stats| stats.elapsed)
1122 .max()
1123 .unwrap_or_default();
1124 let skew = max_partition_elapsed.as_secs_f64()
1125 / mean_partition_elapsed.as_secs_f64().max(f64::EPSILON);
1126 println!(
1127 " {} Timing: setup={:?}, scan={:?}, mean_partition={:?}, max_partition={:?}, partition_skew={:.2}x",
1128 "ℹ".blue(),
1129 setup_elapsed,
1130 scan_elapsed,
1131 mean_partition_elapsed,
1132 max_partition_elapsed,
1133 skew,
1134 );
1135 }
1136 }
1137
1138 let scanner_explain = if collect_scanner_explain {
1139 format!("{}", VerboseScannerDisplay(scanner.as_ref()))
1140 } else {
1141 String::new()
1142 };
1143 if self.verbose {
1144 println!(" {} Scanner explain: {}", "ℹ".blue(), scanner_explain);
1145 }
1146
1147 if self.result_file.is_some() {
1148 let source_config = &scan_config_set.configs[query_index];
1149 run_results.push(ScanRunResult {
1150 iteration: iteration + 1,
1151 query_index: query_index + 1,
1152 name: scan_config.name.clone(),
1153 config: NormalizedScanConfig {
1154 name: scan_config.name.clone(),
1155 projection: scan_config.projection.clone(),
1156 filters: source_config.filters.clone().unwrap_or_default(),
1157 series_row_selector: source_config.series_row_selector.clone(),
1158 },
1159 rows: total_rows,
1160 batches: total_batches,
1161 setup_elapsed_ns: duration_ns(setup_elapsed),
1162 scan_elapsed_ns: duration_ns(scan_elapsed),
1163 elapsed_ns: duration_ns(elapsed),
1164 array_mem_size_bytes: total_array_mem_size,
1165 estimated_size_bytes: total_estimated_size,
1166 partitions: partition_stats
1167 .iter()
1168 .map(|stats| PartitionResult {
1169 partition: stats.partition,
1170 rows: stats.rows,
1171 batches: stats.batches,
1172 array_mem_size_bytes: stats.array_mem_size,
1173 estimated_size_bytes: stats.estimated_size,
1174 first_batch_elapsed_ns: stats.first_batch_elapsed.map(duration_ns),
1175 elapsed_ns: duration_ns(stats.elapsed),
1176 })
1177 .collect(),
1178 scanner_explain,
1179 });
1180 }
1181
1182 #[cfg(unix)]
1184 if iteration == 0
1185 && self.pprof_after_warmup
1186 && self.pprof_file.is_some()
1187 && profiler_guard.is_none()
1188 {
1189 println!(
1190 "{} Starting profiling after warmup iteration...",
1191 "⚡".yellow()
1192 );
1193 profiler_guard = Some(
1194 pprof::ProfilerGuardBuilder::default()
1195 .frequency(99)
1196 .blocklist(&["libc", "libgcc", "pthread", "vdso"])
1197 .build()
1198 .map_err(|e| {
1199 BoxedError::new(PlainError::new(
1200 format!("Failed to start profiler: {e}"),
1201 StatusCode::Unexpected,
1202 ))
1203 })
1204 .context(error::BuildCliSnafu)?,
1205 );
1206 }
1207 }
1208
1209 #[cfg(unix)]
1211 if let (Some(guard), Some(pprof_file)) = (profiler_guard, &self.pprof_file) {
1212 println!("{} Generating flamegraph...", "🔥".yellow());
1213 match guard.report().build() {
1214 Ok(report) => {
1215 let mut flamegraph_data = Vec::new();
1216 if let Err(e) = report.flamegraph(&mut flamegraph_data) {
1217 println!("{}: Failed to generate flamegraph: {}", "Error".red(), e);
1218 } else if let Err(e) = std::fs::write(pprof_file, flamegraph_data) {
1219 println!(
1220 "{}: Failed to write flamegraph to {}: {}",
1221 "Error".red(),
1222 pprof_file.display(),
1223 e
1224 );
1225 } else {
1226 println!(
1227 "{} Flamegraph saved to {}",
1228 "✓".green(),
1229 pprof_file.display().to_string().cyan()
1230 );
1231 }
1232 }
1233 Err(e) => {
1234 println!("{}: Failed to generate pprof report: {}", "Error".red(), e);
1235 }
1236 }
1237 }
1238
1239 if scan_config_set.is_suite {
1241 let avg_elapsed = total_elapsed_all / run_count as u32;
1242 let avg_rows = total_rows_all / run_count as u64;
1243 println!(
1244 "\n{} Overall average: {} rows in {:?} over {} queries",
1245 "Summary".green().bold(),
1246 avg_rows.to_string().cyan(),
1247 avg_elapsed,
1248 run_count,
1249 );
1250 println!("{} Per-query:", "Summary".green().bold());
1251 for (index, summary) in query_summaries.iter().enumerate() {
1252 println!(
1253 " [{}] {}: runs={}, mean_rows={}, mean_elapsed={:?}",
1254 index + 1,
1255 summary.name,
1256 summary.runs,
1257 summary.mean_rows(),
1258 summary.mean_elapsed(),
1259 );
1260 }
1261 } else if run_count > 1 {
1262 let avg_elapsed = total_elapsed_all / run_count as u32;
1263 let avg_rows = total_rows_all / run_count as u64;
1264 println!(
1265 "\n{} Average: {} rows in {:?} over {} iterations",
1266 "Summary".green().bold(),
1267 avg_rows.to_string().cyan(),
1268 avg_elapsed,
1269 run_count,
1270 );
1271 }
1272
1273 if let Some(result_file) = &self.result_file {
1274 let result = ScanbenchResult {
1275 format_version: RESULT_FORMAT_VERSION,
1276 started_at_unix_ms: benchmark_started_at_unix_ms,
1277 benchmark: BenchmarkMetadata {
1278 scanner: self.scanner.clone(),
1279 region_id: self.region_id.clone(),
1280 region_id_u64: region_id.as_u64(),
1281 table_dir: self.table_dir.clone(),
1282 path_type: self.path_type.clone(),
1283 parallelism: self.parallelism,
1284 enable_wal: self.enable_wal,
1285 config_mode: if scan_config_set.is_suite {
1286 "suite".to_string()
1287 } else {
1288 "single".to_string()
1289 },
1290 run_count,
1291 },
1292 runs: run_results,
1293 summary: result_summary(total_rows_all, total_elapsed_all, &query_summaries),
1294 };
1295 write_result_file(result_file, &result).await?;
1296 println!(
1297 "{} Results saved to {}",
1298 "✓".green(),
1299 result_file.display().to_string().cyan()
1300 );
1301 }
1302
1303 println!("\n{}", "Benchmark completed!".green().bold());
1304 Ok(())
1305 }
1306}
1307
1308#[cfg(test)]
1309mod tests {
1310 use datatypes::prelude::ConcreteDataType;
1311 use datatypes::schema::ColumnSchema;
1312 use sqlparser::ast::{BinaryOperator, Expr};
1313 use sqlparser::dialect::GenericDialect;
1314 use sqlparser::parser::Parser;
1315 use store_api::metadata::{ColumnMetadata, RegionMetadataBuilder};
1316 use store_api::storage::RegionId;
1317
1318 use super::{
1319 BenchmarkMetadata, BenchmarkResultSummary, NormalizedScanConfig, PartitionResult,
1320 ScanConfig, ScanRunResult, ScanbenchResult, resolve_filters, resolve_projection,
1321 validate_scan_config_suite, write_result_file,
1322 };
1323 use crate::error;
1324
1325 #[test]
1326 fn test_parse_scan_config_projection_names() {
1327 let json = r#"{"projection_names":["host","ts"]}"#;
1328 let config: ScanConfig = serde_json::from_str(json).unwrap();
1329
1330 assert_eq!(
1331 config.projection_names,
1332 Some(vec!["host".to_string(), "ts".to_string()])
1333 );
1334 assert_eq!(config.projection, None);
1335 }
1336
1337 #[test]
1338 fn test_resolve_projection_by_indexes() -> error::Result<()> {
1339 let config = ScanConfig {
1340 name: None,
1341 projection: Some(vec![0, 2]),
1342 projection_names: None,
1343 filters: None,
1344 series_row_selector: None,
1345 };
1346
1347 let projection = resolve_projection(&config, None)?;
1348 assert_eq!(projection, Some(vec![0, 2]));
1349 Ok(())
1350 }
1351
1352 #[test]
1353 fn test_resolve_projection_by_names_without_metadata() {
1354 let config = ScanConfig {
1355 name: None,
1356 projection: None,
1357 projection_names: Some(vec!["cpu".to_string(), "host".to_string()]),
1358 filters: None,
1359 series_row_selector: None,
1360 };
1361
1362 let err = resolve_projection(&config, None).unwrap_err();
1363 assert!(
1364 err.to_string()
1365 .contains("Missing region metadata while resolving 'projection_names'")
1366 );
1367 }
1368
1369 #[test]
1370 fn test_resolve_projection_conflict_fields() {
1371 let config = ScanConfig {
1372 name: None,
1373 projection: Some(vec![0]),
1374 projection_names: Some(vec!["host".to_string()]),
1375 filters: None,
1376 series_row_selector: None,
1377 };
1378
1379 let err = resolve_projection(&config, None).unwrap_err();
1380 let msg = err.to_string();
1381 assert!(msg.contains("projection"));
1382 assert!(msg.contains("projection_names"));
1383 }
1384
1385 #[test]
1386 fn test_sqlparser_parse_expr_string() {
1387 let dialect = GenericDialect {};
1388 let mut parser = Parser::new(&dialect)
1389 .try_with_sql("host = 'web-1' AND cpu > 80")
1390 .unwrap();
1391
1392 let expr = parser.parse_expr().unwrap();
1393
1394 match expr {
1395 Expr::BinaryOp { op, .. } => assert_eq!(op, BinaryOperator::And),
1396 other => panic!("expected BinaryOp, got: {other:?}"),
1397 }
1398 }
1399
1400 #[test]
1401 fn test_resolve_filters_uint32_type_conversion() {
1402 use api::v1::SemanticType;
1403
1404 let mut builder = RegionMetadataBuilder::new(RegionId::new(1, 0));
1405 builder
1406 .push_column_metadata(ColumnMetadata {
1407 column_schema: ColumnSchema::new(
1408 "table_id",
1409 ConcreteDataType::uint32_datatype(),
1410 false,
1411 ),
1412 semantic_type: SemanticType::Tag,
1413 column_id: 1,
1414 })
1415 .push_column_metadata(ColumnMetadata {
1416 column_schema: ColumnSchema::new(
1417 "ts",
1418 ConcreteDataType::timestamp_millisecond_datatype(),
1419 false,
1420 ),
1421 semantic_type: SemanticType::Timestamp,
1422 column_id: 2,
1423 })
1424 .primary_key(vec![1]);
1425 let metadata = builder.build().unwrap();
1426
1427 let config = ScanConfig {
1428 name: None,
1429 projection: None,
1430 projection_names: None,
1431 filters: Some(vec!["table_id = 1117".to_string()]),
1432 series_row_selector: None,
1433 };
1434
1435 let exprs = resolve_filters(&config, &metadata).unwrap();
1436 assert_eq!(exprs.len(), 1);
1437 let expr_str = format!("{}", exprs[0]);
1439 assert!(
1440 expr_str.contains("UInt32(1117)"),
1441 "Expected UInt32(1117) in expression, got: {expr_str}"
1442 );
1443 }
1444
1445 #[test]
1446 fn test_parse_scan_config_filters() {
1447 let json = r#"{"filters":["host = 'web-1'","cpu > 80"]}"#;
1448 let config: ScanConfig = serde_json::from_str(json).unwrap();
1449
1450 assert_eq!(
1451 config.filters,
1452 Some(vec!["host = 'web-1'".to_string(), "cpu > 80".to_string()])
1453 );
1454 }
1455
1456 #[test]
1457 fn test_parse_and_validate_scan_config_suite() {
1458 let json = r#"[
1459 {"name":" cold ","filters":["host = 'web-1'"]},
1460 {"projection_names":["host","cpu"]}
1461 ]"#;
1462 let configs: Vec<ScanConfig> = serde_json::from_str(json).unwrap();
1463 let configs = validate_scan_config_suite(configs, 1).unwrap();
1464
1465 assert_eq!(2, configs.len());
1466 assert_eq!(Some("cold"), configs[0].name.as_deref());
1467 assert_eq!(Some("query-002"), configs[1].name.as_deref());
1468 assert_eq!(
1469 Some(&vec!["host = 'web-1'".to_string()]),
1470 configs[0].filters.as_ref()
1471 );
1472 }
1473
1474 #[test]
1475 fn test_validate_scan_config_suite_rejects_invalid_input() {
1476 let empty = validate_scan_config_suite(Vec::new(), 1)
1477 .unwrap_err()
1478 .to_string();
1479 assert!(empty.contains("at least one"));
1480
1481 let iterations = validate_scan_config_suite(vec![ScanConfig::default()], 2)
1482 .unwrap_err()
1483 .to_string();
1484 assert!(iterations.contains("--iterations"));
1485
1486 let configs: Vec<ScanConfig> =
1487 serde_json::from_str(r#"[{"name":"query"},{"name":" query "}]"#).unwrap();
1488 let duplicate = validate_scan_config_suite(configs, 1)
1489 .unwrap_err()
1490 .to_string();
1491 assert!(duplicate.contains("duplicate"));
1492
1493 let configs: Vec<ScanConfig> = serde_json::from_str(r#"[{"name":" "}]"#).unwrap();
1494 let blank = validate_scan_config_suite(configs, 1)
1495 .unwrap_err()
1496 .to_string();
1497 assert!(blank.contains("empty name"));
1498 }
1499
1500 #[tokio::test]
1501 async fn test_write_result_file_overwrites_complete_result() {
1502 let dir = tempfile::tempdir().unwrap();
1503 let path = dir.path().join("scanbench-result.json");
1504 tokio::fs::write(&path, b"old result").await.unwrap();
1505
1506 let result = ScanbenchResult {
1507 format_version: 1,
1508 started_at_unix_ms: 42,
1509 benchmark: BenchmarkMetadata {
1510 scanner: "seq".to_string(),
1511 region_id: "1024:0".to_string(),
1512 region_id_u64: RegionId::new(1024, 0).as_u64(),
1513 table_dir: "greptime/public/1024".to_string(),
1514 path_type: "bare".to_string(),
1515 parallelism: 8,
1516 enable_wal: false,
1517 config_mode: "suite".to_string(),
1518 run_count: 1,
1519 },
1520 runs: vec![ScanRunResult {
1521 iteration: 1,
1522 query_index: 1,
1523 name: "cpu-host-1".to_string(),
1524 config: NormalizedScanConfig {
1525 name: "cpu-host-1".to_string(),
1526 projection: Some(vec![1, 2]),
1527 filters: vec!["hostname = 'host_1'".to_string()],
1528 series_row_selector: None,
1529 },
1530 rows: 100,
1531 batches: 2,
1532 setup_elapsed_ns: 10,
1533 scan_elapsed_ns: 80,
1534 elapsed_ns: 90,
1535 array_mem_size_bytes: 1024,
1536 estimated_size_bytes: 512,
1537 partitions: vec![PartitionResult {
1538 partition: 0,
1539 rows: 100,
1540 batches: 2,
1541 array_mem_size_bytes: 1024,
1542 estimated_size_bytes: 512,
1543 first_batch_elapsed_ns: Some(20),
1544 elapsed_ns: 80,
1545 }],
1546 scanner_explain: "SeqScan: region=1024(0)".to_string(),
1547 }],
1548 summary: BenchmarkResultSummary {
1549 runs: 1,
1550 total_rows: 100,
1551 total_elapsed_ns: 90,
1552 mean_rows: 100,
1553 mean_elapsed_ns: 90,
1554 queries: vec![],
1555 },
1556 };
1557
1558 write_result_file(&path, &result).await.unwrap();
1559 let content = tokio::fs::read(&path).await.unwrap();
1560 let actual: serde_json::Value = serde_json::from_slice(&content).unwrap();
1561
1562 assert_eq!(1, actual["format_version"]);
1563 assert_eq!("suite", actual["benchmark"]["config_mode"]);
1564 assert_eq!(2, actual["runs"][0]["config"]["projection"][1]);
1565 assert_eq!(100, actual["runs"][0]["partitions"][0]["rows"]);
1566 assert_eq!(
1567 "SeqScan: region=1024(0)",
1568 actual["runs"][0]["scanner_explain"]
1569 );
1570 assert_eq!(90, actual["summary"]["mean_elapsed_ns"]);
1571 }
1572}