Files
Oxicloud/tools/perf-audit/gc_manifest_batch.rs
T
2026-07-22 02:06:29 +02:00

1141 lines
40 KiB
Rust

//! Reproducible audit harness for `DedupService::garbage_collect_with_grace`
//! phase 1. This is deliberately outside `benches/` and changes no production
//! code.
//!
//! It compares:
//! 1. the current production shape: one DELETE/RETURNING per 500 rows plus
//! one serial UPDATE for every returned manifest; and
//! 2. a proposed single CTE per batch that aggregates decrements by distinct
//! chunk hash before updating `storage.blobs`; and
//! 3. hybrid thresholds that retain the simple DELETE/RETURNING and aggregate
//! only the UPDATE after an exact per-manifest distinct pass in Rust.
//!
//! The runner creates a throw-away database. Within it this program keeps an
//! immutable fixture in `perf_audit.*` and restores the production-shaped
//! `storage.*` tables before every timed sample. Fixture/reset/validation time
//! is excluded from the reported duration.
use sqlx::postgres::PgPoolOptions;
use sqlx::{Connection, PgPool, Row};
use std::collections::{HashMap, HashSet};
use std::env;
use std::error::Error;
use std::time::{Duration, Instant};
const BATCH_SIZE: i64 = 500;
const CURRENT_DELETE: &str = r#"
DELETE FROM storage.chunk_manifests
WHERE ctid = ANY(
SELECT ctid FROM storage.chunk_manifests m
WHERE m.ref_count <= 0
OR NOT EXISTS (
SELECT 1 FROM storage.files f
WHERE f.blob_hash = m.file_hash
)
LIMIT $1
)
RETURNING file_hash, chunk_hashes, total_size
"#;
const CURRENT_UPDATE: &str = r#"
UPDATE storage.blobs
SET ref_count = GREATEST(ref_count - 1, 0),
orphaned_at = CASE
WHEN GREATEST(ref_count - 1, 0) = 0 THEN now()
ELSE orphaned_at
END
WHERE hash = ANY($1)
"#;
const AGGREGATED_UPDATE: &str = r#"
UPDATE storage.blobs b
SET ref_count = GREATEST(b.ref_count - d.decrement_by, 0),
orphaned_at = CASE
WHEN GREATEST(b.ref_count - d.decrement_by, 0) = 0 THEN now()
ELSE b.orphaned_at
END
FROM unnest($1::text[], $2::integer[]) AS d(hash, decrement_by)
WHERE b.hash = d.hash
"#;
// `SELECT DISTINCT` inside the LATERAL subquery is semantically important:
// production holds one blob reference per distinct chunk hash in a manifest,
// even when the same chunk occurs multiple times in that file. The current
// `hash = ANY($1)` likewise updates such a row only once per manifest.
const BATCHED_CTE: &str = r#"
WITH deleted AS MATERIALIZED (
DELETE FROM storage.chunk_manifests
WHERE ctid = ANY(
SELECT ctid FROM storage.chunk_manifests m
WHERE m.ref_count <= 0
OR NOT EXISTS (
SELECT 1 FROM storage.files f
WHERE f.blob_hash = m.file_hash
)
LIMIT $1
)
RETURNING file_hash, chunk_hashes, total_size
), decrements AS MATERIALIZED (
SELECT distinct_chunks.chunk_hash,
COUNT(*)::integer AS decrement_by
FROM deleted d
CROSS JOIN LATERAL (
SELECT DISTINCT chunk_hash
FROM unnest(d.chunk_hashes) AS chunks(chunk_hash)
) AS distinct_chunks
GROUP BY distinct_chunks.chunk_hash
), updated AS (
UPDATE storage.blobs b
SET ref_count = GREATEST(b.ref_count - d.decrement_by, 0),
orphaned_at = CASE
WHEN GREATEST(b.ref_count - d.decrement_by, 0) = 0 THEN now()
ELSE b.orphaned_at
END
FROM decrements d
WHERE b.hash = d.chunk_hash
RETURNING b.hash
)
SELECT deleted.file_hash,
deleted.chunk_hashes,
deleted.total_size,
(SELECT COUNT(*)::bigint FROM updated) AS updated_blob_rows
FROM deleted
"#;
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
enum AggregateMode {
OwnedHashMap,
BorrowedHashMap,
SortedBorrowed { occurrence_window: usize },
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
enum Algorithm {
Current,
Batched,
Hybrid {
aggregate_threshold: usize,
aggregate_mode: AggregateMode,
},
}
impl Algorithm {
fn label(self) -> &'static str {
match self {
Self::Current => "current_n_plus_1",
Self::Batched => "batched_cte",
Self::Hybrid {
aggregate_threshold: 2,
aggregate_mode: AggregateMode::OwnedHashMap,
} => "hybrid_n2",
Self::Hybrid {
aggregate_threshold: 4,
aggregate_mode: AggregateMode::OwnedHashMap,
} => "hybrid_n4",
Self::Hybrid {
aggregate_threshold: 8,
aggregate_mode: AggregateMode::OwnedHashMap,
} => "hybrid_n8",
Self::Hybrid {
aggregate_threshold: 32,
aggregate_mode: AggregateMode::OwnedHashMap,
} => "hybrid_n32",
Self::Hybrid {
aggregate_threshold: 500,
aggregate_mode: AggregateMode::OwnedHashMap,
} => "hybrid_n500",
Self::Hybrid {
aggregate_threshold: 2,
aggregate_mode: AggregateMode::BorrowedHashMap,
} => "hybrid_borrowed_n2",
Self::Hybrid {
aggregate_threshold: 4,
aggregate_mode: AggregateMode::BorrowedHashMap,
} => "hybrid_borrowed_n4",
Self::Hybrid {
aggregate_threshold: 8,
aggregate_mode: AggregateMode::BorrowedHashMap,
} => "hybrid_borrowed_n8",
Self::Hybrid {
aggregate_threshold: 32,
aggregate_mode: AggregateMode::BorrowedHashMap,
} => "hybrid_borrowed_n32",
Self::Hybrid {
aggregate_threshold: 500,
aggregate_mode: AggregateMode::BorrowedHashMap,
} => "hybrid_borrowed_n500",
Self::Hybrid {
aggregate_threshold: 2,
aggregate_mode:
AggregateMode::SortedBorrowed {
occurrence_window: usize::MAX,
},
} => "hybrid_sorted_n2",
Self::Hybrid {
aggregate_threshold: 4,
aggregate_mode:
AggregateMode::SortedBorrowed {
occurrence_window: usize::MAX,
},
} => "hybrid_sorted_n4",
Self::Hybrid {
aggregate_threshold: 8,
aggregate_mode:
AggregateMode::SortedBorrowed {
occurrence_window: usize::MAX,
},
} => "hybrid_sorted_n8",
Self::Hybrid {
aggregate_threshold: 32,
aggregate_mode:
AggregateMode::SortedBorrowed {
occurrence_window: usize::MAX,
},
} => "hybrid_sorted_n32",
Self::Hybrid {
aggregate_threshold: 500,
aggregate_mode:
AggregateMode::SortedBorrowed {
occurrence_window: usize::MAX,
},
} => "hybrid_sorted_n500",
Self::Hybrid {
aggregate_threshold: 32,
aggregate_mode:
AggregateMode::SortedBorrowed {
occurrence_window: 512,
},
} => "hybrid_sorted_n32_w512",
Self::Hybrid {
aggregate_threshold: 32,
aggregate_mode:
AggregateMode::SortedBorrowed {
occurrence_window: 1024,
},
} => "hybrid_sorted_n32_w1024",
Self::Hybrid {
aggregate_threshold: 32,
aggregate_mode:
AggregateMode::SortedBorrowed {
occurrence_window: 2048,
},
} => "hybrid_sorted_n32_w2048",
Self::Hybrid {
aggregate_threshold: 32,
aggregate_mode:
AggregateMode::SortedBorrowed {
occurrence_window: 4096,
},
} => "hybrid_sorted_n32_w4096",
Self::Hybrid { .. } => "hybrid_other",
}
}
}
#[derive(Debug)]
struct RunOutcome {
elapsed: Duration,
statements: u64,
deleted_manifests: u64,
logical_bytes: u64,
updated_blob_rows: u64,
checksum: u64,
}
#[derive(Debug)]
struct FixtureStats {
orphan_manifests: i64,
live_manifests: i64,
blob_rows: i64,
duplicate_manifests: i64,
orphan_logical_bytes: i64,
chunks_per_manifest: i64,
}
#[derive(Clone, Copy, Debug)]
struct Scenario {
orphan_manifests: i32,
live_manifests: i32,
}
fn parse_usize(name: &str, default: usize) -> Result<usize, Box<dyn Error>> {
match env::var(name) {
Ok(raw) => Ok(raw.parse::<usize>().map_err(|e| {
std::io::Error::other(format!("{name} must be an integer, got {raw:?}: {e}"))
})?),
Err(_) => Ok(default),
}
}
fn parse_counts() -> Result<Vec<i32>, Box<dyn Error>> {
let raw = env::var("GC_MANIFEST_COUNTS").unwrap_or_else(|_| "10000,50000".to_owned());
let counts: Result<Vec<_>, _> = raw
.split(',')
.map(str::trim)
.filter(|s| !s.is_empty())
.map(str::parse::<i32>)
.collect();
let counts = counts.map_err(|e| {
std::io::Error::other(format!(
"GC_MANIFEST_COUNTS must be comma-separated integers, got {raw:?}: {e}"
))
})?;
if counts.is_empty() || counts.iter().any(|&n| n <= 0) {
return Err(std::io::Error::other("manifest counts must all be positive").into());
}
Ok(counts)
}
fn parse_scenarios() -> Result<Vec<Scenario>, Box<dyn Error>> {
if let Ok(raw) = env::var("GC_SCENARIOS") {
let mut scenarios = Vec::new();
for value in raw.split(',').map(str::trim).filter(|s| !s.is_empty()) {
let (orphans, live) = value.split_once(':').ok_or_else(|| {
std::io::Error::other(format!(
"GC_SCENARIOS entries must be orphan:live pairs, got {value:?}"
))
})?;
let orphan_manifests = orphans.parse::<i32>().map_err(|e| {
std::io::Error::other(format!("invalid orphan count {orphans:?}: {e}"))
})?;
let live_manifests = live
.parse::<i32>()
.map_err(|e| std::io::Error::other(format!("invalid live count {live:?}: {e}")))?;
if orphan_manifests < 0 || live_manifests < 0 {
return Err(std::io::Error::other("scenario counts must be non-negative").into());
}
if orphan_manifests + live_manifests == 0 {
return Err(
std::io::Error::other("a scenario must contain at least one manifest").into(),
);
}
scenarios.push(Scenario {
orphan_manifests,
live_manifests,
});
}
if scenarios.is_empty() {
return Err(std::io::Error::other("GC_SCENARIOS must not be empty").into());
}
return Ok(scenarios);
}
Ok(parse_counts()?
.into_iter()
.map(|orphan_manifests| Scenario {
orphan_manifests,
live_manifests: (orphan_manifests / 100).max(10),
})
.collect())
}
fn parse_algorithms() -> Result<Vec<Algorithm>, Box<dyn Error>> {
let mut algorithms = Vec::new();
if env::var("GC_EXCLUDE_BASELINES").as_deref() != Ok("1") {
algorithms.push(Algorithm::Current);
if env::var("GC_INCLUDE_CTE").as_deref() != Ok("0") {
algorithms.push(Algorithm::Batched);
}
}
for (variable, aggregate_mode) in [
("GC_HYBRID_THRESHOLDS", AggregateMode::OwnedHashMap),
("GC_BORROWED_THRESHOLDS", AggregateMode::BorrowedHashMap),
(
"GC_SORTED_THRESHOLDS",
AggregateMode::SortedBorrowed {
occurrence_window: usize::MAX,
},
),
] {
let Ok(raw) = env::var(variable) else {
continue;
};
for threshold in raw.split(',').map(str::trim).filter(|s| !s.is_empty()) {
let threshold = threshold.parse::<usize>().map_err(|e| {
std::io::Error::other(format!("invalid hybrid threshold {threshold:?}: {e}"))
})?;
if ![2, 4, 8, 32, 500].contains(&threshold) {
return Err(std::io::Error::other(
"hybrid thresholds are limited to the audited set: 2,4,8,32,500",
)
.into());
}
let algorithm = Algorithm::Hybrid {
aggregate_threshold: threshold,
aggregate_mode,
};
if !algorithms.contains(&algorithm) {
algorithms.push(algorithm);
}
}
}
if let Ok(raw) = env::var("GC_SORTED_WINDOWS") {
for window in raw.split(',').map(str::trim).filter(|s| !s.is_empty()) {
let occurrence_window = window.parse::<usize>().map_err(|e| {
std::io::Error::other(format!("invalid sorted occurrence window {window:?}: {e}"))
})?;
if ![512, 1024, 2048, 4096].contains(&occurrence_window) {
return Err(std::io::Error::other(
"sorted occurrence windows are limited to 512,1024,2048,4096",
)
.into());
}
algorithms.push(Algorithm::Hybrid {
aggregate_threshold: 32,
aggregate_mode: AggregateMode::SortedBorrowed { occurrence_window },
});
}
}
if algorithms.is_empty() {
return Err(std::io::Error::other("at least one GC algorithm must be selected").into());
}
Ok(algorithms)
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn Error>> {
let database_url = env::var("DATABASE_URL").map_err(|_| {
std::io::Error::other("DATABASE_URL must point at the throw-away benchmark database")
})?;
let scenarios = parse_scenarios()?;
let algorithms = parse_algorithms()?;
let chunks_per_manifest = parse_usize("GC_CHUNKS_PER_MANIFEST", 16)?;
let shared_percent = parse_usize("GC_SHARED_PERCENT", 50)?;
let shared_pool = parse_usize("GC_SHARED_POOL", 512)?;
let warmups = parse_usize("GC_WARMUPS", 1)?;
let samples = parse_usize("GC_SAMPLES", 5)?;
if chunks_per_manifest < 2 || chunks_per_manifest > 128 {
return Err(std::io::Error::other("GC_CHUNKS_PER_MANIFEST must be 2..=128").into());
}
if shared_percent == 0 || shared_percent >= 100 {
return Err(std::io::Error::other("GC_SHARED_PERCENT must be 1..=99").into());
}
if shared_pool == 0 || samples == 0 {
return Err(std::io::Error::other("shared pool and samples must be > 0").into());
}
// Probe one direct connection first so authentication/network failures are
// reported with their real cause instead of the pool's generic timeout.
let probe = sqlx::postgres::PgConnection::connect(&database_url).await?;
probe.close().await?;
let pool = PgPoolOptions::new()
.max_connections(4)
.connect(&database_url)
.await?;
println!("dedup GC phase-1 benchmark");
println!("database : {database_url}");
println!("batch size : {BATCH_SIZE}");
println!("chunks/manifest : {chunks_per_manifest}");
println!("shared chunks : {shared_percent}% (pool={shared_pool})");
println!("duplicate control : every 10th manifest repeats one chunk");
println!("warmups / samples : {warmups} / {samples}");
println!(
"algorithms : {}",
algorithms
.iter()
.map(|algorithm| algorithm.label())
.collect::<Vec<_>>()
.join(", ")
);
create_schema(&pool).await?;
for scenario in scenarios {
build_template(
&pool,
scenario.orphan_manifests,
scenario.live_manifests,
chunks_per_manifest as i32,
shared_percent as i32,
shared_pool as i32,
)
.await?;
let stats = fixture_stats(&pool).await?;
println!("\nfixture");
println!(" orphan manifests : {}", stats.orphan_manifests);
println!(" live controls : {}", stats.live_manifests);
println!(" unique blobs : {}", stats.blob_rows);
println!(" repeated-chunk manifests: {}", stats.duplicate_manifests);
for _ in 0..warmups {
for &algorithm in &algorithms {
reset_fixture(&pool).await?;
let warm = run_algorithm(&pool, algorithm).await?;
validate(&pool, &stats, algorithm, &warm).await?;
}
}
let mut outcomes: Vec<(Algorithm, Vec<RunOutcome>)> = algorithms
.iter()
.copied()
.map(|algorithm| (algorithm, Vec::with_capacity(samples)))
.collect();
for sample in 0..samples {
// Rotate the complete candidate set so no algorithm owns a
// systematic hot/cold or checkpoint position. Rotation alone is
// important for a two-way A/B: reversing after a one-step rotation
// would accidentally restore the original order.
let mut order = algorithms.clone();
let order_len = order.len();
order.rotate_left(sample % order_len);
for algorithm in order {
reset_fixture(&pool).await?;
let outcome = run_algorithm(&pool, algorithm).await?;
validate(&pool, &stats, algorithm, &outcome).await?;
println!(
" sample {:>2} {:>18}: {:>10.3} ms, {:>7} statements",
sample + 1,
algorithm.label(),
outcome.elapsed.as_secs_f64() * 1_000.0,
outcome.statements,
);
outcomes
.iter_mut()
.find(|(candidate, _)| *candidate == algorithm)
.expect("algorithm outcome bucket")
.1
.push(outcome);
}
}
if let Some((_, current)) = outcomes
.iter()
.find(|(algorithm, _)| *algorithm == Algorithm::Current)
{
for (algorithm, candidate) in &outcomes {
if *algorithm != Algorithm::Current {
print_comparison(current, *algorithm, candidate);
}
}
} else {
for (algorithm, outcome) in &outcomes {
print_absolute(*algorithm, outcome);
}
}
}
pool.close().await;
Ok(())
}
async fn create_schema(pool: &PgPool) -> Result<(), sqlx::Error> {
// The runner uses a dedicated throw-away database, so owning the canonical
// `storage` schema here cannot collide with a live OxiCloud instance.
sqlx::query("DROP SCHEMA IF EXISTS storage CASCADE")
.execute(pool)
.await?;
sqlx::query("DROP SCHEMA IF EXISTS perf_audit CASCADE")
.execute(pool)
.await?;
sqlx::query("CREATE SCHEMA storage").execute(pool).await?;
sqlx::query("CREATE SCHEMA perf_audit")
.execute(pool)
.await?;
sqlx::query(
"CREATE TABLE storage.blobs (
hash VARCHAR(64) PRIMARY KEY,
size BIGINT NOT NULL,
ref_count INTEGER NOT NULL CHECK (ref_count >= 0),
orphaned_at TIMESTAMPTZ
)",
)
.execute(pool)
.await?;
sqlx::query(
"CREATE INDEX idx_blobs_gc_eligible
ON storage.blobs (orphaned_at) WHERE ref_count = 0",
)
.execute(pool)
.await?;
sqlx::query(
"CREATE TABLE storage.chunk_manifests (
file_hash VARCHAR(64) PRIMARY KEY,
chunk_hashes TEXT[] NOT NULL,
chunk_sizes BIGINT[] NOT NULL,
total_size BIGINT NOT NULL,
chunk_count INTEGER NOT NULL,
content_type TEXT,
ref_count INTEGER NOT NULL CHECK (ref_count >= 0)
)",
)
.execute(pool)
.await?;
sqlx::query(
"CREATE INDEX idx_chunk_manifests_ref_count_zero
ON storage.chunk_manifests (file_hash) WHERE ref_count = 0",
)
.execute(pool)
.await?;
sqlx::query(
"CREATE TABLE storage.files (
blob_hash VARCHAR(64) NOT NULL,
is_trashed BOOLEAN NOT NULL DEFAULT FALSE
)",
)
.execute(pool)
.await?;
sqlx::query("CREATE INDEX idx_files_blob_hash ON storage.files (blob_hash)")
.execute(pool)
.await?;
sqlx::query(
"CREATE TABLE perf_audit.manifests (
file_hash VARCHAR(64) PRIMARY KEY,
chunk_hashes TEXT[] NOT NULL,
chunk_sizes BIGINT[] NOT NULL,
total_size BIGINT NOT NULL,
chunk_count INTEGER NOT NULL,
content_type TEXT,
ref_count INTEGER NOT NULL
)",
)
.execute(pool)
.await?;
sqlx::query(
"CREATE TABLE perf_audit.files (
blob_hash VARCHAR(64) NOT NULL
)",
)
.execute(pool)
.await?;
sqlx::query(
"CREATE TABLE perf_audit.blobs (
hash VARCHAR(64) PRIMARY KEY,
size BIGINT NOT NULL,
ref_count INTEGER NOT NULL,
expected_after INTEGER NOT NULL
)",
)
.execute(pool)
.await?;
Ok(())
}
async fn build_template(
pool: &PgPool,
orphan_count: i32,
live_count: i32,
chunks: i32,
shared_percent: i32,
shared_pool: i32,
) -> Result<(), sqlx::Error> {
sqlx::query("TRUNCATE perf_audit.files, perf_audit.manifests, perf_audit.blobs")
.execute(pool)
.await?;
let shared_slots = (chunks * shared_percent / 100).clamp(1, chunks - 1);
insert_manifests(
pool,
"orphan",
orphan_count,
chunks,
shared_slots,
shared_pool,
0,
)
.await?;
insert_manifests(
pool,
"live",
live_count,
chunks,
shared_slots,
shared_pool,
1,
)
.await?;
sqlx::query(
"INSERT INTO perf_audit.files (blob_hash)
SELECT file_hash FROM perf_audit.manifests WHERE ref_count > 0",
)
.execute(pool)
.await?;
// Count one reference per DISTINCT chunk hash per manifest. This mirrors
// `ChunkIngestOutcome::distinct_hashes()` in the production ingest path.
sqlx::query(
"INSERT INTO perf_audit.blobs (hash, size, ref_count, expected_after)
SELECT d.chunk_hash,
65536,
COUNT(*)::integer,
COUNT(*) FILTER (WHERE m.ref_count > 0)::integer
FROM perf_audit.manifests m
CROSS JOIN LATERAL (
SELECT DISTINCT chunk_hash
FROM unnest(m.chunk_hashes) AS chunks(chunk_hash)
) d
GROUP BY d.chunk_hash",
)
.execute(pool)
.await?;
Ok(())
}
async fn insert_manifests(
pool: &PgPool,
kind: &str,
count: i32,
chunks: i32,
shared_slots: i32,
shared_pool: i32,
ref_count: i32,
) -> Result<(), sqlx::Error> {
sqlx::query(
"INSERT INTO perf_audit.manifests
(file_hash, chunk_hashes, chunk_sizes, total_size,
chunk_count, content_type, ref_count)
SELECT $1 || '-file-' || i::text,
ARRAY(
SELECT CASE
-- Duplicate control: ANY($1) updates this hash once,
-- not twice, in the current implementation.
WHEN i % 10 = 0 AND slot = $3 - 1
THEN 'shared-' || ((i * 17) % $5)::text
WHEN slot < $4
THEN 'shared-' || ((i * 17 + slot * 31) % $5)::text
ELSE 'unique-' || $1 || '-' || i::text || '-' || slot::text
END
FROM generate_series(0, $3 - 1) AS slots(slot)
ORDER BY slot
),
array_fill(65536::bigint, ARRAY[$3]),
$3::bigint * 65536,
$3,
'application/octet-stream',
$6
FROM generate_series(1, $2) AS manifests(i)",
)
.bind(kind)
.bind(count)
.bind(chunks)
.bind(shared_slots)
.bind(shared_pool)
.bind(ref_count)
.execute(pool)
.await?;
Ok(())
}
async fn fixture_stats(pool: &PgPool) -> Result<FixtureStats, sqlx::Error> {
let row = sqlx::query(
"SELECT
COUNT(*) FILTER (WHERE ref_count = 0)::bigint AS orphans,
COUNT(*) FILTER (WHERE ref_count > 0)::bigint AS live,
(SELECT COUNT(*)::bigint FROM perf_audit.blobs) AS blobs,
COUNT(*) FILTER (
WHERE cardinality(chunk_hashes)
> (SELECT COUNT(DISTINCT h) FROM unnest(chunk_hashes) AS x(h))
)::bigint AS duplicate_manifests,
COALESCE(SUM(total_size) FILTER (WHERE ref_count = 0), 0)::bigint
AS orphan_logical_bytes,
COALESCE(MAX(chunk_count), 0)::bigint AS chunks_per_manifest
FROM perf_audit.manifests",
)
.fetch_one(pool)
.await?;
Ok(FixtureStats {
orphan_manifests: row.try_get("orphans")?,
live_manifests: row.try_get("live")?,
blob_rows: row.try_get("blobs")?,
duplicate_manifests: row.try_get("duplicate_manifests")?,
orphan_logical_bytes: row.try_get("orphan_logical_bytes")?,
chunks_per_manifest: row.try_get("chunks_per_manifest")?,
})
}
async fn reset_fixture(pool: &PgPool) -> Result<(), sqlx::Error> {
sqlx::query("TRUNCATE storage.files, storage.chunk_manifests, storage.blobs")
.execute(pool)
.await?;
sqlx::query(
"INSERT INTO storage.blobs (hash, size, ref_count, orphaned_at)
SELECT hash, size, ref_count, NULL FROM perf_audit.blobs",
)
.execute(pool)
.await?;
sqlx::query(
"INSERT INTO storage.chunk_manifests
(file_hash, chunk_hashes, chunk_sizes, total_size,
chunk_count, content_type, ref_count)
SELECT file_hash, chunk_hashes, chunk_sizes, total_size,
chunk_count, content_type, ref_count
FROM perf_audit.manifests",
)
.execute(pool)
.await?;
sqlx::query(
"INSERT INTO storage.files (blob_hash, is_trashed)
SELECT blob_hash, FALSE FROM perf_audit.files",
)
.execute(pool)
.await?;
sqlx::query("ANALYZE storage.files, storage.chunk_manifests, storage.blobs")
.execute(pool)
.await?;
Ok(())
}
async fn run_algorithm(pool: &PgPool, algorithm: Algorithm) -> Result<RunOutcome, sqlx::Error> {
let started = Instant::now();
let mut statements = 0u64;
let mut deleted_manifests = 0u64;
let mut logical_bytes = 0u64;
let mut updated_blob_rows = 0u64;
let mut checksum = 0u64;
loop {
match algorithm {
Algorithm::Current => {
let batch: Vec<(String, Vec<String>, i64)> = sqlx::query_as(CURRENT_DELETE)
.bind(BATCH_SIZE)
.fetch_all(pool)
.await?;
statements += 1;
if batch.is_empty() {
break;
}
for (file_hash, chunk_hashes, size) in batch {
let affected = sqlx::query(CURRENT_UPDATE)
.bind(&chunk_hashes)
.execute(pool)
.await?
.rows_affected();
statements += 1;
deleted_manifests += 1;
logical_bytes += size as u64;
updated_blob_rows += affected;
checksum = checksum.wrapping_add(file_hash.len() as u64);
}
}
Algorithm::Batched => {
let batch: Vec<(String, Vec<String>, i64, i64)> = sqlx::query_as(BATCHED_CTE)
.bind(BATCH_SIZE)
.fetch_all(pool)
.await?;
statements += 1;
if batch.is_empty() {
break;
}
let batch_updated = batch[0].3 as u64;
updated_blob_rows += batch_updated;
for (file_hash, _chunk_hashes, size, _) in batch {
deleted_manifests += 1;
logical_bytes += size as u64;
checksum = checksum.wrapping_add(file_hash.len() as u64);
}
}
Algorithm::Hybrid {
aggregate_threshold,
aggregate_mode,
} => {
let batch: Vec<(String, Vec<String>, i64)> = sqlx::query_as(CURRENT_DELETE)
.bind(BATCH_SIZE)
.fetch_all(pool)
.await?;
statements += 1;
if batch.is_empty() {
break;
}
if batch.len() < aggregate_threshold {
for (_, chunk_hashes, _) in &batch {
let affected = sqlx::query(CURRENT_UPDATE)
.bind(chunk_hashes)
.execute(pool)
.await?
.rows_affected();
statements += 1;
updated_blob_rows += affected;
}
} else {
match aggregate_mode {
AggregateMode::OwnedHashMap | AggregateMode::BorrowedHashMap => {
let mut decrements = HashMap::<&str, i32>::new();
for (_, chunk_hashes, _) in &batch {
let distinct: HashSet<&str> =
chunk_hashes.iter().map(String::as_str).collect();
for hash in distinct {
*decrements.entry(hash).or_default() += 1;
}
}
if aggregate_mode == AggregateMode::BorrowedHashMap {
let (hashes, decrement_by): (Vec<&str>, Vec<i32>) =
decrements.into_iter().unzip();
updated_blob_rows += sqlx::query(AGGREGATED_UPDATE)
.bind(&hashes)
.bind(&decrement_by)
.execute(pool)
.await?
.rows_affected();
} else {
let (hashes, decrement_by): (Vec<String>, Vec<i32>) = decrements
.into_iter()
.map(|(hash, decrement)| (hash.to_owned(), decrement))
.unzip();
updated_blob_rows += sqlx::query(AGGREGATED_UPDATE)
.bind(&hashes)
.bind(&decrement_by)
.execute(pool)
.await?
.rows_affected();
}
}
AggregateMode::SortedBorrowed { occurrence_window } => {
let (affected, update_statements) =
run_sorted_updates(pool, &batch, occurrence_window).await?;
updated_blob_rows += affected;
// The common one-window case is accounted below.
statements += update_statements - 1;
}
}
statements += 1;
}
for (file_hash, _chunk_hashes, size) in batch {
deleted_manifests += 1;
logical_bytes += size as u64;
checksum = checksum.wrapping_add(file_hash.len() as u64);
}
}
}
}
Ok(RunOutcome {
elapsed: started.elapsed(),
statements,
deleted_manifests,
logical_bytes,
updated_blob_rows,
checksum,
})
}
async fn run_sorted_updates(
pool: &PgPool,
batch: &[(String, Vec<String>, i64)],
occurrence_window: usize,
) -> Result<(u64, u64), sqlx::Error> {
let mut first = 0usize;
let mut affected = 0u64;
let mut statements = 0u64;
while first < batch.len() {
let mut end = first;
let mut occurrences = 0usize;
while end < batch.len() {
let next = batch[end].1.len();
if end > first && occurrences.saturating_add(next) > occurrence_window {
break;
}
occurrences = occurrences.saturating_add(next);
end += 1;
if occurrences >= occurrence_window {
break;
}
}
let group = &batch[first..end];
if group.len() == 1 {
affected += sqlx::query(CURRENT_UPDATE)
.bind(&group[0].1)
.execute(pool)
.await?
.rows_affected();
} else {
let mut all_hashes = Vec::<&str>::with_capacity(occurrences);
let mut per_manifest = Vec::<&str>::new();
for (_, chunk_hashes, _) in group {
per_manifest.clear();
per_manifest.extend(chunk_hashes.iter().map(String::as_str));
per_manifest.sort_unstable();
per_manifest.dedup();
all_hashes.extend_from_slice(&per_manifest);
}
all_hashes.sort_unstable();
let mut hashes = Vec::<&str>::with_capacity(all_hashes.len());
let mut decrement_by = Vec::<i32>::with_capacity(all_hashes.len());
for hash in all_hashes {
if hashes.last().copied() == Some(hash) {
*decrement_by.last_mut().expect("count for existing hash") += 1;
} else {
hashes.push(hash);
decrement_by.push(1);
}
}
affected += sqlx::query(AGGREGATED_UPDATE)
.bind(&hashes)
.bind(&decrement_by)
.execute(pool)
.await?
.rows_affected();
}
statements += 1;
first = end;
}
Ok((affected, statements))
}
async fn validate(
pool: &PgPool,
stats: &FixtureStats,
algorithm: Algorithm,
outcome: &RunOutcome,
) -> Result<(), Box<dyn Error>> {
let row = sqlx::query(
"SELECT
(SELECT COUNT(*)::bigint
FROM storage.chunk_manifests m
WHERE m.ref_count <= 0
OR NOT EXISTS (
SELECT 1 FROM storage.files f
WHERE f.blob_hash = m.file_hash
)) AS remaining_collectible,
(SELECT COUNT(*)::bigint FROM storage.chunk_manifests) AS remaining_live,
(SELECT COUNT(*)::bigint
FROM storage.blobs b
JOIN perf_audit.blobs expected USING (hash)
WHERE b.ref_count <> expected.expected_after
OR (expected.expected_after = 0 AND b.orphaned_at IS NULL)
OR (expected.expected_after > 0 AND b.orphaned_at IS NOT NULL)
) AS ref_mismatches,
(SELECT COUNT(*)::bigint FROM storage.blobs WHERE ref_count < 0) AS underflows",
)
.fetch_one(pool)
.await?;
let remaining_collectible: i64 = row.try_get("remaining_collectible")?;
let remaining_live: i64 = row.try_get("remaining_live")?;
let ref_mismatches: i64 = row.try_get("ref_mismatches")?;
let underflows: i64 = row.try_get("underflows")?;
let expected_bytes = stats.orphan_logical_bytes as u64;
let successful_batches = (stats.orphan_manifests as u64).div_ceil(BATCH_SIZE as u64);
let expected_statements = successful_batches
+ 1
+ match algorithm {
// The historical implementation issued one UPDATE per returned
// manifest. The CTE has no per-row statement.
Algorithm::Current => stats.orphan_manifests as u64,
Algorithm::Batched => 0,
Algorithm::Hybrid {
aggregate_threshold,
aggregate_mode,
} => {
let mut remaining = stats.orphan_manifests as u64;
let mut updates = 0;
while remaining > 0 {
let batch = remaining.min(BATCH_SIZE as u64);
updates += if batch < aggregate_threshold as u64 {
batch
} else if let AggregateMode::SortedBorrowed { occurrence_window } =
aggregate_mode
{
let manifests_per_window = if occurrence_window == usize::MAX {
BATCH_SIZE as u64
} else {
(occurrence_window as u64 / stats.chunks_per_manifest as u64).max(1)
};
batch.div_ceil(manifests_per_window)
} else {
1
};
remaining -= batch;
}
updates
}
};
if remaining_collectible != 0
|| remaining_live != stats.live_manifests
|| ref_mismatches != 0
|| underflows != 0
|| outcome.deleted_manifests != stats.orphan_manifests as u64
|| outcome.logical_bytes != expected_bytes
|| (stats.orphan_manifests > 0 && outcome.checksum == 0)
|| (stats.orphan_manifests > 0 && outcome.updated_blob_rows == 0)
|| outcome.statements != expected_statements
{
return Err(std::io::Error::other(format!(
"correctness failure: remaining_collectible={remaining_collectible}, \
remaining_live={remaining_live}/{}, ref_mismatches={ref_mismatches}, \
underflows={underflows}, deleted={}/{}, bytes={}/{expected_bytes}, \
updated_blob_rows={}, checksum={}, statements={}/{expected_statements}",
stats.live_manifests,
outcome.deleted_manifests,
stats.orphan_manifests,
outcome.logical_bytes,
outcome.updated_blob_rows,
outcome.checksum,
outcome.statements,
))
.into());
}
Ok(())
}
fn percentile_ms(outcomes: &[RunOutcome], percentile: f64) -> f64 {
let mut values: Vec<f64> = outcomes
.iter()
.map(|o| o.elapsed.as_secs_f64() * 1_000.0)
.collect();
values.sort_by(f64::total_cmp);
let index = ((values.len() as f64 * percentile).ceil() as usize)
.saturating_sub(1)
.min(values.len() - 1);
values[index]
}
fn print_comparison(
current: &[RunOutcome],
candidate_algorithm: Algorithm,
candidate: &[RunOutcome],
) {
let cur_median = percentile_ms(current, 0.50);
let cur_p95 = percentile_ms(current, 0.95);
let new_median = percentile_ms(candidate, 0.50);
let new_p95 = percentile_ms(candidate, 0.95);
let speedup = cur_median / new_median;
let reduction = (1.0 - new_median / cur_median) * 100.0;
let current_statements = current[0].statements;
let candidate_statements = candidate[0].statements;
println!("comparison: {}", candidate_algorithm.label());
println!(
" {:>18}: median={:>10.3} ms p95={:>10.3} ms statements={current_statements}",
Algorithm::Current.label(),
cur_median,
cur_p95,
);
println!(
" {:>18}: median={:>10.3} ms p95={:>10.3} ms statements={candidate_statements}",
candidate_algorithm.label(),
new_median,
new_p95,
);
println!(" median delta : {reduction:.2}% faster ({speedup:.2}x)");
println!(
" statement delta : {:.2}% fewer ({current_statements} -> {candidate_statements})",
(1.0 - candidate_statements as f64 / current_statements as f64) * 100.0,
);
println!(" correctness : PASS for every warmup and measured sample");
}
fn print_absolute(algorithm: Algorithm, outcomes: &[RunOutcome]) {
println!("summary: {}", algorithm.label());
println!(
" median={:.3} ms p95={:.3} ms statements={}",
percentile_ms(outcomes, 0.50),
percentile_ms(outcomes, 0.95),
outcomes[0].statements,
);
println!(" correctness : PASS for every warmup and measured sample");
}