From a6a4d686518ca87f0969098b4239ee60bb78a91f Mon Sep 17 00:00:00 2001 From: Paul Z Date: Mon, 6 Nov 2023 18:38:00 +0100 Subject: [PATCH] memory optimization --- src/game.rs | 76 +++++++++++++++++++++++--- src/garbage_collector.rs | 8 +-- src/main.rs | 100 +++++++++++----------------------- src/question.rs | 26 +++++++++ src/question/single_choice.rs | 65 ++++++++++++---------- src/stream.rs | 76 +++++++++++++------------- 6 files changed, 204 insertions(+), 147 deletions(-) diff --git a/src/game.rs b/src/game.rs index a5f4df1..8035abe 100644 --- a/src/game.rs +++ b/src/game.rs @@ -1,11 +1,14 @@ use std::{ collections::{HashMap, HashSet}, + ops::Deref, sync::Arc, time::Duration, }; use qrcode::{render::svg, QrCode}; +use rand::{distributions, Rng}; use sailfish::TemplateOnce; +use serde::Deserialize; use tokio::{ select, sync::{broadcast, RwLock}, @@ -16,6 +19,65 @@ use crate::{ ViewerState, }; +#[derive(Debug, Clone, PartialEq, Eq, Hash, Deserialize)] +pub struct GameId(Arc); +#[derive(Debug, Clone, PartialEq, Eq, Hash, Deserialize)] +pub struct PlayerId(Arc); + +impl Deref for GameId { + type Target = str; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl From for GameId { + fn from(value: String) -> Self { + Self(value.into()) + } +} + +impl GameId { + pub fn random() -> Self { + Self( + rand::thread_rng() + .sample_iter(distributions::Alphanumeric) + .take(8) + .map(char::from) + .collect::() + .into(), + ) + } +} + +impl Deref for PlayerId { + type Target = str; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl From for PlayerId { + fn from(value: String) -> Self { + Self(value.into()) + } +} + +impl PlayerId { + pub fn random() -> Self { + Self( + rand::thread_rng() + .sample_iter(distributions::Alphanumeric) + .take(32) + .map(char::from) + .collect::() + .into(), + ) + } +} + #[derive(Clone, Debug)] pub enum GameState { NotStarted, @@ -25,18 +87,18 @@ pub enum GameState { } pub struct Game { - pub id: String, + pub id: GameId, pub owner: String, pub state: Arc>, pub quiz: Quiz, - pub players: HashSet, + pub players: HashSet, pub on_state_update: broadcast::Sender<()>, pub on_submission: broadcast::Sender<()>, pub questions: Vec>, } impl Game { - pub fn new(id: String, owner: String, quiz: Quiz) -> Self { + pub fn new(id: GameId, owner: String, quiz: Quiz) -> Self { Self { id, owner, @@ -87,7 +149,7 @@ impl Game { } } - pub async fn player_view(&self, player_id: &str, htmx: bool) -> HandlerResult { + pub async fn player_view(&self, player_id: &PlayerId, htmx: bool) -> HandlerResult { if !self.players.contains(player_id) { return Err(Error::PlayerNotFound); } @@ -134,7 +196,7 @@ impl Game { pub async fn handle_answer( &mut self, - player_id: &str, + player_id: &PlayerId, values: &HashMap, ) -> HandlerResult<()> { if !self.players.contains(player_id) { @@ -145,7 +207,7 @@ impl Game { if let GameState::Answering(i) = *state { self.questions[i as usize] - .handle_answer(player_id, values) + .handle_answer(player_id.clone(), values) .await?; self.on_submission.send(()); @@ -164,7 +226,7 @@ impl Game { let viewer_state = match *state { GameState::NotStarted => { - let url = format!("{}/{}", base_url, &self.id); + let url = format!("{}/{}", base_url, &self.id.deref()); let img = QrCode::new(&url).expect(""); let img = img.render::().build(); ViewerState::NotStarted((self.players.len() as u32, img, url)) diff --git a/src/garbage_collector.rs b/src/garbage_collector.rs index 1ef1cfa..0c82abf 100644 --- a/src/garbage_collector.rs +++ b/src/garbage_collector.rs @@ -6,11 +6,11 @@ use std::{ use tokio::sync::RwLock; -use crate::game::Game; +use crate::game::{Game, GameId}; pub fn start_gc( game_expiry: Arc>>, - games: Arc>>, + games: Arc>>, ) { let games = games.clone(); let game_expiry = game_expiry.clone(); @@ -36,12 +36,12 @@ pub fn start_gc( #[derive(PartialEq, Eq)] pub struct GarbageCollectorItem { - id: String, + id: GameId, expires_at: u64, } impl GarbageCollectorItem { - pub fn new_in(id: String, time: u64) -> Self { + pub fn new_in(id: GameId, time: u64) -> Self { Self { id, expires_at: SystemTime::now() diff --git a/src/main.rs b/src/main.rs index 67f29c1..12d355e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -3,15 +3,14 @@ use std::{ collections::{BinaryHeap, HashMap}, env, + ops::Deref, sync::Arc, }; use axum::{ - async_trait, - body::HttpBody, error_handling::HandleErrorLayer, - extract::{FromRef, Multipart, Path, Query, State}, - http::{Request, StatusCode, Uri}, + extract::{Multipart, Path, Query, State}, + http::{StatusCode, Uri}, response::{ sse::{Event, KeepAlive}, Html, IntoResponse, Redirect, Sse, @@ -21,21 +20,19 @@ use axum::{ }; use axum_htmx::{HxRedirect, HxRequest}; use axum_oidc::{ - error::MiddlewareError, EmptyAdditionalClaims, OidcAuthLayer, OidcClaims, OidcClient, - OidcLoginLayer, + error::MiddlewareError, EmptyAdditionalClaims, OidcAuthLayer, OidcClaims, OidcLoginLayer, }; use futures_util::Stream; -use game::Game; +use game::{Game, GameId, PlayerId}; use garbage_collector::{start_gc, GarbageCollectorItem}; -use question::single_choice::SingleChoiceQuestion; -use rand::{distributions, Rng}; +use question::{single_choice::SingleChoiceQuestion, Question}; use sailfish::TemplateOnce; use serde::{Deserialize, Serialize}; use stream::{PlayerBroadcastStream, ViewerBroadcastStream}; use tokio::sync::RwLock; -use tower::{Layer, ServiceBuilder}; +use tower::ServiceBuilder; use tower_http::services::ServeDir; -use tower_sessions::{cookie::SameSite, Expiry, MemoryStore, SessionManagerLayer}; +use tower_sessions::{cookie::SameSite, MemoryStore, SessionManagerLayer}; use crate::error::Error; @@ -50,9 +47,9 @@ mod question; #[derive(Clone)] pub struct AppState { - games: Arc>>, + games: Arc>>, game_expiry: Arc>>, - application_base: String, + application_base: &'static str, } #[tokio::main] @@ -108,7 +105,7 @@ pub async fn main() { let app_state = AppState { games, game_expiry, - application_base, + application_base: Box::leak(application_base.into()), }; let app = Router::new() @@ -147,11 +144,7 @@ pub async fn handle_create( let quiz = quiz.ok_or(Error::QuizFileNotFound)?; - let game_id: String = rand::thread_rng() - .sample_iter(distributions::Alphanumeric) - .take(8) - .map(char::from) - .collect(); + let game_id = GameId::random(); let game = Game::new(game_id.clone(), claims.subject().to_string(), quiz); @@ -159,7 +152,7 @@ pub async fn handle_create( games.insert(game_id.clone(), game); - let url = format!("{}/{}/view", state.application_base, &game_id); + let url = format!("{}/{}/view", state.application_base, &game_id.deref()); let mut game_expiry = state.game_expiry.write().await; game_expiry.push(GarbageCollectorItem::new_in(game_id, 24 * 3600)); @@ -168,7 +161,7 @@ pub async fn handle_create( } pub async fn handle_view( - Path(id): Path, + Path(id): Path, State(state): State, HxRequest(htmx): HxRequest, OidcClaims(claims): OidcClaims, @@ -180,13 +173,12 @@ pub async fn handle_view( return Err(Error::Forbidden); } - Ok(Html(game.viewer_view(htmx, &state.application_base).await?)) + Ok(Html(game.viewer_view(htmx, state.application_base).await?)) } pub async fn handle_view_next( - Path(id): Path, + Path(id): Path, State(state): State, - HxRequest(htmx): HxRequest, OidcClaims(claims): OidcClaims, ) -> HandlerResult { let mut games = state.games.write().await; @@ -202,7 +194,7 @@ pub async fn handle_view_next( } pub async fn sse_view( - Path(id): Path, + Path(id): Path, State(state): State, OidcClaims(claims): OidcClaims, ) -> HandlerResult>>> { @@ -216,13 +208,8 @@ pub async fn sse_view( let rx1 = game.on_state_update.subscribe(); let rx2 = game.on_submission.subscribe(); - let stream = ViewerBroadcastStream::new( - rx1, - rx2, - state.games.clone(), - id, - state.application_base.clone(), - ); + let stream = + ViewerBroadcastStream::new(rx1, rx2, state.games.clone(), id, state.application_base); Ok(Sse::new(stream).keep_alive(KeepAlive::default())) } @@ -234,27 +221,25 @@ pub struct PlayerQuery { pub async fn handle_player( Query(query): Query, - Path(id): Path, + Path(id): Path, State(state): State, HxRequest(htmx): HxRequest, ) -> HandlerResult { let mut games = state.games.write().await; let game = games.get_mut(&id).ok_or(Error::NotFound)?; - if let Some(player_id) = query.player { + if let Some(player_id) = query.player.map(PlayerId::from) { Ok(Html(game.player_view(&player_id, htmx).await?).into_response()) } else { - let player_id: String = rand::thread_rng() - .sample_iter(distributions::Alphanumeric) - .take(32) - .map(char::from) - .collect(); - game.players.insert(player_id.to_string()); + let player_id = PlayerId::random(); + game.players.insert(player_id.clone()); game.on_submission.send(()); Ok(Redirect::temporary(&format!( "{}/{}?player={}", - state.application_base, id, player_id + state.application_base, + id.deref(), + player_id.deref() )) .into_response()) } @@ -262,13 +247,13 @@ pub async fn handle_player( #[derive(Deserialize)] pub struct SubmissionPayload { - player_id: String, + player_id: PlayerId, #[serde(flatten)] values: HashMap, } pub async fn handle_player_answer( - Path(id): Path, + Path(id): Path, State(state): State, Form(form): Form, ) -> HandlerResult { @@ -282,12 +267,12 @@ pub async fn handle_player_answer( #[derive(Deserialize)] pub struct SsePlayerQuery { - player: String, + player: PlayerId, } pub async fn sse_player( Query(query): Query, - Path(id): Path, + Path(id): Path, State(state): State, ) -> HandlerResult>>> { let games = state.games.read().await; @@ -312,7 +297,6 @@ struct PlayTemplate<'a> { state: PlayerState, } -#[derive(Clone)] pub enum PlayerState { NotStarted, Answering { inner_body: String }, @@ -329,7 +313,6 @@ struct ViewTemplate<'a> { state: ViewerState, } -#[derive(Clone)] pub enum ViewerState { NotStarted((u32, String, String)), Answering { @@ -357,30 +340,11 @@ pub enum QuizQuestion { #[derive(Debug, Serialize, Deserialize, Clone)] pub struct SingleChoice { - name: String, - answers: Vec, + name: Box, + answers: Box<[Box]>, correct: u32, } -#[async_trait] -pub trait Question: Send + Sync { - async fn render_player(&self, player_id: &str, show_result: bool) -> Result; - - async fn handle_answer( - &mut self, - player_id: &str, - values: &HashMap, - ) -> Result<(), Error>; - - async fn has_answered(&self, player_id: &str) -> Result; - - async fn answered_correctly(&self, player_id: &str) -> Result; - - async fn answer_count(&self) -> Result; - - async fn render_viewer(&self, show_result: bool) -> Result; -} - impl From for Box { fn from(value: QuizQuestion) -> Self { match value { diff --git a/src/question.rs b/src/question.rs index 983b6d2..b797108 100644 --- a/src/question.rs +++ b/src/question.rs @@ -1 +1,27 @@ +use std::collections::HashMap; + +use axum::async_trait; + +use crate::{error::Error, game::PlayerId}; + pub mod single_choice; + +#[async_trait] +pub trait Question: Send + Sync { + async fn render_player(&self, player_id: &PlayerId, show_result: bool) + -> Result; + + async fn handle_answer( + &mut self, + player_id: PlayerId, + values: &HashMap, + ) -> Result<(), Error>; + + async fn has_answered(&self, player_id: &PlayerId) -> Result; + + async fn answered_correctly(&self, player_id: &PlayerId) -> Result; + + async fn answer_count(&self) -> Result; + + async fn render_viewer(&self, show_result: bool) -> Result; +} diff --git a/src/question/single_choice.rs b/src/question/single_choice.rs index 3437a84..a2cd1a7 100644 --- a/src/question/single_choice.rs +++ b/src/question/single_choice.rs @@ -3,11 +3,11 @@ use std::collections::HashMap; use axum::async_trait; use sailfish::TemplateOnce; -use crate::{error::Error, Question, SingleChoice}; +use crate::{error::Error, game::PlayerId, Question, SingleChoice}; pub struct SingleChoiceQuestion { inner: SingleChoice, - submissions: HashMap, + submissions: HashMap, } impl SingleChoiceQuestion { @@ -21,12 +21,15 @@ impl SingleChoiceQuestion { #[async_trait] impl Question for SingleChoiceQuestion { - async fn render_player(&self, player_id: &str, show_result: bool) -> Result { + async fn render_player( + &self, + player_id: &PlayerId, + show_result: bool, + ) -> Result { if show_result { let player_sub_index = self.submissions.get(player_id); - let player_sub_value = - player_sub_index.map(|x| self.inner.answers[*x as usize].as_str()); + let player_sub_value = player_sub_index.map(|x| &*self.inner.answers[*x as usize]); Ok(ResultTemplate { is_correct: Some(&self.inner.correct) == player_sub_index, @@ -46,7 +49,7 @@ impl Question for SingleChoiceQuestion { async fn handle_answer( &mut self, - player_id: &str, + player_id: PlayerId, values: &HashMap, ) -> Result<(), Error> { let value = values @@ -59,20 +62,20 @@ impl Question for SingleChoiceQuestion { return Err(Error::InvalidInput); } - if self.submissions.contains_key(player_id) { + if self.submissions.contains_key(&player_id) { return Err(Error::QuestionAlreadySubmitted); } - self.submissions.insert(player_id.to_string(), value); + self.submissions.insert(player_id, value); Ok(()) } - async fn has_answered(&self, player_id: &str) -> Result { + async fn has_answered(&self, player_id: &PlayerId) -> Result { Ok(self.submissions.get(player_id).is_some()) } - async fn answered_correctly(&self, player_id: &str) -> Result { + async fn answered_correctly(&self, player_id: &PlayerId) -> Result { Ok(self .submissions .get(player_id) @@ -90,30 +93,34 @@ impl Question for SingleChoiceQuestion { name: &self.inner.name, total_submissions: self.submissions.len() as u32, submissions: &self.submissions.iter().fold( - vec![0; self.inner.answers.len()], + vec![0; self.inner.answers.len()].into_boxed_slice(), |mut acc, (_, v)| { acc[*v as usize] += 1; acc }, ), - submissions_correct: &self.submissions.iter().fold( - vec![0; self.inner.answers.len()], - |mut acc, (_, v)| { - if *v == self.inner.correct { + submissions_correct: &self + .submissions + .iter() + .filter(|(_, v)| **v == self.inner.correct) + .fold( + vec![0; self.inner.answers.len()].into_boxed_slice(), + |mut acc, (_, v)| { acc[*v as usize] += 1; - } - acc - }, - ), - submissions_wrong: &self.submissions.iter().fold( - vec![0; self.inner.answers.len()], - |mut acc, (_, v)| { - if *v != self.inner.correct { + acc + }, + ), + submissions_wrong: &self + .submissions + .iter() + .filter(|(_, v)| **v != self.inner.correct) + .fold( + vec![0; self.inner.answers.len()].into_boxed_slice(), + |mut acc, (_, v)| { acc[*v as usize] += 1; - } - acc - }, - ), + acc + }, + ), correct_answer: &self.inner.answers[self.inner.correct as usize], answers: &self.inner.answers, } @@ -126,7 +133,7 @@ impl Question for SingleChoiceQuestion { struct PlayerTemplate<'a> { name: &'a str, player_id: &'a str, - answers: &'a [String], + answers: &'a [Box], } #[derive(TemplateOnce)] @@ -147,5 +154,5 @@ struct ViewerTemplate<'a> { submissions_correct: &'a [u32], submissions_wrong: &'a [u32], correct_answer: &'a str, - answers: &'a [String], + answers: &'a [Box], } diff --git a/src/stream.rs b/src/stream.rs index fcb480d..fbe6433 100644 --- a/src/stream.rs +++ b/src/stream.rs @@ -15,43 +15,46 @@ use tokio::{ }; use tokio_util::sync::ReusableBoxFuture; -use crate::{error::Error, game::Game}; +use crate::{ + error::Error, + game::{Game, GameId, PlayerId}, +}; pub struct PlayerBroadcastStream { - id: String, - player_id: String, - games: Arc>>, + id: GameId, + player_id: PlayerId, + games: Arc>>, inner: ReusableBoxFuture<'static, (Result, RecvError>, Receiver<()>)>, } impl PlayerBroadcastStream { async fn make_future( mut rx: Receiver<()>, - games: Arc>>, - id: String, - player_id: String, + games: Arc>>, + id: GameId, + player_id: PlayerId, ) -> (Result, RecvError>, Receiver<()>) { let result = match rx.recv().await { - Ok(_) => Ok(Self::build_template(games, id, player_id).await), + Ok(_) => Ok(Self::build_template(games, &id, &player_id).await), Err(e) => Err(e), }; (result, rx) } async fn build_template( - games: Arc>>, - id: String, - player_id: String, + games: Arc>>, + id: &GameId, + player_id: &PlayerId, ) -> Result { let games = games.read().await; - let game = games.get(&id).ok_or(Error::NotFound)?; + let game = games.get(id).ok_or(Error::NotFound)?; - Ok(Event::default().data(game.player_view(&player_id, true).await?)) + Ok(Event::default().data(game.player_view(player_id, true).await?)) } pub fn new( recv: Receiver<()>, - games: Arc>>, - id: String, - player_id: String, + games: Arc>>, + id: GameId, + player_id: PlayerId, ) -> Self { Self { inner: ReusableBoxFuture::new(Self::make_future( @@ -91,8 +94,8 @@ impl Stream for PlayerBroadcastStream { } pub struct ViewerBroadcastStream { - id: String, - games: Arc>>, + id: GameId, + games: Arc>>, inner: ReusableBoxFuture< 'static, ( @@ -102,16 +105,16 @@ pub struct ViewerBroadcastStream { ), >, - base_url: String, + base_url: &'static str, } impl ViewerBroadcastStream { async fn make_future( mut rx1: Receiver<()>, mut rx2: Receiver<()>, - games: Arc>>, - id: String, - base_url: String, + games: Arc>>, + id: GameId, + base_url: &'static str, ) -> ( Result, RecvError>, Receiver<()>, @@ -121,27 +124,27 @@ impl ViewerBroadcastStream { a = rx1.recv() => a, b = rx2.recv() => b } { - Ok(_) => Ok(Self::build_template(games, id, base_url).await), + Ok(_) => Ok(Self::build_template(games, &id, base_url).await), Err(e) => Err(e), }; (result, rx1, rx2) } async fn build_template( - games: Arc>>, - id: String, - base_url: String, + games: Arc>>, + id: &GameId, + base_url: &'static str, ) -> Result { let games = games.read().await; - let game = games.get(&id).ok_or(Error::NotFound)?; + let game = games.get(id).ok_or(Error::NotFound)?; - Ok(Event::default().data(game.viewer_view(true, &base_url).await?)) + Ok(Event::default().data(game.viewer_view(true, base_url).await?)) } pub fn new( rx1: Receiver<()>, rx2: Receiver<()>, - games: Arc>>, - id: String, - base_url: String, + games: Arc>>, + id: GameId, + base_url: &'static str, ) -> Self { Self { inner: ReusableBoxFuture::new(Self::make_future( @@ -149,7 +152,7 @@ impl ViewerBroadcastStream { rx2, games.clone(), id.clone(), - base_url.clone(), + base_url, )), games, id, @@ -166,13 +169,8 @@ impl Stream for ViewerBroadcastStream { cx: &mut std::task::Context<'_>, ) -> std::task::Poll> { let (result, rx1, rx2) = ready!(self.inner.poll(cx)); - let future = Self::make_future( - rx1, - rx2, - self.games.clone(), - self.id.clone(), - self.base_url.clone(), - ); + let future = + Self::make_future(rx1, rx2, self.games.clone(), self.id.clone(), self.base_url); self.inner.set(future); match result { Ok(item) => Poll::Ready(Some(item)),