Skip to main content

persistant_relational/
speed.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2//! Stage 3 of backend benchmarking for the relational kind: **speed**, as data, with the environment
3//! recorded. The rules and the data types are the kind-agnostic ones in [`persistant_core::bench`]
4//! (only a backend that passes the contract leaf a workload depends on is timed; the rest are listed
5//! as excluded, never ranked; a figure carries its environment and the command that re-runs it).
6//! This module owns the relational workloads and how they are timed: wall-clock, one statement at a
7//! time, from the caller's side of the trait. A network database includes its round trip.
8
9use crate::conformance::{Leaf, Sql, Ty, drop_tables};
10use crate::contract;
11use crate::{RelError, RelationalBackend, RelationalTx, Value};
12use persistant_core::choose::Eligibility;
13use persistant_core::{Config, Reason};
14use std::time::Instant;
15
16pub use persistant_core::bench::{
17    Comparison, Environment, Left, Outcome, Ranked, SpeedParseError as ParseError, SpeedRow,
18    SpeedTable, Stats, compare,
19};
20
21/// One thing worth timing, with the contract leaf it depends on.
22#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
23#[non_exhaustive]
24pub enum Workload {
25    /// `INSERT` of one row to a fresh key, outside a transaction.
26    Insert,
27    /// `SELECT` of one row by primary key, over a table of 100 rows.
28    SelectByKey,
29    /// `UPDATE` of one row by primary key.
30    Update,
31    /// `begin`, one `INSERT`, `commit`.
32    Transaction,
33}
34
35impl Workload {
36    /// Every workload.
37    pub const ALL: [Workload; 4] = [
38        Workload::Insert,
39        Workload::SelectByKey,
40        Workload::Update,
41        Workload::Transaction,
42    ];
43
44    /// The id used in data files.
45    pub fn id(self) -> &'static str {
46        match self {
47            Workload::Insert => "speed.rel.insert",
48            Workload::SelectByKey => "speed.rel.select_by_key",
49            Workload::Update => "speed.rel.update",
50            Workload::Transaction => "speed.rel.transaction",
51        }
52    }
53
54    /// The contract leaf the workload needs: no pass, no number.
55    pub fn required(self) -> Leaf {
56        match self {
57            Workload::Insert | Workload::SelectByKey | Workload::Update => Leaf::Values,
58            Workload::Transaction => Leaf::Transaction,
59        }
60    }
61}
62
63/// Time `workloads` on `target`, `iterations` operations each. A workload whose leaf the target does
64/// not pass gets an excluded row and no timing.
65pub async fn run<R: RelationalBackend>(
66    target: &R,
67    config: &Config,
68    env: &Environment,
69    workloads: &[Workload],
70    iterations: u32,
71) -> Vec<SpeedRow> {
72    let mut out = Vec::with_capacity(workloads.len());
73    for w in workloads.iter().copied() {
74        let leaf = w.required();
75        let outcome = match contract::check(target, &[leaf]).await {
76            Eligibility::Eligible => match time(target, w, iterations).await {
77                Ok(Some(s)) => Outcome::Measured(s),
78                Ok(None) => Outcome::Excluded {
79                    leaf: leaf.id().to_string(),
80                    reason: Reason::Untested,
81                    note: "no operations were timed".to_string(),
82                },
83                Err(e) => Outcome::Excluded {
84                    leaf: leaf.id().to_string(),
85                    reason: Reason::Missing,
86                    note: format!("the workload failed on a backend that passed the leaf: {e}"),
87                },
88            },
89            Eligibility::Excluded(ex) => {
90                let e = &ex[0];
91                Outcome::Excluded {
92                    leaf: e.leaf.clone(),
93                    reason: e.reason,
94                    note: e.note.clone(),
95                }
96            }
97        };
98        out.push(SpeedRow {
99            workload: w.id().to_string(),
100            backend: target.backend().to_string(),
101            version: target.version().to_string(),
102            config: config.clone(),
103            env: env.clone(),
104            outcome,
105        });
106    }
107    out
108}
109
110const TABLE: &str = "persistant_speed_rel";
111const SEED_ROWS: i64 = 100;
112
113async fn time<R: RelationalBackend>(
114    b: &R,
115    w: Workload,
116    iterations: u32,
117) -> Result<Option<Stats>, RelError> {
118    let s = Sql(b.dialect());
119    drop_tables(b, &[TABLE]).await?;
120    b.execute(
121        &format!(
122            "CREATE TABLE {TABLE} ({}, n BIGINT NOT NULL, t {} NOT NULL){}",
123            s.pk("id"),
124            s.ty(Ty::Text),
125            s.tail()
126        ),
127        &[],
128    )
129    .await?;
130    let result = timed_loop(b, &s, w, iterations).await;
131    // Best effort: leave a shared target as found, whatever happened.
132    let _ = drop_tables(b, &[TABLE]).await;
133    result
134}
135
136async fn timed_loop<R: RelationalBackend>(
137    b: &R,
138    s: &Sql,
139    w: Workload,
140    iterations: u32,
141) -> Result<Option<Stats>, RelError> {
142    let ins = format!(
143        "INSERT INTO {TABLE} (id, n, t) VALUES ({}, {}, {})",
144        s.p(1),
145        s.p(2),
146        s.p(3)
147    );
148    let sel = format!("SELECT n, t FROM {TABLE} WHERE id = {}", s.p(1));
149    let upd = format!("UPDATE {TABLE} SET n = n + 1 WHERE id = {}", s.p(1));
150    let row = |id: i64| [Value::Int(id), Value::Int(id), Value::Text("x".repeat(64))];
151    if matches!(w, Workload::SelectByKey | Workload::Update) {
152        for id in 0..SEED_ROWS {
153            b.execute(&ins, &row(id)).await?;
154        }
155    }
156    let mut us: Vec<u64> = Vec::with_capacity(iterations as usize);
157    macro_rules! timed {
158        ($e:expr) => {{
159            let t = Instant::now();
160            let r = $e;
161            us.push(u64::try_from(t.elapsed().as_nanos()).unwrap_or(u64::MAX));
162            r
163        }};
164    }
165    for i in 0..i64::from(iterations) {
166        match w {
167            Workload::Insert => {
168                timed!(b.execute(&ins, &row(i)).await?);
169            }
170            Workload::SelectByKey => {
171                let rows = timed!(b.query(&sel, &[Value::Int(i % SEED_ROWS)]).await?);
172                if rows.len() != 1 {
173                    return Err(RelError::Backend(
174                        "select by key did not return its row".to_string(),
175                    ));
176                }
177            }
178            Workload::Update => {
179                let n = timed!(b.execute(&upd, &[Value::Int(i % SEED_ROWS)]).await?);
180                if n != 1 {
181                    return Err(RelError::Backend(
182                        "update by key did not touch one row".to_string(),
183                    ));
184                }
185            }
186            Workload::Transaction => {
187                let t = Instant::now();
188                let mut tx = b.begin(crate::Isolation::ReadCommitted).await?;
189                tx.execute(&ins, &row(i)).await?;
190                tx.commit().await?;
191                us.push(u64::try_from(t.elapsed().as_nanos()).unwrap_or(u64::MAX));
192            }
193        }
194    }
195    Ok(Stats::from_samples(us))
196}