debug(suggestions): add detailed timing logs for debugging

Frontend logging:
- Log when effect triggers with artist count
- Log when backend call starts/finishes with timing
- Log result details (tracks, artists counts)

Backend logging (playlist_suggestions.rs):
- Log MBID resolution with cache hits/misses/failures
- Log config values
- Log engine call timing

Engine logging (suggestions.rs):
- Step 1: ensure_vector timing per artist
- Step 2: playlist vector computation
- Step 3: find_nearest timing
- Step 4: track search timing per artist
- Early exit when pool size reached
This commit is contained in:
vicrodh committed 2026-01-27 12:02:31 -06:00
1 parent e9a5dfcb80
commit 1560cd4e3d
3 files changed
+101 -11

No files matched your search

+24 -2
View File
@@ -123,7 +123,10 @@ impl SuggestionsEngine {
exclude_track_ids: &HashSet<u64>, exclude_track_ids: &HashSet<u64>,
include_reasons: bool, include_reasons: bool,
) -> Result<SuggestionResult, String> { ) -> Result<SuggestionResult, String> {
use std::time::Instant;
if playlist_artist_mbids.is_empty() { if playlist_artist_mbids.is_empty() {
log::debug!("[SuggestionsEngine] Empty playlist, returning empty");
return Ok(SuggestionResult { return Ok(SuggestionResult {
tracks: Vec::new(), tracks: Vec::new(),
source_artists: Vec::new(), source_artists: Vec::new(),
@@ -133,17 +136,26 @@ impl SuggestionsEngine {
} }
// 1. Ensure vectors exist for playlist artists // 1. Ensure vectors exist for playlist artists
for mbid in playlist_artist_mbids { log::info!("[SuggestionsEngine] Step 1: Ensuring vectors for {} artists", playlist_artist_mbids.len());
let step1_start = Instant::now();
for (i, mbid) in playlist_artist_mbids.iter().enumerate() {
let artist_start = Instant::now();
let _ = self let _ = self
.builder .builder
.ensure_vector(mbid, None, None, self.config.vector_max_age_days) .ensure_vector(mbid, None, None, self.config.vector_max_age_days)
.await; .await;
log::debug!("[SuggestionsEngine] ensure_vector {}/{} took {:?}", i + 1, playlist_artist_mbids.len(), artist_start.elapsed());
} }
log::info!("[SuggestionsEngine] Step 1 completed in {:?}", step1_start.elapsed());
// 2. Compute combined playlist vector // 2. Compute combined playlist vector
log::info!("[SuggestionsEngine] Step 2: Computing playlist vector");
let step2_start = Instant::now();
let playlist_vector = self.compute_playlist_vector(playlist_artist_mbids).await?; let playlist_vector = self.compute_playlist_vector(playlist_artist_mbids).await?;
log::info!("[SuggestionsEngine] Step 2 completed in {:?}, vector empty={}", step2_start.elapsed(), playlist_vector.is_empty());
if playlist_vector.is_empty() { if playlist_vector.is_empty() {
log::warn!("[SuggestionsEngine] Playlist vector is empty, returning empty result");
return Ok(SuggestionResult { return Ok(SuggestionResult {
tracks: Vec::new(), tracks: Vec::new(),
source_artists: Vec::new(), source_artists: Vec::new(),
@@ -153,18 +165,23 @@ impl SuggestionsEngine {
} }
// 3. Find nearest artists (excluding playlist artists) // 3. Find nearest artists (excluding playlist artists)
log::info!("[SuggestionsEngine] Step 3: Finding nearest artists");
let step3_start = Instant::now();
let exclude_vec: Vec<String> = playlist_artist_mbids.to_vec(); let exclude_vec: Vec<String> = playlist_artist_mbids.to_vec();
let similar_artists = { let similar_artists = {
let store = self.store.lock().await; let store = self.store.lock().await;
store.find_nearest(&playlist_vector, self.config.max_artists, &exclude_vec)? store.find_nearest(&playlist_vector, self.config.max_artists, &exclude_vec)?
}; };
log::info!("[SuggestionsEngine] Step 3 completed in {:?}, found {} similar artists", step3_start.elapsed(), similar_artists.len());
let similar_artists_count = similar_artists.len(); let similar_artists_count = similar_artists.len();
let mut source_artists = Vec::new(); let mut source_artists = Vec::new();
let mut all_tracks = Vec::new(); let mut all_tracks = Vec::new();
// 4. Search for tracks by similar artists // 4. Search for tracks by similar artists
for artist in &similar_artists { log::info!("[SuggestionsEngine] Step 4: Searching tracks for similar artists");
let step4_start = Instant::now();
for (i, artist) in similar_artists.iter().enumerate() {
if artist.similarity < self.config.min_similarity { if artist.similarity < self.config.min_similarity {
continue; continue;
} }
@@ -172,9 +189,12 @@ impl SuggestionsEngine {
source_artists.push(artist.name.clone().unwrap_or_else(|| artist.mbid.clone())); source_artists.push(artist.name.clone().unwrap_or_else(|| artist.mbid.clone()));
// Search Qobuz for tracks by this artist // Search Qobuz for tracks by this artist
let search_start = Instant::now();
let tracks = self let tracks = self
.search_artist_tracks(&artist.mbid, artist.name.as_deref(), artist.similarity) .search_artist_tracks(&artist.mbid, artist.name.as_deref(), artist.similarity)
.await; .await;
log::debug!("[SuggestionsEngine] search_artist_tracks {}/{} took {:?}, got {} tracks",
i + 1, similar_artists.len(), search_start.elapsed(), tracks.len());
for mut track in tracks { for mut track in tracks {
// Skip if already in playlist // Skip if already in playlist
@@ -197,9 +217,11 @@ impl SuggestionsEngine {
// Stop if we have enough tracks // Stop if we have enough tracks
if all_tracks.len() >= self.config.max_pool_size { if all_tracks.len() >= self.config.max_pool_size {
log::info!("[SuggestionsEngine] Reached pool size {} after {} artists", all_tracks.len(), i + 1);
break; break;
} }
} }
log::info!("[SuggestionsEngine] Step 4 completed in {:?}, got {} tracks", step4_start.elapsed(), all_tracks.len());
// 5. Sort by similarity score and limit pool size // 5. Sort by similarity score and limit pool size
all_tracks.sort_by(|a, b| { all_tracks.sort_by(|a, b| {
+57 -6
View File
@@ -45,8 +45,21 @@ pub async fn get_playlist_suggestions_v2(
mb_state: State<'_, MusicBrainzSharedState>, mb_state: State<'_, MusicBrainzSharedState>,
app_state: State<'_, AppState>, app_state: State<'_, AppState>,
) -> Result<SuggestionResult, String> { ) -> Result<SuggestionResult, String> {
use std::time::Instant;
let total_start = Instant::now();
log::info!(
"[Suggestions] Starting for {} artists, exclude {} tracks",
input.artists.len(),
input.exclude_track_ids.len()
);
// Resolve artist names to MBIDs (with caching) // Resolve artist names to MBIDs (with caching)
let resolve_start = Instant::now();
let mut artist_mbids = Vec::new(); let mut artist_mbids = Vec::new();
let mut cached_count = 0;
let mut resolved_count = 0;
let mut failed_count = 0;
for artist in &input.artists { for artist in &input.artists {
// Check cache first // Check cache first
@@ -61,9 +74,13 @@ pub async fn get_playlist_suggestions_v2(
&& resolved.confidence != MatchConfidence::Low && resolved.confidence != MatchConfidence::Low
{ {
artist_mbids.push(mbid); artist_mbids.push(mbid);
cached_count += 1;
continue; continue;
} }
} }
// Cached as "no match"
failed_count += 1;
continue;
} }
// Try to resolve via MusicBrainz search // Try to resolve via MusicBrainz search
@@ -91,13 +108,16 @@ pub async fn get_playlist_suggestions_v2(
let _ = cache.set_artist(&artist.name, &resolved); let _ = cache.set_artist(&artist.name, &resolved);
} }
artist_mbids.push(mbid); artist_mbids.push(mbid);
resolved_count += 1;
continue; continue;
} }
} }
Err(e) => { Err(e) => {
log::debug!("Failed to resolve artist '{}': {}", artist.name, e); log::warn!("[Suggestions] MusicBrainz search failed for '{}': {}", artist.name, e);
} }
} }
} else {
log::debug!("[Suggestions] MusicBrainz disabled, skipping '{}'", artist.name);
} }
// Cache negative result // Cache negative result
@@ -105,15 +125,20 @@ pub async fn get_playlist_suggestions_v2(
let cache = mb_state.cache.lock().await; let cache = mb_state.cache.lock().await;
let _ = cache.set_artist(&artist.name, &ResolvedArtist::empty()); let _ = cache.set_artist(&artist.name, &ResolvedArtist::empty());
} }
failed_count += 1;
} }
let resolve_elapsed = resolve_start.elapsed();
log::info!( log::info!(
"Resolved {}/{} artists to MBIDs for suggestions", "[Suggestions] MBID resolution took {:?}: {} cached, {} resolved, {} failed",
artist_mbids.len(), resolve_elapsed,
input.artists.len() cached_count,
resolved_count,
failed_count
); );
if artist_mbids.is_empty() { if artist_mbids.is_empty() {
log::warn!("[Suggestions] No MBIDs resolved, returning empty result");
return Ok(SuggestionResult { return Ok(SuggestionResult {
tracks: Vec::new(), tracks: Vec::new(),
source_artists: Vec::new(), source_artists: Vec::new(),
@@ -133,6 +158,9 @@ pub async fn get_playlist_suggestions_v2(
// Create the engine with config // Create the engine with config
let config = input.config.unwrap_or_default(); let config = input.config.unwrap_or_default();
log::info!("[Suggestions] Config: max_artists={}, tracks_per={}, pool_size={}",
config.max_artists, config.tracks_per_artist, config.max_pool_size);
let engine = SuggestionsEngine::new( let engine = SuggestionsEngine::new(
store_state.store.clone(), store_state.store.clone(),
builder, builder,
@@ -144,9 +172,32 @@ pub async fn get_playlist_suggestions_v2(
let exclude_set: HashSet<u64> = input.exclude_track_ids.into_iter().collect(); let exclude_set: HashSet<u64> = input.exclude_track_ids.into_iter().collect();
// Generate suggestions // Generate suggestions
engine log::info!("[Suggestions] Calling engine.generate_suggestions with {} MBIDs", artist_mbids.len());
let engine_start = std::time::Instant::now();
let result = engine
.generate_suggestions(&artist_mbids, &exclude_set, input.include_reasons) .generate_suggestions(&artist_mbids, &exclude_set, input.include_reasons)
.await .await;
let engine_elapsed = engine_start.elapsed();
let total_elapsed = total_start.elapsed();
match &result {
Ok(r) => {
log::info!(
"[Suggestions] Engine took {:?}, total {:?}. Result: {} tracks from {} similar artists",
engine_elapsed,
total_elapsed,
r.tracks.len(),
r.similar_artists_count
);
}
Err(e) => {
log::error!("[Suggestions] Engine failed after {:?}: {}", engine_elapsed, e);
}
}
result
} }
/// Get store statistics for debugging /// Get store statistics for debugging
+20 -3
View File
@@ -57,28 +57,45 @@
// Load suggestions when artists change // Load suggestions when artists change
$effect(() => { $effect(() => {
if (artists.length > 0 && !hasLoadedOnce) { if (artists.length > 0 && !hasLoadedOnce) {
console.log('[Suggestions] Effect triggered, artists:', artists.length);
void loadSuggestions(false); void loadSuggestions(false);
} }
}); });
async function loadSuggestions(expanded: boolean) { async function loadSuggestions(expanded: boolean) {
if (loading) return; if (loading) {
console.log('[Suggestions] Already loading, skipping');
return;
}
const poolSize = expanded ? EXPANDED_POOL : INITIAL_POOL;
console.log(`[Suggestions] Starting load (expanded=${expanded}, poolSize=${poolSize}, artists=${artists.length})`);
const startTime = performance.now();
loading = true; loading = true;
error = null; error = null;
try { try {
console.log('[Suggestions] Calling backend...');
result = await getPlaylistSuggestionsV2( result = await getPlaylistSuggestionsV2(
artists, artists,
excludeTrackIds, excludeTrackIds,
showReasons, showReasons,
{ max_pool_size: expanded ? EXPANDED_POOL : INITIAL_POOL } { max_pool_size: poolSize }
); );
const elapsed = ((performance.now() - startTime) / 1000).toFixed(2);
console.log(`[Suggestions] Backend returned in ${elapsed}s:`, {
tracks: result.tracks.length,
sourceArtists: result.source_artists.length,
playlistArtists: result.playlist_artists_count,
similarArtists: result.similar_artists_count
});
pool = result.tracks; pool = result.tracks;
currentPage = 0; currentPage = 0;
hasLoadedOnce = true; hasLoadedOnce = true;
} catch (err) { } catch (err) {
console.error('Failed to load suggestions:', err); const elapsed = ((performance.now() - startTime) / 1000).toFixed(2);
console.error(`[Suggestions] Failed after ${elapsed}s:`, err);
error = err instanceof Error ? err.message : 'Failed to load suggestions'; error = err instanceof Error ? err.message : 'Failed to load suggestions';
pool = []; pool = [];
} finally { } finally {