perf: batch recent play counts for gallery lists

This commit is contained in:
2026-05-12 10:59:51 +08:00
parent 612d105a23
commit 183e78d475
3 changed files with 213 additions and 26 deletions

View File

@@ -1,4 +1,5 @@
use crate::*;
use std::collections::{HashMap, HashSet};
const PUBLIC_WORK_PLAY_DAY_MICROS: i64 = 86_400_000_000;
const PUBLIC_WORK_RECENT_PLAY_WINDOW_DAYS: i64 = 7;
@@ -1299,25 +1300,94 @@ pub(crate) fn count_recent_public_work_plays(
profile_id: &str,
now_micros: i64,
) -> u32 {
count_recent_public_work_plays_for_profiles(
ctx,
source_type,
&[profile_id.to_string()],
now_micros,
)
.remove(profile_id.trim())
.unwrap_or(0)
}
pub(crate) fn count_recent_public_work_plays_for_profiles(
ctx: &ReducerContext,
source_type: &str,
profile_ids: &[String],
now_micros: i64,
) -> HashMap<String, u32> {
let source_type = source_type.trim();
let profile_id = profile_id.trim();
if source_type.is_empty() || profile_id.is_empty() {
return 0;
if source_type.is_empty() || profile_ids.is_empty() {
return HashMap::new();
}
let current_day = public_work_play_day_from_micros(now_micros);
let first_day = current_day.saturating_sub(PUBLIC_WORK_RECENT_PLAY_WINDOW_DAYS - 1);
ctx.db
.public_work_play_daily_stat()
let requested_profile_ids = profile_ids
.iter()
.filter(|row| {
row.source_type == source_type
&& row.profile_id == profile_id
&& row.played_day >= first_day
&& row.played_day <= current_day
})
.fold(0u32, |total, row| total.saturating_add(row.play_count))
.map(|profile_id| profile_id.trim())
.filter(|profile_id| !profile_id.is_empty())
.collect::<HashSet<_>>();
let mut counts = HashMap::new();
for profile_id in requested_profile_ids {
let mut total = 0u32;
for played_day in first_day..=current_day {
let day_total = ctx
.db
.public_work_play_daily_stat()
.by_public_work_play_daily_stat_work_day()
.filter((source_type, profile_id, played_day))
.fold(0u32, |sum, row| sum.saturating_add(row.play_count));
total = total.saturating_add(day_total);
}
if total > 0 {
counts.insert(profile_id.to_string(), total);
}
}
counts
}
#[cfg(test)]
fn build_recent_public_work_play_counts(
rows: impl IntoIterator<Item = PublicWorkPlayDailyStat>,
source_type: &str,
profile_ids: &[String],
now_micros: i64,
) -> HashMap<String, u32> {
let source_type = source_type.trim();
if source_type.is_empty() || profile_ids.is_empty() {
return HashMap::new();
}
let requested_profile_ids = profile_ids
.iter()
.map(|profile_id| profile_id.trim())
.filter(|profile_id| !profile_id.is_empty())
.collect::<HashSet<_>>();
if requested_profile_ids.is_empty() {
return HashMap::new();
}
let current_day = public_work_play_day_from_micros(now_micros);
let first_day = current_day.saturating_sub(PUBLIC_WORK_RECENT_PLAY_WINDOW_DAYS - 1);
let mut counts = HashMap::new();
for row in rows {
if row.source_type != source_type
|| !requested_profile_ids.contains(row.profile_id.as_str())
|| row.played_day < first_day
|| row.played_day > current_day
{
continue;
}
let entry = counts.entry(row.profile_id.clone()).or_insert(0u32);
*entry = entry.saturating_add(row.play_count);
}
counts
}
fn public_work_play_day_from_micros(value: i64) -> i64 {
@@ -1336,6 +1406,75 @@ fn build_public_work_like_id(source_type: &str, profile_id: &str, user_id: &str)
format!("{source_type}:{profile_id}:{user_id}")
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn recent_public_work_play_counts_group_requested_profiles_in_window() {
let now_micros = PUBLIC_WORK_PLAY_DAY_MICROS * 10;
let updated_at = Timestamp::from_micros_since_unix_epoch(now_micros);
let rows = vec![
PublicWorkPlayDailyStat {
stat_id: "puzzle:profile-a:10".to_string(),
source_type: "puzzle".to_string(),
owner_user_id: "user-a".to_string(),
profile_id: "profile-a".to_string(),
played_day: 10,
play_count: 3,
updated_at,
},
PublicWorkPlayDailyStat {
stat_id: "puzzle:profile-a:4".to_string(),
source_type: "puzzle".to_string(),
owner_user_id: "user-a".to_string(),
profile_id: "profile-a".to_string(),
played_day: 4,
play_count: 5,
updated_at,
},
PublicWorkPlayDailyStat {
stat_id: "puzzle:profile-a:3".to_string(),
source_type: "puzzle".to_string(),
owner_user_id: "user-a".to_string(),
profile_id: "profile-a".to_string(),
played_day: 3,
play_count: 99,
updated_at,
},
PublicWorkPlayDailyStat {
stat_id: "custom-world:profile-a:10".to_string(),
source_type: "custom-world".to_string(),
owner_user_id: "user-a".to_string(),
profile_id: "profile-a".to_string(),
played_day: 10,
play_count: 7,
updated_at,
},
PublicWorkPlayDailyStat {
stat_id: "puzzle:profile-b:9".to_string(),
source_type: "puzzle".to_string(),
owner_user_id: "user-b".to_string(),
profile_id: "profile-b".to_string(),
played_day: 9,
play_count: 11,
updated_at,
},
];
let counts = build_recent_public_work_play_counts(
rows,
"puzzle",
&["profile-a".to_string(), "profile-b".to_string()],
now_micros,
);
assert_eq!(counts.get("profile-a"), Some(&8));
assert_eq!(counts.get("profile-b"), Some(&11));
assert_eq!(counts.get("profile-c"), None);
}
}
fn ensure_profile_dashboard_state(ctx: &ReducerContext, user_id: &str, updated_at: Timestamp) {
if ctx
.db