PNRxA commited on
Commit
bc0c819
·
1 Parent(s): 66e881f

added latency logging

Browse files
api/src/id/mod.rs CHANGED
@@ -125,7 +125,7 @@ async fn resolve_imdb(imdb_id: &str, tmdb: &TmdbClient) -> Result<ResolvedId, Ap
125
  release_date: tv.first_air_date.clone(),
126
  });
127
  }
128
- Err(AppError::IdNotFound(imdb_id.to_string()))
129
  }
130
 
131
  async fn resolve_tmdb(id_value: &str, tmdb: &TmdbClient) -> Result<ResolvedId, AppError> {
@@ -282,5 +282,5 @@ async fn resolve_tvdb(tvdb_id: &str, tmdb: &TmdbClient) -> Result<ResolvedId, Ap
282
  release_date: details.release_date,
283
  });
284
  }
285
- Err(AppError::IdNotFound(tvdb_id.to_string()))
286
  }
 
125
  release_date: tv.first_air_date.clone(),
126
  });
127
  }
128
+ Err(AppError::IdNotFound(format!("{imdb_id} (not found on TMDB)")))
129
  }
130
 
131
  async fn resolve_tmdb(id_value: &str, tmdb: &TmdbClient) -> Result<ResolvedId, AppError> {
 
282
  release_date: details.release_date,
283
  });
284
  }
285
+ Err(AppError::IdNotFound(format!("{tvdb_id} (not found on TMDB via TVDB lookup)")))
286
  }
api/src/poster/generate.rs CHANGED
@@ -12,6 +12,9 @@ use crate::services::db::STYLE_VERTICAL;
12
  use crate::services::ratings::RatingBadge;
13
  use crate::services::tmdb::TmdbClient;
14
 
 
 
 
15
  const BADGE_SPACING: u32 = 10;
16
  const BADGE_BOTTOM_MARGIN: u32 = 10;
17
  const BADGE_TOP_MARGIN: u32 = 20;
@@ -123,7 +126,15 @@ pub async fn generate_poster(params: PosterParams<'_>) -> Result<Vec<u8>, AppErr
123
  .await
124
  .map_err(|e| AppError::Other(e.to_string()))??;
125
 
126
- tracing::debug!(elapsed_ms = start.elapsed().as_millis() as u64, "poster render complete");
 
 
 
 
 
 
 
 
127
  Ok(buf)
128
  }
129
 
 
12
  use crate::services::ratings::RatingBadge;
13
  use crate::services::tmdb::TmdbClient;
14
 
15
+ /// Threshold (ms) above which render is logged as slow.
16
+ const SLOW_RENDER_MS: u64 = 2000;
17
+
18
  const BADGE_SPACING: u32 = 10;
19
  const BADGE_BOTTOM_MARGIN: u32 = 10;
20
  const BADGE_TOP_MARGIN: u32 = 20;
 
126
  .await
127
  .map_err(|e| AppError::Other(e.to_string()))??;
128
 
129
+ let render_ms = start.elapsed().as_millis() as u64;
130
+ tracing::debug!(elapsed_ms = render_ms, "poster render complete");
131
+ if render_ms > SLOW_RENDER_MS {
132
+ tracing::warn!(
133
+ poster_path,
134
+ render_ms,
135
+ "slow generate_poster"
136
+ );
137
+ }
138
  Ok(buf)
139
  }
140
 
api/src/poster/serve.rs CHANGED
@@ -18,6 +18,9 @@ use crate::AppState;
18
  /// Hardcoded CDN TTL for placeholder/fallback images (1 day).
19
  pub const PLACEHOLDER_CDN_MAX_AGE: u64 = 86400;
20
 
 
 
 
21
  /// Alias for the unified image kind enum used throughout the serve layer.
22
  pub type ImageKind = cache::ImageType;
23
 
@@ -277,7 +280,10 @@ async fn resolve_with_ratings(
277
  id_type: IdType,
278
  id_value: &str,
279
  ) -> Result<(id::ResolvedId, ratings::RatingsResult, CrossIdInfo), AppError> {
 
280
  let resolved = id::resolve(id_type, id_value, &state.tmdb, &state.id_cache).await?;
 
 
281
  let ratings_result = ratings::fetch_ratings(
282
  &resolved,
283
  &state.tmdb,
@@ -286,6 +292,15 @@ async fn resolve_with_ratings(
286
  &state.ratings_cache,
287
  )
288
  .await;
 
 
 
 
 
 
 
 
 
289
  let cross_ids = CrossIdInfo::from_resolved(&resolved, &ratings_result);
290
  Ok((resolved, ratings_result, cross_ids))
291
  }
@@ -474,6 +489,7 @@ pub async fn handle_inner(
474
  mut settings: PosterSettings,
475
  image_size: Option<ImageSize>,
476
  ) -> Result<(Bytes, Option<String>), AppError> {
 
477
  let id_type = IdType::parse(id_type_str)?;
478
  let id_value = id_value_jpg.strip_suffix(".jpg").unwrap_or(id_value_jpg);
479
 
@@ -490,7 +506,9 @@ pub async fn handle_inner(
490
  // Fast path (non-fanart): try to reconstruct the cache key from SQLite-stored
491
  // available sources, avoiding external API calls entirely on cache hits.
492
  if !use_fanart {
 
493
  if let Some(available) = read_available_ratings_cached(state, &id_key).await {
 
494
  let ratings_suffix = ratings::badges_suffix_from_available(&available, &settings.ratings_order, settings.ratings_limit);
495
  let suffix = settings_cache_suffix_with_ratings(&settings, ImageKind::Poster, image_size, &ratings_suffix);
496
  let cache_value = format!("{id_value}{suffix}");
@@ -502,20 +520,47 @@ pub async fn handle_inner(
502
  let id_value = id_value.to_string();
503
  let cache_suffix = cache_suffix.clone();
504
  let settings = settings.clone();
 
505
  if let Some(bytes) = check_caches(state, &cache_key, &cache_path, |s, k, p| {
506
  trigger_background_refresh(s, k, p, id_type, &id_value, &cache_suffix, &settings, image_size);
507
  }).await? {
 
 
508
  let release_date = cache::read_meta_db(&state.db, &cache_key).await;
 
 
 
 
 
 
 
 
 
 
 
 
509
  return Ok((bytes, release_date));
510
  }
511
  }
 
 
 
 
 
 
 
 
 
512
  }
513
  }
514
 
515
  // Slow path: resolve ID and fetch ratings (moka-cached, so still fast on
516
  // repeat requests within the 30-min TTL).
 
 
517
  let (resolved, ratings_result, cross_ids) =
518
  resolve_with_ratings(state, id_type, id_value).await?;
 
519
 
520
  // Persist available sources for future fast-path lookups (always write,
521
  // even with external_cache_only — this is an optimization index, not a
@@ -572,14 +617,26 @@ pub async fn handle_inner(
572
  let id_value = id_value.to_string();
573
  let cache_suffix = cache_suffix.clone();
574
  let settings = settings.clone();
 
575
  if let Some(bytes) = check_caches(state, &cache_key, &cache_path, |s, k, p| {
576
  trigger_background_refresh(s, k, p, id_type, &id_value, &cache_suffix, &settings, image_size);
577
  }).await? {
 
 
 
 
 
 
 
 
 
 
578
  return Ok((bytes, release_date));
579
  }
580
  }
581
 
582
  // Request coalescing — concurrent requests for the same poster share one generation
 
583
  let state2 = state.clone();
584
  let cache_key2 = cache_key.clone();
585
  let cache_path2 = cache_path.clone();
@@ -600,6 +657,18 @@ pub async fn handle_inner(
600
  .await
601
  .map_err(|e| AppError::Other(e.to_string()))?;
602
 
 
 
 
 
 
 
 
 
 
 
 
 
603
  // Insert into memory cache
604
  state
605
  .poster_mem_cache
@@ -947,7 +1016,11 @@ async fn generate_poster_with_source(
947
  let poster_path = resolved
948
  .poster_path
949
  .as_deref()
950
- .ok_or_else(|| AppError::Other("no poster available".into()))?;
 
 
 
 
951
 
952
  // Try to fetch poster bytes from fanart.tv if configured
953
  let fanart_result = if &*settings.poster_source == SOURCE_FANART || settings.lang_override {
 
18
  /// Hardcoded CDN TTL for placeholder/fallback images (1 day).
19
  pub const PLACEHOLDER_CDN_MAX_AGE: u64 = 86400;
20
 
21
+ /// Threshold (ms) above which requests are logged as slow.
22
+ const SLOW_REQUEST_MS: u64 = 2000;
23
+
24
  /// Alias for the unified image kind enum used throughout the serve layer.
25
  pub type ImageKind = cache::ImageType;
26
 
 
280
  id_type: IdType,
281
  id_value: &str,
282
  ) -> Result<(id::ResolvedId, ratings::RatingsResult, CrossIdInfo), AppError> {
283
+ let id_resolve_start = Instant::now();
284
  let resolved = id::resolve(id_type, id_value, &state.tmdb, &state.id_cache).await?;
285
+ let id_resolve_ms = id_resolve_start.elapsed().as_millis() as u64;
286
+
287
  let ratings_result = ratings::fetch_ratings(
288
  &resolved,
289
  &state.tmdb,
 
292
  &state.ratings_cache,
293
  )
294
  .await;
295
+
296
+ if id_resolve_ms > SLOW_REQUEST_MS {
297
+ tracing::warn!(
298
+ id = %id_value,
299
+ id_resolve_ms,
300
+ "slow id resolution"
301
+ );
302
+ }
303
+
304
  let cross_ids = CrossIdInfo::from_resolved(&resolved, &ratings_result);
305
  Ok((resolved, ratings_result, cross_ids))
306
  }
 
489
  mut settings: PosterSettings,
490
  image_size: Option<ImageSize>,
491
  ) -> Result<(Bytes, Option<String>), AppError> {
492
+ let request_start = Instant::now();
493
  let id_type = IdType::parse(id_type_str)?;
494
  let id_value = id_value_jpg.strip_suffix(".jpg").unwrap_or(id_value_jpg);
495
 
 
506
  // Fast path (non-fanart): try to reconstruct the cache key from SQLite-stored
507
  // available sources, avoiding external API calls entirely on cache hits.
508
  if !use_fanart {
509
+ let fast_path_start = Instant::now();
510
  if let Some(available) = read_available_ratings_cached(state, &id_key).await {
511
+ let available_ratings_ms = fast_path_start.elapsed().as_millis() as u64;
512
  let ratings_suffix = ratings::badges_suffix_from_available(&available, &settings.ratings_order, settings.ratings_limit);
513
  let suffix = settings_cache_suffix_with_ratings(&settings, ImageKind::Poster, image_size, &ratings_suffix);
514
  let cache_value = format!("{id_value}{suffix}");
 
520
  let id_value = id_value.to_string();
521
  let cache_suffix = cache_suffix.clone();
522
  let settings = settings.clone();
523
+ let cache_check_start = Instant::now();
524
  if let Some(bytes) = check_caches(state, &cache_key, &cache_path, |s, k, p| {
525
  trigger_background_refresh(s, k, p, id_type, &id_value, &cache_suffix, &settings, image_size);
526
  }).await? {
527
+ let cache_check_ms = cache_check_start.elapsed().as_millis() as u64;
528
+ let meta_start = Instant::now();
529
  let release_date = cache::read_meta_db(&state.db, &cache_key).await;
530
+ let meta_ms = meta_start.elapsed().as_millis() as u64;
531
+ let total_ms = request_start.elapsed().as_millis() as u64;
532
+ if total_ms > SLOW_REQUEST_MS {
533
+ tracing::warn!(
534
+ id = %id_key,
535
+ total_ms,
536
+ available_ratings_ms,
537
+ cache_check_ms,
538
+ meta_db_ms = meta_ms,
539
+ "slow poster request (fast path hit)"
540
+ );
541
+ }
542
  return Ok((bytes, release_date));
543
  }
544
  }
545
+ let total_fast_path_ms = fast_path_start.elapsed().as_millis() as u64;
546
+ if total_fast_path_ms > SLOW_REQUEST_MS {
547
+ tracing::warn!(
548
+ id = %id_key,
549
+ total_fast_path_ms,
550
+ available_ratings_ms,
551
+ "slow fast path — cache miss, falling to slow path"
552
+ );
553
+ }
554
  }
555
  }
556
 
557
  // Slow path: resolve ID and fetch ratings (moka-cached, so still fast on
558
  // repeat requests within the 30-min TTL).
559
+ let slow_path_start = Instant::now();
560
+ let resolve_start = Instant::now();
561
  let (resolved, ratings_result, cross_ids) =
562
  resolve_with_ratings(state, id_type, id_value).await?;
563
+ let resolve_ms = resolve_start.elapsed().as_millis() as u64;
564
 
565
  // Persist available sources for future fast-path lookups (always write,
566
  // even with external_cache_only — this is an optimization index, not a
 
617
  let id_value = id_value.to_string();
618
  let cache_suffix = cache_suffix.clone();
619
  let settings = settings.clone();
620
+ let slow_cache_check_start = Instant::now();
621
  if let Some(bytes) = check_caches(state, &cache_key, &cache_path, |s, k, p| {
622
  trigger_background_refresh(s, k, p, id_type, &id_value, &cache_suffix, &settings, image_size);
623
  }).await? {
624
+ let total_ms = request_start.elapsed().as_millis() as u64;
625
+ if total_ms > SLOW_REQUEST_MS {
626
+ tracing::warn!(
627
+ id = %id_key,
628
+ total_ms,
629
+ resolve_ms,
630
+ cache_check_ms = slow_cache_check_start.elapsed().as_millis() as u64,
631
+ "slow poster request (slow path cache hit)"
632
+ );
633
+ }
634
  return Ok((bytes, release_date));
635
  }
636
  }
637
 
638
  // Request coalescing — concurrent requests for the same poster share one generation
639
+ let generate_start = Instant::now();
640
  let state2 = state.clone();
641
  let cache_key2 = cache_key.clone();
642
  let cache_path2 = cache_path.clone();
 
657
  .await
658
  .map_err(|e| AppError::Other(e.to_string()))?;
659
 
660
+ let total_ms = request_start.elapsed().as_millis() as u64;
661
+ if total_ms > SLOW_REQUEST_MS {
662
+ tracing::warn!(
663
+ id = %id_key,
664
+ total_ms,
665
+ resolve_ms,
666
+ generate_ms = generate_start.elapsed().as_millis() as u64,
667
+ slow_path_ms = slow_path_start.elapsed().as_millis() as u64,
668
+ "slow poster request (generated)"
669
+ );
670
+ }
671
+
672
  // Insert into memory cache
673
  state
674
  .poster_mem_cache
 
1016
  let poster_path = resolved
1017
  .poster_path
1018
  .as_deref()
1019
+ .ok_or_else(|| {
1020
+ let id_desc = resolved.imdb_id.as_deref()
1021
+ .unwrap_or_else(|| "unknown");
1022
+ AppError::Other(format!("no poster available for {id_desc} / tmdb:{} (TMDB has no poster_path)", resolved.tmdb_id))
1023
+ })?;
1024
 
1025
  // Try to fetch poster bytes from fanart.tv if configured
1026
  let fanart_result = if &*settings.poster_source == SOURCE_FANART || settings.lang_override {
api/src/services/ratings.rs CHANGED
@@ -5,6 +5,9 @@ use crate::services::tmdb::TmdbClient;
5
  use image::Rgba;
6
  use serde::Deserialize;
7
 
 
 
 
8
  #[derive(Debug, Clone, Copy, PartialEq, Eq)]
9
  pub enum RatingSource {
10
  Imdb,
@@ -155,13 +158,40 @@ async fn fetch_ratings_inner(
155
  omdb: Option<&OmdbClient>,
156
  mdblist: Option<&MdblistClient>,
157
  ) -> RatingsResult {
158
- let tmdb_fut = fetch_tmdb_rating(resolved, tmdb);
159
- let omdb_fut = fetch_omdb_ratings(resolved.imdb_id.as_deref(), omdb);
160
- let mdblist_fut = fetch_mdblist_ratings(resolved, mdblist);
161
 
162
- let (tmdb_badges, omdb_badges, mdblist_raw) =
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
163
  tokio::join!(tmdb_fut, omdb_fut, mdblist_fut);
164
 
 
 
 
 
 
 
 
 
 
 
 
 
 
165
  let (mdblist_badges, mdb_tmdb_id, mdb_tvdb_id, mdb_imdb_id) = match mdblist_raw {
166
  Some((badges, tmdb_id, tvdb_id, imdb_id)) => (Some(badges), tmdb_id, tvdb_id, imdb_id),
167
  None => (None, None, None, None),
 
5
  use image::Rgba;
6
  use serde::Deserialize;
7
 
8
+ /// Threshold (ms) above which ratings fetches are logged as slow.
9
+ const SLOW_RATINGS_MS: u64 = 2000;
10
+
11
  #[derive(Debug, Clone, Copy, PartialEq, Eq)]
12
  pub enum RatingSource {
13
  Imdb,
 
158
  omdb: Option<&OmdbClient>,
159
  mdblist: Option<&MdblistClient>,
160
  ) -> RatingsResult {
161
+ let ratings_start = std::time::Instant::now();
 
 
162
 
163
+ let tmdb_fut = async {
164
+ let start = std::time::Instant::now();
165
+ let result = fetch_tmdb_rating(resolved, tmdb).await;
166
+ (result, start.elapsed())
167
+ };
168
+ let omdb_fut = async {
169
+ let start = std::time::Instant::now();
170
+ let result = fetch_omdb_ratings(resolved.imdb_id.as_deref(), omdb).await;
171
+ (result, start.elapsed())
172
+ };
173
+ let mdblist_fut = async {
174
+ let start = std::time::Instant::now();
175
+ let result = fetch_mdblist_ratings(resolved, mdblist).await;
176
+ (result, start.elapsed())
177
+ };
178
+
179
+ let ((tmdb_badges, tmdb_dur), (omdb_badges, omdb_dur), (mdblist_raw, mdblist_dur)) =
180
  tokio::join!(tmdb_fut, omdb_fut, mdblist_fut);
181
 
182
+ let ratings_elapsed = ratings_start.elapsed().as_millis() as u64;
183
+ if ratings_elapsed > SLOW_RATINGS_MS {
184
+ tracing::warn!(
185
+ tmdb_id = resolved.tmdb_id,
186
+ imdb_id = ?resolved.imdb_id,
187
+ total_ms = ratings_elapsed,
188
+ tmdb_ms = tmdb_dur.as_millis() as u64,
189
+ omdb_ms = omdb_dur.as_millis() as u64,
190
+ mdblist_ms = mdblist_dur.as_millis() as u64,
191
+ "slow ratings fetch"
192
+ );
193
+ }
194
+
195
  let (mdblist_badges, mdb_tmdb_id, mdb_tvdb_id, mdb_imdb_id) = match mdblist_raw {
196
  Some((badges, tmdb_id, tvdb_id, imdb_id)) => (Some(badges), tmdb_id, tvdb_id, imdb_id),
197
  None => (None, None, None, None),