perf: batch recent play counts for gallery lists
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
use crate::*;
|
||||
use std::collections::HashSet;
|
||||
use std::collections::{HashMap, HashSet};
|
||||
|
||||
#[spacetimedb::table(
|
||||
accessor = custom_world_profile,
|
||||
@@ -1549,11 +1549,26 @@ fn list_custom_world_gallery_snapshots(
|
||||
) -> Result<Vec<CustomWorldGalleryEntrySnapshot>, String> {
|
||||
sync_missing_custom_world_gallery_entries(ctx)?;
|
||||
|
||||
let mut entries = ctx
|
||||
let entries = ctx
|
||||
.db
|
||||
.custom_world_gallery_entry()
|
||||
.iter()
|
||||
.map(|row| build_custom_world_gallery_entry_snapshot(ctx, &row))
|
||||
.collect::<Vec<_>>();
|
||||
let profile_ids = entries
|
||||
.iter()
|
||||
.map(|row| row.profile_id.clone())
|
||||
.collect::<Vec<_>>();
|
||||
let recent_play_counts = count_recent_public_work_plays_for_profiles(
|
||||
ctx,
|
||||
"custom-world",
|
||||
&profile_ids,
|
||||
ctx.timestamp.to_micros_since_unix_epoch(),
|
||||
);
|
||||
let mut entries = entries
|
||||
.iter()
|
||||
.map(|row| {
|
||||
build_custom_world_gallery_entry_snapshot_with_recent_counts(row, &recent_play_counts)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
entries.sort_by(|left, right| {
|
||||
@@ -5078,6 +5093,19 @@ fn build_custom_world_draft_card_snapshot(
|
||||
fn build_custom_world_gallery_entry_snapshot(
|
||||
ctx: &ReducerContext,
|
||||
row: &CustomWorldGalleryEntry,
|
||||
) -> CustomWorldGalleryEntrySnapshot {
|
||||
let recent_play_counts = count_recent_public_work_plays_for_profiles(
|
||||
ctx,
|
||||
"custom-world",
|
||||
&[row.profile_id.clone()],
|
||||
ctx.timestamp.to_micros_since_unix_epoch(),
|
||||
);
|
||||
build_custom_world_gallery_entry_snapshot_with_recent_counts(row, &recent_play_counts)
|
||||
}
|
||||
|
||||
fn build_custom_world_gallery_entry_snapshot_with_recent_counts(
|
||||
row: &CustomWorldGalleryEntry,
|
||||
recent_play_counts: &HashMap<String, u32>,
|
||||
) -> CustomWorldGalleryEntrySnapshot {
|
||||
CustomWorldGalleryEntrySnapshot {
|
||||
profile_id: row.profile_id.clone(),
|
||||
@@ -5095,12 +5123,10 @@ fn build_custom_world_gallery_entry_snapshot(
|
||||
play_count: row.play_count,
|
||||
remix_count: row.remix_count,
|
||||
like_count: row.like_count,
|
||||
recent_play_count_7d: count_recent_public_work_plays(
|
||||
ctx,
|
||||
"custom-world",
|
||||
&row.profile_id,
|
||||
ctx.timestamp.to_micros_since_unix_epoch(),
|
||||
),
|
||||
recent_play_count_7d: recent_play_counts
|
||||
.get(&row.profile_id)
|
||||
.copied()
|
||||
.unwrap_or(0),
|
||||
published_at_micros: row.published_at.to_micros_since_unix_epoch(),
|
||||
updated_at_micros: row.updated_at.to_micros_since_unix_epoch(),
|
||||
}
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
use crate::runtime::{
|
||||
ProfilePlayedWorkUpsertInput, ProfileSaveArchiveUpsertInput, PublicWorkLikeRecordInput,
|
||||
PublicWorkPlayRecordInput, add_profile_observed_play_time, count_recent_public_work_plays,
|
||||
grant_profile_wallet_points, record_public_work_like, record_public_work_play,
|
||||
upsert_profile_played_work, upsert_profile_save_archive,
|
||||
count_recent_public_work_plays_for_profiles, grant_profile_wallet_points,
|
||||
record_public_work_like, record_public_work_play, upsert_profile_played_work,
|
||||
upsert_profile_save_archive,
|
||||
};
|
||||
use module_puzzle::{
|
||||
PUZZLE_MAX_TAG_COUNT, PUZZLE_NEXT_LEVEL_MODE_NONE, PUZZLE_NEXT_LEVEL_MODE_SAME_WORK,
|
||||
@@ -1392,12 +1393,21 @@ fn delete_puzzle_work_tx(
|
||||
|
||||
fn list_puzzle_gallery_tx(ctx: &TxContext) -> Result<Vec<PuzzleWorkProfile>, String> {
|
||||
let now_micros = ctx.timestamp.to_micros_since_unix_epoch();
|
||||
let mut items = ctx
|
||||
let rows = ctx
|
||||
.db
|
||||
.puzzle_work_profile()
|
||||
.iter()
|
||||
.filter(|row| row.publication_status == PuzzlePublicationStatus::Published)
|
||||
.map(|row| build_puzzle_work_profile_from_row_with_recent_count(ctx, &row, now_micros))
|
||||
.collect::<Vec<_>>();
|
||||
let profile_ids = rows
|
||||
.iter()
|
||||
.map(|row| row.profile_id.clone())
|
||||
.collect::<Vec<_>>();
|
||||
let recent_play_counts =
|
||||
count_recent_public_work_plays_for_profiles(ctx, "puzzle", &profile_ids, now_micros);
|
||||
let mut items = rows
|
||||
.iter()
|
||||
.map(|row| build_puzzle_work_profile_from_row_with_recent_counts(row, &recent_play_counts))
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
items.sort_by(|left, right| right.updated_at_micros.cmp(&left.updated_at_micros));
|
||||
Ok(items)
|
||||
@@ -2268,6 +2278,18 @@ fn build_puzzle_work_profile_from_row_with_recent_count(
|
||||
Ok(profile)
|
||||
}
|
||||
|
||||
fn build_puzzle_work_profile_from_row_with_recent_counts(
|
||||
row: &PuzzleWorkProfileRow,
|
||||
recent_play_counts: &std::collections::HashMap<String, u32>,
|
||||
) -> Result<PuzzleWorkProfile, String> {
|
||||
let mut profile = build_puzzle_work_profile_from_row_without_recent_count(row)?;
|
||||
profile.recent_play_count_7d = recent_play_counts
|
||||
.get(&row.profile_id)
|
||||
.copied()
|
||||
.unwrap_or(0);
|
||||
Ok(profile)
|
||||
}
|
||||
|
||||
fn build_puzzle_work_profile_from_row_without_recent_count(
|
||||
row: &PuzzleWorkProfileRow,
|
||||
) -> Result<PuzzleWorkProfile, String> {
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user