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:
1 parent
e9a5dfcb80
commit
1560cd4e3d
3 files changed
+101
-11
No files matched your search
@@ -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| {
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
Reference in new issue
Block a user