Files
SoulStream/crates/metadata/examples/artist_page_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

120 lines
4.6 KiB
Rust

//! Latency harness for the whole discography-load pipeline — the three
//! backend stages the artist page runs, in the order the frontend runs them:
//!
//! 1. `catalog_artist_overview` — the first-paint content stage
//! 2. `catalog_artist` — the exhaustive classification page
//! 3. `catalog_artist_identity` — the MusicBrainz supplement/type-hint pass
//!
//! Companion to `album_info_bench` (descriptions). Run with
//! `SOULSTREAM_STAGE_TRACE=1` to attribute time inside each stage.
//!
//! Usage:
//! cargo run -p metadata --example artist_page_bench -- "Artist" ["Artist" ...]
use metadata::MetadataClient;
use std::sync::Arc;
use std::time::Instant;
#[tokio::main]
async fn main() {
let artists: Vec<String> = std::env::args().skip(1).collect();
if artists.is_empty() {
eprintln!("usage: artist_page_bench -- \"Artist\" [\"Artist\" ...]");
return;
}
for artist in &artists {
let client = Arc::new(MetadataClient::new());
let search = client.catalog_search(artist).await;
let Some(id) = search
.artists
.iter()
.find(|a| normalize(&a.name) == normalize(artist))
.map(|a| a.id)
else {
println!("== {artist}: no exact-name artist found, skipping ==\n");
continue;
};
println!("== {artist} (deezer {id}) ==");
// The frontend overlaps overview + page, then runs the identity pass
// after the page lands. Reproduce exactly that shape.
let cold_started = Instant::now();
let (overview, page) = tokio::join!(
async {
let t = Instant::now();
let overview = client.catalog_artist_overview(id).await;
(t.elapsed().as_millis(), overview)
},
async {
let t = Instant::now();
let page = client.catalog_artist(id).await;
(t.elapsed().as_millis(), page)
},
);
let overlap_ms = cold_started.elapsed().as_millis();
println!(
" cold overview: {} ms ({} albums) cold page: {} ms ({} albums) overlapped: {overlap_ms} ms",
overview.0,
overview.1.as_ref().map(|o| o.albums.len()).unwrap_or(0),
page.0,
page.1.as_ref().map(|p| p.albums.len()).unwrap_or(0),
);
let t = Instant::now();
let identity = client.catalog_artist_identity(id, artist).await;
let identity_ms = t.elapsed().as_millis();
println!(
" cold identity pass: {identity_ms} ms (supplement {} albums, {} type hints)",
identity.albums.len(),
identity.type_hints.len(),
);
println!(
" COLD TOTAL to fully sorted (page ∥ overview, then identity): {} ms",
overlap_ms + identity_ms
);
let t = Instant::now();
let _ = client.catalog_artist(id).await;
println!(" warm page: {} ms", t.elapsed().as_millis());
let t = Instant::now();
let _ = client.catalog_artist_identity(id, artist).await;
println!(" warm identity: {} ms", t.elapsed().as_millis());
// The new frontend shape: the identity pass is kicked off BESIDE the
// page load instead of after it, so cold time-to-sorted is max(), not
// sum(). A fresh client so nothing above warms it.
let fresh = Arc::new(MetadataClient::new());
let t = Instant::now();
let (_page, _identity) = tokio::join!(
fresh.catalog_artist(id),
fresh.catalog_artist_identity(id, artist),
);
println!(
" cold page ∥ identity (new frontend overlap): {} ms",
t.elapsed().as_millis()
);
// Restart warmth: two separate clients sharing one disk cache dir —
// the second models an app relaunch.
let dir = std::env::temp_dir().join("soulstream-artist-page-bench-cache");
let _ = std::fs::remove_dir_all(&dir);
let writer = Arc::new(MetadataClient::new().with_disk_cache(dir.clone()));
let _ = writer.catalog_artist_identity(id, artist).await;
let restarted = Arc::new(MetadataClient::new().with_disk_cache(dir.clone()));
let t = Instant::now();
let _ = restarted.catalog_artist_identity(id, artist).await;
println!(
" identity after app restart (disk cache): {} ms\n",
t.elapsed().as_millis()
);
}
}
fn normalize(s: &str) -> String {
s.to_lowercase()
.chars()
.filter(|c| c.is_alphanumeric())
.collect()
}