v1.1.0 — Gzip canvas compression + throttled sends
All checks were successful
CI / ci (push) Successful in 6m29s
All checks were successful
CI / ci (push) Successful in 6m29s
- Canvas snapshots gzip-compressed before sending: 1MB raw -> ~1-5KB for a mostly-white canvas, ~100-500KB for a busy one - Semaphore throttles concurrent canvas sends (default 20) to prevent memory spikes during connection bursts - Frontend decompresses gzip via browser DecompressionStream API - New config: MAX_CONCURRENT_CANVAS_SENDS (default 20) - Added flate2 dependency for gzip encoding
This commit is contained in:
@@ -7,6 +7,7 @@ edition.workspace = true
|
||||
application = { workspace = true }
|
||||
domain = { workspace = true }
|
||||
axum = { workspace = true, features = ["ws"] }
|
||||
flate2 = { workspace = true }
|
||||
futures = { workspace = true }
|
||||
serde = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
use std::io::Write;
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::Ordering;
|
||||
|
||||
@@ -5,8 +6,10 @@ use axum::extract::State;
|
||||
use axum::extract::ws::{Message, WebSocket, WebSocketUpgrade};
|
||||
use axum::response::IntoResponse;
|
||||
use domain::{BroadcastEvent, BroadcastSubscription, Color, Position};
|
||||
use flate2::Compression;
|
||||
use flate2::write::GzEncoder;
|
||||
use futures::{SinkExt, StreamExt, stream::SplitSink};
|
||||
use tokio::sync::mpsc;
|
||||
use tokio::sync::{Semaphore, mpsc};
|
||||
use tracing::{error, info};
|
||||
|
||||
use crate::WsState;
|
||||
@@ -36,7 +39,7 @@ async fn handle_connection(socket: WebSocket, state: Arc<WsState>) {
|
||||
// Subscribe before snapshotting to avoid missing updates
|
||||
let subscription = state.app_state.broadcaster().subscribe();
|
||||
|
||||
if !send_canvas_snapshot(&mut sender, &state.app_state).await {
|
||||
if !send_canvas_snapshot(&mut sender, &state.app_state, &state.canvas_send_semaphore).await {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -67,10 +70,39 @@ async fn handle_connection(socket: WebSocket, state: Arc<WsState>) {
|
||||
application::soldiers::disconnect::execute(&state.app_state, &connection_id);
|
||||
}
|
||||
|
||||
async fn send_canvas_snapshot(sender: &mut WsSender, state: &AppState) -> bool {
|
||||
async fn send_canvas_snapshot(
|
||||
sender: &mut WsSender,
|
||||
state: &AppState,
|
||||
semaphore: &Semaphore,
|
||||
) -> bool {
|
||||
let Ok(_permit) = semaphore.acquire().await else {
|
||||
return false;
|
||||
};
|
||||
|
||||
let pixels = application::canvas::get_state::execute(state);
|
||||
let bytes = Color::collect_as_bytes(&pixels);
|
||||
sender.send(Message::Binary(bytes.into())).await.is_ok()
|
||||
let raw_bytes = Color::collect_as_bytes(&pixels);
|
||||
|
||||
let Some(compressed) = gzip_compress(&raw_bytes) else {
|
||||
error!("Failed to compress canvas snapshot");
|
||||
return false;
|
||||
};
|
||||
|
||||
info!(
|
||||
"Sending canvas snapshot: {}KB raw -> {}KB gzip",
|
||||
raw_bytes.len() / 1024,
|
||||
compressed.len() / 1024
|
||||
);
|
||||
|
||||
sender
|
||||
.send(Message::Binary(compressed.into()))
|
||||
.await
|
||||
.is_ok()
|
||||
}
|
||||
|
||||
fn gzip_compress(data: &[u8]) -> Option<Vec<u8>> {
|
||||
let mut encoder = GzEncoder::new(Vec::new(), Compression::fast());
|
||||
encoder.write_all(data).ok()?;
|
||||
encoder.finish().ok()
|
||||
}
|
||||
|
||||
async fn run_send_loop(
|
||||
|
||||
@@ -6,16 +6,19 @@ use std::sync::atomic::AtomicU64;
|
||||
|
||||
use application::AppState;
|
||||
use axum::{Router, routing::get};
|
||||
use tokio::sync::Semaphore;
|
||||
|
||||
pub(crate) struct WsState {
|
||||
app_state: Arc<AppState>,
|
||||
connection_counter: AtomicU64,
|
||||
canvas_send_semaphore: Arc<Semaphore>,
|
||||
}
|
||||
|
||||
pub fn build_router(state: Arc<AppState>) -> Router {
|
||||
pub fn build_router(state: Arc<AppState>, max_concurrent_canvas_sends: usize) -> Router {
|
||||
let ws_state = Arc::new(WsState {
|
||||
app_state: state,
|
||||
connection_counter: AtomicU64::new(0),
|
||||
canvas_send_semaphore: Arc::new(Semaphore::new(max_concurrent_canvas_sends)),
|
||||
});
|
||||
|
||||
Router::new()
|
||||
|
||||
Reference in New Issue
Block a user