use anyhow::{Context, Result};
use object_store::memory::InMemory;
use std::path::PathBuf;
use std::sync::Arc;
use std::time::Instant;
use turso_backup::snapshot::{snapshot_and_upload, BackupTarget, SnapshotOutcome};
use turso_backup::stream::{restore_latest_stream, tail_frames, CoreWalSeam, StreamConfig};
const PAGE_SIZE: usize = 4096;
#[tokio::main(flavor = "current_thread")]
async fn main() -> Result<()> {
let max_dbs = env_usize("RSS_HARNESS_MAX_DBS", 40);
let rows_per_db = env_usize("RSS_HARNESS_ROWS", 40).max(2);
let workdir = std::env::var("RSS_HARNESS_WORKDIR")
.map(PathBuf::from)
.unwrap_or_else(|_| {
std::env::temp_dir().join(format!("turso-backup-rss-harness-{}", std::process::id()))
});
std::fs::create_dir_all(&workdir)
.with_context(|| format!("creating workdir {}", workdir.display()))?;
let mut appliers: Vec<CoreWalSeam> = Vec::with_capacity(max_dbs);
let mut samples: Vec<(f64, f64)> = Vec::with_capacity(max_dbs);
let mut prev_rss_kb: Option<i64> = None;
let started = Instant::now();
for i in 1..=max_dbs {
let step_started = Instant::now();
let src_path = workdir.join(format!("src-{i}.db"));
let dest_path = workdir.join(format!("dest-{i}.db"));
let src_path = src_path.to_str().context("src path is not valid UTF-8")?;
let dest_path = dest_path.to_str().context("dest path is not valid UTF-8")?;
seed_rows(src_path, 0, rows_per_db as i64)
.await
.with_context(|| format!("seeding base rows for db #{i}"))?;
let target = BackupTarget {
store: Arc::new(InMemory::new()),
prefix: format!("tenant-{i}"),
};
let base_key = match snapshot_and_upload(src_path, &target)
.await
.with_context(|| format!("snapshot_and_upload for db #{i}"))?
{
SnapshotOutcome::Uploaded { key, .. } => key,
other => anyhow::bail!("db #{i}: expected a fresh Uploaded snapshot, got {other:?}"),
};
checkpoint_truncate(src_path)
.await
.with_context(|| format!("checkpointing db #{i}"))?;
seed_rows(src_path, 1_000_000 * i as i64, (rows_per_db / 2).max(1) as i64)
.await
.with_context(|| format!("seeding post-checkpoint rows for db #{i}"))?;
{
let seam = CoreWalSeam::open(src_path)
.with_context(|| format!("opening source WAL seam for db #{i}"))?;
let cfg = StreamConfig {
base_snapshot_key: &base_key,
page_size: PAGE_SIZE,
backpressure: turso_backup::backpressure::BackpressureConfig::default(),
rpo_target: None,
epoch: 0,
owner: None,
pointer_generation: 0,
};
tail_frames(&seam, &target, &cfg)
.await
.with_context(|| format!("tail_frames for db #{i}"))?;
}
let outcome = restore_latest_stream(&target, dest_path)
.await
.with_context(|| format!("restore_latest_stream for db #{i}"))?;
let applier = CoreWalSeam::open(dest_path)
.with_context(|| format!("attaching applier for db #{i}"))?;
appliers.push(applier);
for sfx in ["", "-wal", "-shm"] {
let _ = std::fs::remove_file(format!("{src_path}{sfx}"));
}
let rss_kb = read_rss_kb().context("reading process RSS via `ps`")?;
let delta_kb = prev_rss_kb.map(|p| rss_kb - p);
prev_rss_kb = Some(rss_kb);
samples.push((i as f64, rss_kb as f64));
println!(
"{{\"db_count\":{i},\"rss_kb\":{rss_kb},\"delta_kb\":{},\"frames_replayed\":{},\"elapsed_ms\":{},\"total_elapsed_ms\":{}}}",
delta_kb.map(|d| d.to_string()).unwrap_or_else(|| "null".to_string()),
outcome.frames_replayed,
step_started.elapsed().as_millis(),
started.elapsed().as_millis(),
);
}
if let Some((slope, intercept)) = linear_fit(&samples) {
println!(
"{{\"summary\":true,\"slope_kb_per_db\":{slope:.3},\"intercept_kb\":{intercept:.3},\"samples\":{}}}",
samples.len()
);
}
drop(appliers);
for i in 1..=max_dbs {
for sfx in ["", "-wal", "-shm"] {
let _ = std::fs::remove_file(workdir.join(format!("dest-{i}.db{sfx}")));
}
}
let _ = std::fs::remove_dir(&workdir);
Ok(())
}
async fn seed_rows(path: &str, start: i64, count: i64) -> Result<()> {
let db = turso::Builder::new_local(path)
.build()
.await
.with_context(|| format!("opening {path}"))?;
let conn = db.connect().with_context(|| format!("connecting to {path}"))?;
conn.execute(
"CREATE TABLE IF NOT EXISTS t (id INTEGER PRIMARY KEY, v TEXT)",
(),
)
.await
.context("create table")?;
conn.execute("BEGIN", ()).await.context("begin")?;
for i in start..start + count {
conn.execute("INSERT INTO t (id, v) VALUES (?, ?)", (i, format!("v{i}")))
.await
.with_context(|| format!("insert row {i}"))?;
}
conn.execute("COMMIT", ()).await.context("commit")?;
Ok(())
}
async fn checkpoint_truncate(path: &str) -> Result<()> {
let db = turso::Builder::new_local(path)
.build()
.await
.with_context(|| format!("opening {path} for checkpoint"))?;
let conn = db.connect().with_context(|| format!("connecting to {path} for checkpoint"))?;
let mut rows = conn
.query("PRAGMA wal_checkpoint(TRUNCATE)", ())
.await
.context("wal_checkpoint(TRUNCATE)")?;
while rows.next().await.context("draining checkpoint rows")?.is_some() {}
Ok(())
}
fn read_rss_kb() -> Result<i64> {
let pid = std::process::id().to_string();
let out = std::process::Command::new("ps")
.args(["-o", "rss=", "-p", &pid])
.output()
.context("spawning `ps`")?;
anyhow::ensure!(out.status.success(), "`ps` exited with {:?}", out.status);
let text = String::from_utf8_lossy(&out.stdout);
text.trim()
.parse::<i64>()
.with_context(|| format!("parsing `ps -o rss=` output {text:?}"))
}
fn env_usize(key: &str, default: usize) -> usize {
std::env::var(key)
.ok()
.and_then(|v| v.parse().ok())
.unwrap_or(default)
}
fn linear_fit(points: &[(f64, f64)]) -> Option<(f64, f64)> {
let n = points.len() as f64;
if points.len() < 2 {
return None;
}
let sum_x: f64 = points.iter().map(|(x, _)| x).sum();
let sum_y: f64 = points.iter().map(|(_, y)| y).sum();
let sum_xy: f64 = points.iter().map(|(x, y)| x * y).sum();
let sum_xx: f64 = points.iter().map(|(x, _)| x * x).sum();
let denom = n * sum_xx - sum_x * sum_x;
if denom.abs() < f64::EPSILON {
return None;
}
let slope = (n * sum_xy - sum_x * sum_y) / denom;
let intercept = (sum_y - slope * sum_x) / n;
Some((slope, intercept))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn linear_fit_recovers_a_known_line() {
let points: Vec<(f64, f64)> = (1..=10).map(|x| (x as f64, 2.0 * x as f64 + 10.0)).collect();
let (slope, intercept) = linear_fit(&points).unwrap();
assert!((slope - 2.0).abs() < 1e-9, "slope = {slope}");
assert!((intercept - 10.0).abs() < 1e-9, "intercept = {intercept}");
}
#[test]
fn linear_fit_tolerates_noise_and_reports_a_reasonable_slope() {
let points = vec![
(1.0, 105.0),
(2.0, 109.0),
(3.0, 116.0),
(4.0, 119.0),
(5.0, 126.0),
];
let (slope, intercept) = linear_fit(&points).unwrap();
assert!((4.0..6.0).contains(&slope), "slope = {slope}");
assert!((90.0..110.0).contains(&intercept), "intercept = {intercept}");
}
#[test]
fn linear_fit_none_below_two_points() {
assert_eq!(linear_fit(&[]), None);
assert_eq!(linear_fit(&[(1.0, 1.0)]), None);
}
#[test]
fn linear_fit_none_for_degenerate_vertical_points() {
assert_eq!(linear_fit(&[(3.0, 1.0), (3.0, 5.0), (3.0, 9.0)]), None);
}
#[test]
fn env_usize_uses_default_when_unset() {
assert_eq!(env_usize("RSS_HARNESS_TEST_VAR_NOT_SET", 7), 7);
}
#[test]
fn env_usize_parses_a_set_value() {
std::env::set_var("RSS_HARNESS_TEST_VAR_SET", "99");
assert_eq!(env_usize("RSS_HARNESS_TEST_VAR_SET", 7), 99);
std::env::remove_var("RSS_HARNESS_TEST_VAR_SET");
}
#[test]
fn read_rss_kb_returns_a_positive_value() {
let rss = read_rss_kb().unwrap();
assert!(rss > 0, "expected a positive RSS reading, got {rss}");
}
}