Files
SoulStream/crates/metadata/examples/album_info_bench.rs
T
KeelrLeek a667b4b28e Baseline: full tree snapshot after git repository loss
The previous .git directory was found gutted (only info/ remained), so
history is unrecoverable locally. This commit restores version control
with the tree exactly as it stands: the working app, the preview-tracks
launcher on the artist hero, the scrollable-track-previews design bundle
(docs + approved mockups), and the Phase 0 validation artifacts.
2026-07-22 17:57:00 -05:00

124 lines
4.1 KiB
Rust

//! Latency harness for the album-description path (`album_info_for_artist_id`).
//!
//! This is the description counterpart to `discography_audit`: it measures the
//! call the artist page's expander and background hint harvest both make, so
//! optimization work has a before/after number instead of an impression.
//!
//! Usage:
//! cargo run -p metadata --example album_info_bench -- "Artist" "Title" "Title" ...
//!
//! Reports, in order:
//! 1. COLD SEQUENTIAL — one fresh client, titles one at a time. This is what
//! the frontend harvest does today (`for (const title of titles) await`),
//! so its total is the wall-clock the user waits for the tabs to settle.
//! 2. WARM SEQUENTIAL — same client again, exercising the per-source caches.
//! 3. COLD CONCURRENT — a second fresh client, all titles at once. The gap
//! against (1) is the ceiling available from parallelizing the harvest.
use metadata::MetadataClient;
use std::sync::Arc;
use std::time::Instant;
#[tokio::main]
async fn main() {
let mut args = std::env::args().skip(1);
let Some(artist) = args.next() else {
eprintln!("usage: album_info_bench -- \"Artist\" \"Title\" [\"Title\" ...]");
return;
};
let titles: Vec<String> = args.collect();
if titles.is_empty() {
eprintln!("give at least one album title");
return;
}
let client = Arc::new(MetadataClient::new());
let search = client.catalog_search(&artist).await;
let artist_id = search
.artists
.iter()
.find(|a| normalize(&a.name) == normalize(&artist))
.map(|a| a.id);
println!(
"artist {artist:?} -> id {artist_id:?}, {} titles\n",
titles.len()
);
let cold = run_sequential(&client, artist_id, &artist, &titles, "COLD SEQUENTIAL").await;
let warm = run_sequential(&client, artist_id, &artist, &titles, "WARM SEQUENTIAL").await;
// A second client so the concurrent run is genuinely cold; sharing the
// first one would measure the cache, not the network path.
let fresh = Arc::new(MetadataClient::new());
let started = Instant::now();
let mut tasks = Vec::new();
for title in &titles {
let fresh = Arc::clone(&fresh);
let artist = artist.clone();
let title = title.clone();
tasks.push(tokio::spawn(async move {
let at = Instant::now();
let info = fresh
.album_info_for_artist_id(artist_id, &artist, &title)
.await;
(title, at.elapsed().as_millis(), info.type_hint)
}));
}
let mut rows = Vec::new();
for task in tasks {
if let Ok(row) = task.await {
rows.push(row);
}
}
let concurrent = started.elapsed().as_millis();
println!("== COLD CONCURRENT ==");
for (title, ms, hint) in &rows {
println!(" {ms:>6} ms {title} hint={hint:?}");
}
println!(" total {concurrent} ms\n");
println!("== SUMMARY ==");
println!(" cold sequential : {cold:>6} ms <- today's harvest wall-clock");
println!(" warm sequential : {warm:>6} ms <- per-source cache hit");
println!(" cold concurrent : {concurrent:>6} ms <- ceiling from fan-out");
if concurrent > 0 {
println!(
" speedup : {:.1}x",
cold as f64 / concurrent as f64
);
}
}
async fn run_sequential(
client: &Arc<MetadataClient>,
artist_id: Option<u64>,
artist: &str,
titles: &[String],
label: &str,
) -> u128 {
println!("== {label} ==");
let started = Instant::now();
for title in titles {
let at = Instant::now();
let info = client
.album_info_for_artist_id(artist_id, artist, title)
.await;
println!(
" {:>6} ms {title} hint={:?} source={:?}",
at.elapsed().as_millis(),
info.type_hint,
info.source,
);
}
let total = started.elapsed().as_millis();
println!(" total {total} ms\n");
total
}
fn normalize(s: &str) -> String {
s.to_lowercase()
.chars()
.filter(|c| c.is_alphanumeric())
.collect()
}