use std::fmt::Write as _;
use rudb_metrics::{Document, Operator};
use rudb_plan::{Node, NodeRef, OperatorRef, PipelineRef, Plan, Shape};
use rudb_seam::{Registries, SeamId, Settings};
use crate::estimate::{Statistics, rows};
#[derive(Debug, Clone, Copy)]
pub struct Seams<'a> {
settings: &'a Settings,
registries: &'a Registries,
}
impl<'a> Seams<'a> {
#[must_use]
pub fn new(settings: &'a Settings, registries: &'a Registries) -> Self {
Self { settings, registries }
}
fn chosen(self, seam: SeamId) -> Option<(String, bool)> {
if !self.registries.has(seam) {
return None;
}
let rows = self.registries.rows();
let rows = rows.iter().filter(|row| row.seam == seam);
if let Some(pinned) = self.settings.pinned(seam) {
let is_reference =
rows.clone().find(|row| row.name == pinned).is_some_and(|row| row.is_reference);
return Some((format!("{pinned} (pinned)"), is_reference));
}
let row = rows.clone().find(|row| row.is_default).or_else(|| rows.clone().next())?;
Some((format!("{} (default)", row.name), row.is_reference))
}
fn all_reference(self, node: &Node) -> bool {
seams_of(node).iter().all(|seam| self.chosen(*seam).is_none_or(|(_, reference)| reference))
}
}
#[must_use]
pub fn explain(plan: &Plan, statistics: &Statistics) -> String {
let settings = Settings::new();
let registries = Registries::new();
explain_with(plan, statistics, Seams::new(&settings, ®istries))
}
#[must_use]
pub fn explain_with(plan: &Plan, statistics: &Statistics, seams: Seams<'_>) -> String {
printed(plan, statistics, seams, None)
}
#[must_use]
pub fn analyzed(
plan: &Plan,
statistics: &Statistics,
seams: Seams<'_>,
measured: &Document,
) -> String {
printed(plan, statistics, seams, Some(measured))
}
pub fn record_estimates(plan: &Plan, statistics: &Statistics, document: &mut Document) {
let shape = Shape::of(plan);
let mut estimated = vec![None; shape.operators() as usize];
for node in 0..u32::try_from(plan.node_count()).unwrap_or(u32::MAX) {
if let Some(id) = shape.operator_of(node) {
estimated[id as usize] = rows(plan, node, statistics);
}
}
for operator in &mut document.operators {
if let Some(estimate) = estimated.get(operator.id as usize) {
operator.estimated_rows = *estimate;
}
}
}
fn printed(
plan: &Plan,
statistics: &Statistics,
seams: Seams<'_>,
measured: Option<&Document>,
) -> String {
let shape = Shape::of(plan);
let printing = Printing { plan, statistics, shape: &shape, seams, measured };
let mut out = String::new();
printing.write_node(plan.root(), 0, &mut out);
write_pipelines(&shape, measured, &mut out);
write_seams(seams, &mut out);
if let Some(measured) = measured {
write_totals(measured, &mut out);
}
out
}
#[derive(Clone, Copy)]
struct Printing<'a> {
plan: &'a Plan,
statistics: &'a Statistics,
shape: &'a Shape,
seams: Seams<'a>,
measured: Option<&'a Document>,
}
impl Printing<'_> {
fn write_node(self, node: NodeRef, depth: usize, out: &mut String) {
let printed = self.plan.operator(node);
let estimate = match rows(self.plan, node, self.statistics) {
Some(count) => format!("~{count} rows"),
None => "rows unknown".to_owned(),
};
let pipeline = self.shape.pipeline(node);
let marker =
if self.seams.all_reference(self.plan.node(node)) { " [reference]" } else { "" };
let actual = self
.measured
.map(|measured| actually(measured, self.shape.operator(node)))
.unwrap_or_default();
let _ = writeln!(
out,
"{:indent$}{printed} [{estimate}] [pipeline {pipeline}]{marker}{actual}",
"",
indent = depth * 2
);
if let (Some(measured), Some(gathered)) = (self.measured, self.shape.gathered(node)) {
if let Some(operator) = row(measured, gathered) {
let _ = writeln!(
out,
"{:indent$}{} of the side that finishes first{}",
"",
operator.kind,
actually(measured, gathered),
indent = (depth + 1) * 2
);
}
}
for child in children(self.plan.node(node)) {
self.write_node(child, depth + 1, out);
}
}
}
fn actually(measured: &Document, id: OperatorRef) -> String {
let Some(operator) = row(measured, id) else {
return " [not measured]".to_owned();
};
let held = operator.memory.high_water;
let memory = if held == 0 { String::new() } else { format!(", {} held", bytes(held)) };
let slow = match operator.fallbacks.worst() {
None => String::new(),
Some((cause, _)) => {
format!(", {} fell back, most of it {}", operator.fallbacks.total(), cause.name())
}
};
format!(" [{} rows, {}{memory}{slow}]", operator.rows_out, duration(operator.wall_ns))
}
fn row(measured: &Document, id: OperatorRef) -> Option<&Operator> {
measured.operators.iter().find(|operator| operator.id == id)
}
fn write_pipelines(shape: &Shape, measured: Option<&Document>, out: &mut String) {
let _ = writeln!(out, "\nPipelines");
for pipeline in shape.all() {
let waits = shape.waits_for(pipeline);
let waiting = if waits.is_empty() {
"waits for nothing".to_owned()
} else {
format!("waits for {}", listed(waits))
};
let root = if pipeline == ROOT { ", and the answer comes out of it" } else { "" };
let took = measured
.and_then(|measured| measured.pipelines.iter().find(|row| row.id == pipeline))
.map(|row| format!(" [{} wall, {} cpu]", duration(row.wall_ns), duration(row.cpu_ns)))
.unwrap_or_default();
let _ = writeln!(out, " pipeline {pipeline} {waiting}{root}{took}");
}
}
fn write_totals(measured: &Document, out: &mut String) {
let timing = &measured.timing;
let _ = writeln!(out, "\nTotals");
let _ = writeln!(
out,
" {} building the tree, {} running it, {} in all",
duration(timing.physical_ns),
duration(timing.execute_ns),
duration(timing.total_ns)
);
let _ = writeln!(
out,
" {} of cpu, {} held at the peak",
duration(measured.resource.cpu_ns),
bytes(measured.resource.peak_bytes)
);
let warnings = measured.warnings();
if !warnings.is_empty() {
let _ = writeln!(out, "\nWarnings");
for warning in &warnings {
let _ = writeln!(out, " {warning}");
}
}
}
fn duration(ns: u64) -> String {
match ns {
0 => "0s".to_owned(),
1..1_000 => format!("{ns}ns"),
1_000..1_000_000 => format!("{:.3}us", ns as f64 / 1_000.0),
1_000_000..1_000_000_000 => format!("{:.3}ms", ns as f64 / 1_000_000.0),
_ => format!("{:.3}s", ns as f64 / 1_000_000_000.0),
}
}
fn bytes(count: u64) -> String {
match count {
0..1024 => format!("{count} bytes"),
1024..1_048_576 => format!("{:.1} KiB", count as f64 / 1024.0),
1_048_576..1_073_741_824 => format!("{:.1} MiB", count as f64 / 1_048_576.0),
_ => format!("{:.1} GiB", count as f64 / 1_073_741_824.0),
}
}
fn write_seams(seams: Seams<'_>, out: &mut String) {
let _ = writeln!(out, "\nSeams");
let mut printed = 0;
for seam in SeamId::ALL {
if let Some((chosen, _)) = seams.chosen(*seam) {
let _ = writeln!(out, " {} = {chosen}", seam.name());
printed += 1;
}
}
let unregistered = SeamId::ALL.len() - printed;
if unregistered > 0 {
let _ = writeln!(
out,
" {unregistered} seams have nothing registered and are running their reference implementation, see rudb_strategies()"
);
}
}
const ROOT: PipelineRef = 0;
fn listed(pipelines: &[PipelineRef]) -> String {
let numbers: Vec<String> = pipelines.iter().map(u32::to_string).collect();
match numbers.split_last() {
None => String::new(),
Some((last, [])) => last.clone(),
Some((last, rest)) => format!("{} and {last}", rest.join(", ")),
}
}
fn children(node: &Node) -> Vec<NodeRef> {
node.children().into_iter().flatten().collect()
}
fn seams_of(node: &Node) -> &'static [SeamId] {
const HASHED: &[SeamId] = &[SeamId::HashKey, SeamId::HashFunction, SeamId::HashTable];
match node {
Node::Get { .. } => &[SeamId::VectorForm, SeamId::ScanMaterialisation],
Node::Filter { .. } => &[SeamId::ExprEval, SeamId::KernelCompare, SeamId::KernelFilter],
Node::Project { .. } => &[SeamId::ExprEval],
Node::Aggregate { .. } => &[
SeamId::HashKey,
SeamId::HashFunction,
SeamId::HashTable,
SeamId::AggState,
SeamId::AggParallel,
],
Node::Distinct { .. } | Node::SetOp { .. } => HASHED,
Node::Sort { .. } => &[SeamId::Sort],
Node::TopN { .. } => &[SeamId::TopK],
Node::Join { .. } => {
&[SeamId::JoinBuild, SeamId::JoinFilter, SeamId::HashKey, SeamId::HashFunction]
}
Node::Dummy
| Node::Values { .. }
| Node::TableFunction { .. }
| Node::Limit { .. }
| Node::CrossProduct { .. } => &[],
}
}
#[cfg(test)]
mod tests {
use rudb_plan::Plan;
use rudb_seam::{Registries, Settings};
use super::{Seams, explain, explain_with};
use crate::estimate::Statistics;
fn parsed(text: &str) -> Plan {
Plan::parse(text).unwrap_or_else(|error| panic!("{text} did not parse: {error}"))
}
fn printed(text: &str, tables: &[(&str, u64)]) -> String {
let mut statistics = Statistics::new();
for (table, count) in tables {
statistics.record("memory", "main", table, *count);
}
explain(&parsed(text), &statistics)
}
fn tree(out: &str) -> Vec<&str> {
out.lines().take_while(|line| !line.is_empty()).collect()
}
#[test]
fn every_operator_gets_a_line_with_its_own_estimate_on_it() {
let out = printed(
concat!(
"Filter (#0.0::INTEGER > 1::INTEGER)::BOOLEAN\n",
" Get memory.main.t AS t #0 [a::INTEGER]\n",
),
&[("t", 1000)],
);
assert_eq!(
tree(&out).join("\n"),
concat!(
"Filter (#0.0::INTEGER > 1::INTEGER)::BOOLEAN [~200 rows] [pipeline 0] [reference]\n",
" Get memory.main.t AS t #0 [a::INTEGER] [~1000 rows] [pipeline 0] [reference]",
)
);
}
#[test]
fn an_operator_nobody_can_estimate_says_so_rather_than_saying_nothing() {
let out = printed("Get memory.main.t AS t #0 [a::INTEGER]\n", &[]);
assert!(tree(&out)[0].contains("[rows unknown]"), "{out}");
}
#[test]
fn both_sides_of_a_join_are_printed_under_it_and_each_carries_its_own_number() {
let out = printed(
concat!(
"Join INNER on=[(#0.0::INTEGER = #1.0::INTEGER)::BOOLEAN]\n",
" Get memory.main.small AS small #0 [a::INTEGER]\n",
" Get memory.main.big AS big #1 [a::INTEGER]\n",
),
&[("small", 10), ("big", 5000)],
);
let lines = tree(&out);
assert_eq!(lines.len(), 3, "{out}");
assert!(lines[0].contains("[~5000 rows]"), "{out}");
assert!(lines[1].contains("small") && lines[1].contains("[~10 rows]"), "{out}");
assert!(lines[2].contains("big") && lines[2].contains("[~5000 rows]"), "{out}");
assert!(lines[1].starts_with(" Get"), "{out}");
}
#[test]
fn a_plan_that_does_not_break_is_one_pipeline_and_says_so() {
let out = printed("Get memory.main.t AS t #0 [a::INTEGER]\n", &[("t", 4)]);
assert!(out.contains("[pipeline 0]"), "{out}");
assert!(
out.contains(" pipeline 0 waits for nothing, and the answer comes out of it"),
"{out}"
);
}
#[test]
fn a_sort_prints_the_pipeline_it_ends_and_the_edge_above_it() {
let out = printed(
concat!(
"Sort [#0.0::INTEGER ASC NULLS LAST]\n",
" Get memory.main.t AS t #0 [a::INTEGER]\n",
),
&[("t", 100)],
);
let lines = tree(&out);
assert!(lines[0].contains("[pipeline 1]"), "the sort ends the one below it: {out}");
assert!(lines[1].contains("[pipeline 1]"), "{out}");
assert!(out.contains(" pipeline 0 waits for 1"), "{out}");
assert!(out.contains(" pipeline 1 waits for nothing"), "{out}");
}
#[test]
fn a_join_prints_three_pipelines_in_the_order_they_have_to_run() {
let out = printed(
concat!(
"Join INNER on=[(#0.0::INTEGER = #1.0::INTEGER)::BOOLEAN]\n",
" Get memory.main.l AS l #0 [a::INTEGER]\n",
" Get memory.main.r AS r #1 [a::INTEGER]\n",
),
&[("l", 10), ("r", 10)],
);
let lines = tree(&out);
assert!(lines[0].contains("[pipeline 2]"), "{out}");
assert!(lines[1].contains("[pipeline 2]"), "the probing side: {out}");
assert!(lines[2].contains("[pipeline 1]"), "the gathered side runs first: {out}");
assert!(out.contains(" pipeline 0 waits for 2"), "{out}");
assert!(out.contains(" pipeline 2 waits for 1"), "{out}");
}
#[test]
fn with_nothing_registered_the_seam_section_says_what_that_means() {
let out = printed("Get memory.main.t AS t #0 [a::INTEGER]\n", &[("t", 4)]);
assert!(out.contains("\nSeams\n"), "{out}");
assert!(
out.contains(
" 27 seams have nothing registered and are running their reference implementation"
),
"{out}"
);
}
#[test]
fn a_hint_that_pins_a_seam_nobody_has_registered_changes_nothing_that_prints() {
let mut settings = Settings::new();
settings.pin(rudb_seam::SeamId::Sort, "merge");
let registries = Registries::new();
let plan = parsed(
"Sort [#0.0::INTEGER ASC NULLS LAST]\n Get memory.main.t AS t #0 [a::INTEGER]\n",
);
let out = explain_with(&plan, &Statistics::new(), Seams::new(&settings, ®istries));
assert!(out.contains("[reference]"), "{out}");
assert!(!out.contains("sort = "), "{out}");
}
}