//! 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 = 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, artist_id: Option, 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() }