From a183b33ce832c98f5a5027db03eb3977630419a7 Mon Sep 17 00:00:00 2001 From: javalsai Date: Sat, 30 May 2026 06:26:35 +0200 Subject: [PATCH] perf: remove arc clones, general clones, and unnecessary regex --- Cargo.lock | 39 ------- Cargo.toml | 1 - rustfmt.toml | 1 + src/main.rs | 319 ++++++++++++++++++++++++++++----------------------- 4 files changed, 179 insertions(+), 181 deletions(-) create mode 100644 rustfmt.toml diff --git a/Cargo.lock b/Cargo.lock index 7f86b7f..61b0130 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,15 +2,6 @@ # It is not intended for manual editing. version = 4 -[[package]] -name = "aho-corasick" -version = "1.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" -dependencies = [ - "memchr", -] - [[package]] name = "anyhow" version = "1.0.102" @@ -106,7 +97,6 @@ dependencies = [ "axum", "futures", "rand 0.10.1", - "regex", "serde", "serde_json", "tokio", @@ -677,35 +667,6 @@ dependencies = [ "bitflags", ] -[[package]] -name = "regex" -version = "1.12.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" -dependencies = [ - "aho-corasick", - "memchr", - "regex-automata", - "regex-syntax", -] - -[[package]] -name = "regex-automata" -version = "0.4.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" -dependencies = [ - "aho-corasick", - "memchr", - "regex-syntax", -] - -[[package]] -name = "regex-syntax" -version = "0.8.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" - [[package]] name = "ryu" version = "1.0.23" diff --git a/Cargo.toml b/Cargo.toml index 53f467f..4794c16 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,7 +7,6 @@ edition = "2024" axum = {version="0.8.9",features=["ws"]} futures = "0.3.32" rand = "0.10.1" -regex = "1.12.3" serde = {version="1.0.228",features=["derive"]} serde_json = "1.0.149" tokio = {version="1.52.3",features=["full"]} diff --git a/rustfmt.toml b/rustfmt.toml new file mode 100644 index 0000000..218e203 --- /dev/null +++ b/rustfmt.toml @@ -0,0 +1 @@ +hard_tabs = true diff --git a/src/main.rs b/src/main.rs index 54b6ad2..6bca1ef 100644 --- a/src/main.rs +++ b/src/main.rs @@ -3,6 +3,7 @@ use std::{ fs, io::Write, sync::{Arc, Mutex, mpsc}, + ops::{Deref, DerefMut}, }; use axum::{ @@ -14,77 +15,91 @@ use axum::{ response::{Html, IntoResponse}, routing::get, }; -use futures::StreamExt as _; +use futures::{StreamExt as _}; use rand::random_bool; -use regex::Regex; -use serde::{Deserialize, Serialize}; +use serde::{Deserialize, Serialize, de}; use serde_json::json; use tokio::time::{Duration, sleep}; -#[derive(Deserialize,Serialize,Debug,Ord,Eq,PartialEq,PartialOrd,Clone)] -struct Entry -{ +#[derive(Deserialize, Serialize, Debug, Ord, Eq, PartialEq, PartialOrd, Clone)] +struct Entry { score: u32, person: String, } -enum Leaderboard -{ - Hiscores, - Loscores, - Pingscores, -} #[derive(Clone)] struct AppState { - tx: mpsc::Sender<(Entry,Leaderboard)>, + tx: mpsc::Sender, hiscores: Arc>>, loscores: Arc>>, pingscores: Arc>>, // u64 is reset count and u32 is PB } +struct LeaderboardUpdate { + name: Arc, + update: LeaderboardUpdateType, +} + +enum LeaderboardUpdateType { + Reset { hiscore_pingscore: u32 }, + Increment { loscore: u32 }, +} + static CHANCE: f64 = 1.0/3.0; +const PATH_HISCORES: &str = "hiscores.json"; +const PATH_LOSCORES: &str = "loscores.json"; +const PATH_PINGSCORES: &str = "pingscores.json"; + #[tokio::main] async fn main() { - let file_contents: String = fs::read_to_string("hiscores.json").unwrap(); - let hiscores: Arc>> = Arc::new(Mutex::new(serde_json::from_str(&file_contents).unwrap())); - let file_contents: String = fs::read_to_string("loscores.json").unwrap(); - let loscores: Arc>> = Arc::new(Mutex::new(serde_json::from_str(&file_contents).unwrap())); - let file_contents: String = fs::read_to_string("pingscores.json").unwrap(); - let pingscores: Arc>> = Arc::new(Mutex::new(serde_json::from_str(&file_contents).unwrap())); - let hiscore_clone1 = Arc::clone(&hiscores); - let loscore_clone1 = Arc::clone(&loscores); - let pingscore_clone1 = Arc::clone(&pingscores); + fn read_file de::Deserialize<'de>>(file_path: &str) -> Arc> { + let file_contents: String = fs::read_to_string(file_path).unwrap(); + Arc::new(Mutex::new(serde_json::from_str(&file_contents).unwrap())) + } - let (tx, rx) = mpsc::channel::<(Entry,Leaderboard)>(); + let hiscores: Arc>> = read_file(PATH_HISCORES); + let loscores: Arc>> = read_file(PATH_LOSCORES); + let pingscores: Arc>> = read_file(PATH_PINGSCORES); - tokio::spawn( - async move { - handle_hiscores(rx, hiscore_clone1, loscore_clone1, pingscore_clone1); - } - ); - let pingscore_clone2 = Arc::clone(&pingscores); - tokio::spawn( - async move { - loop // write pingscores every 30s - { - sleep(Duration::from_millis(30000)).await; - let pingscores = pingscore_clone2.lock().unwrap(); - let file_contents: String = serde_json::to_string(&pingscores.clone()).unwrap(); - drop(pingscores); - let mut file = fs::OpenOptions::new().write(true).truncate(true).open("pingscores.json").unwrap(); - file.write_all(file_contents.as_bytes()).unwrap(); - file.flush().unwrap(); + let (tx, rx) = mpsc::channel::(); + + { + let (hiscores, loscores, pingscores) = + (hiscores.clone(), loscores.clone(), pingscores.clone()); + tokio::spawn(async move { + handle_hiscores(rx, &hiscores, &loscores, &pingscores); + }); + } + + { + let pingscores = pingscores.clone(); + tokio::spawn( + async move { + loop // write pingscores every 30s + { + sleep(Duration::from_millis(30000)).await; + let pingscores = pingscores.lock().unwrap(); + let file_contents: String = serde_json::to_string(&pingscores.clone()).unwrap(); + drop(pingscores); + let mut file = fs::OpenOptions::new().write(true).truncate(true).open(PATH_PINGSCORES).unwrap(); + file.write_all(file_contents.as_bytes()).unwrap(); + file.flush().unwrap(); + } } - } - ); + ); + } - let state = AppState { tx,hiscores: Arc::clone(&hiscores),loscores: Arc::clone(&loscores), pingscores: Arc::clone(&pingscores)}; let app = Router::new() .route("/", get(index)) .route("/ws", get(ws_handler)) .route("/leaderboard", get(leaderboard)) - .with_state(state); + .with_state(AppState { + tx, + hiscores: Arc::clone(&hiscores), + loscores: Arc::clone(&loscores), + pingscores: Arc::clone(&pingscores), + }); let listener = tokio::net::TcpListener::bind("0.0.0.0:8084") .await @@ -103,66 +118,85 @@ async fn leaderboard() -> Html<&'static str> { Html(include_str!("../leaderboard.html")) } // receiver: 0 for hiscore, 1 for loscore, 2 for pingscore -fn handle_hiscores(rx: mpsc::Receiver<(Entry, Leaderboard)>, hiscores_arc: Arc>>, loscores_arc: Arc>>,pingscores_arc: Arc>>,) -{ +fn handle_hiscores( + rx: mpsc::Receiver, + hiscores: &Mutex>, + loscores: &Mutex>, + pingscores: &Mutex>, +) { + fn update_scoretable> + DerefMut>( + score_name: &str, + mut scoretable: G, + name: &str, + score: u32, + file_path: &str, + ) { + if scoretable.last().is_none_or(|e| score > e.score) { + println!("New {score_name} {score} by {name}"); + scoretable.push(Entry { + score, + person: name.to_string(), + }); + scoretable.sort(); + scoretable.reverse(); + scoretable.truncate(20); + let file_contents: String = serde_json::to_string(&*scoretable).unwrap(); + drop(scoretable); + let mut file = fs::OpenOptions::new() + .write(true) + .truncate(true) + .open(file_path) + .unwrap(); + file.write_all(file_contents.as_bytes()).unwrap(); + file.flush().unwrap(); + } + } + // Panic galore - let mut hiscores = hiscores_arc.lock().unwrap(); - hiscores.sort(); - hiscores.reverse(); - let file_contents: String = serde_json::to_string(&hiscores.clone()).unwrap(); - drop(hiscores); - let mut file = fs::OpenOptions::new().write(true).truncate(true).open("hiscores.json").unwrap(); + let mut hiscores_lock = hiscores.lock().unwrap(); + hiscores_lock.sort(); + hiscores_lock.reverse(); + let file_contents: String = serde_json::to_string(&hiscores_lock.clone()).unwrap(); + drop(hiscores_lock); + let mut file = fs::OpenOptions::new().write(true).truncate(true).open(PATH_HISCORES).unwrap(); file.write_all(file_contents.as_bytes()).unwrap(); - file.flush().unwrap(); - loop - { - match rx.recv() - { - Ok((new_entry,Leaderboard::Hiscores)) => - { - let mut hiscores = hiscores_arc.lock().unwrap(); - if hiscores.get(19).is_none_or(|hiscore| new_entry.score > hiscore.score) { - println!("New hiscore {new_entry:?}"); - hiscores.push(new_entry); - hiscores.sort(); - hiscores.reverse(); - hiscores.truncate(20); - let file_contents: String = serde_json::to_string(&hiscores.clone()).unwrap(); - drop(hiscores); - let mut file = fs::OpenOptions::new().write(true).truncate(true).open("hiscores.json").unwrap(); - file.write_all(file_contents.as_bytes()).unwrap(); - file.flush().unwrap(); - } - }, - Ok((new_entry,Leaderboard::Loscores)) => - { - let mut loscores = loscores_arc.lock().unwrap(); - if loscores.get(19).is_none_or(|loscore| new_entry.score > loscore.score) { - println!("New loscore {new_entry:?}"); - loscores.push(new_entry); - loscores.sort(); - loscores.reverse(); - loscores.truncate(20); - let file_contents: String = serde_json::to_string(&loscores.clone()).unwrap(); - drop(loscores); - let mut file = fs::OpenOptions::new().write(true).truncate(true).open("loscores.json").unwrap(); - file.write_all(file_contents.as_bytes()).unwrap(); - file.flush().unwrap(); - } - }, - Ok((new_entry,Leaderboard::Pingscores)) => - { - let name = new_entry.person; - let mut pingscores = pingscores_arc.lock().unwrap(); - if new_entry.score > pingscores.get(&name).unwrap_or(&(0,0)).1 // pb + drop(file); + + loop { + let LeaderboardUpdate { name, update } = rx.recv().expect("channel error"); + + match update { + LeaderboardUpdateType::Reset { hiscore_pingscore } => { + // Hiscore + update_scoretable( + "hiscore", + hiscores.lock().unwrap(), + &name, + hiscore_pingscore, + PATH_HISCORES, + ); + + // Pingscore + let mut pingscores = pingscores.lock().unwrap(); + if hiscore_pingscore > pingscores.get(&*name).unwrap_or(&(0, 0)).1 + // pb { - pingscores.entry(name.clone()).or_insert((0,0)).1 = new_entry.score; - println!("{name} new PB: {}",new_entry.score); + pingscores.entry(name.to_string()).or_insert((0, 0)).1 = hiscore_pingscore; + println!("{name} new PB: {hiscore_pingscore}"); }; - pingscores.entry(name.clone()).or_insert((0,0)).0 += 1; // reset count + pingscores.entry(name.to_string()).or_insert((0, 0)).0 += 1; // reset count drop(pingscores); } - Err(error) => println!("{error}"), + + LeaderboardUpdateType::Increment { loscore } => { + update_scoretable( + "loscore", + loscores.lock().unwrap(), + &name, + loscore, + PATH_LOSCORES, + ); + } } } } @@ -171,40 +205,46 @@ async fn ws_handler( ws: WebSocketUpgrade, State(state): State, ) -> impl IntoResponse { - ws.on_upgrade(move |socket| { - let tx = state.tx.clone(); - let hiscores = Arc::clone(&state.hiscores); - let loscores = Arc::clone(&state.loscores); - let pingscores = Arc::clone(&state.pingscores); - async move { - handle_socket(socket, tx, hiscores, loscores, pingscores).await; - } + ws.on_upgrade(|socket| async move { + handle_socket( + socket, + &state.tx, + &state.hiscores, + &state.loscores, + &state.pingscores, + ) + .await; }) } -async fn handle_socket -( +async fn handle_socket( mut socket: WebSocket, - tx: mpsc::Sender<(Entry,Leaderboard)>, - hiscores_arc: Arc>>, - loscores_arc: Arc>>, - pingscores_arc: Arc>>, + tx: &mpsc::Sender, + hiscores: &Mutex>, + loscores: &Mutex>, + pingscores: &Mutex>, ) { let mut value: u32 = 0; - let msg = - { - let hiscores = hiscores_arc.lock().unwrap(); - let loscores = loscores_arc.lock().unwrap(); - let pingscores = pingscores_arc.lock().unwrap(); + + let msg = { + let hiscores = hiscores.lock().unwrap(); + let loscores = loscores.lock().unwrap(); + let pingscores = pingscores.lock().unwrap(); json!({ "hiscores": &*hiscores, "loscores": &*loscores, "pingscores": &*pingscores}).to_string() }; - let name_message = socket.next().await.unwrap().unwrap(); - let name: String = match name_message - { - Message::Text(text) => validate_name(text.to_string()), - _ => "anon".to_string(), + + let name: Arc = match socket.next().await.unwrap().unwrap() { + Message::Text(text) + if let text = text.to_string() + && validate_name(&text) => + { + Arc::from(text.into_boxed_str()) + } + _ => Arc::from("anon"), }; + println!("Client connected: {name}"); + let mut resets: u32 = 0; let mut prev: u32 = 0; @@ -216,15 +256,24 @@ async fn handle_socket match msg { Ok(Message::Text(_)) => { if random_bool(CHANCE) { // reset - let _ = tx.send((Entry{ person: name.clone(), score: value },Leaderboard::Hiscores)); //hiscores - let _ = tx.send((Entry{ person: name.clone(), score: value },Leaderboard::Pingscores)); //pingscores + let _ = tx.send(LeaderboardUpdate { + name: name.clone(), + update: LeaderboardUpdateType::Reset { + hiscore_pingscore: value, + }, + }); resets += 1; value = 0 } // 1/3 chance of failing else { value += 1; if prev == 0 { - let _ = tx.send((Entry{ person: name.clone(), score: resets },Leaderboard::Loscores));//loscores + let _ = tx.send(LeaderboardUpdate { + name: name.clone(), + update: LeaderboardUpdateType::Increment { + loscore: resets, + }, + }); resets = 0; } } @@ -241,25 +290,13 @@ async fn handle_socket } } - -fn validate_name(input: String) -> String { +fn validate_name(input: &str) -> bool { let input = input.trim(); - if input == "null" - { - return "anon".to_string(); - } + // Length check if input.is_empty() || input.len() > 32 { - return "anon".to_string(); + return false; } - // Allow only letters, numbers, _ and - - let re = Regex::new(r"^[a-zA-Z0-9_-]+$").unwrap(); - - if re.is_match(input) { - input.to_string() - } else { - "anon".to_string() - } + input.chars().all(|c| c.is_ascii_alphanumeric()) } -