forked from Keelrleek/SoulStream
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.
124 lines
4.1 KiB
Rust
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()
|
|
}
|