From 3f8693c929540be3790fc24c3bf16ac5f8cd76c9 Mon Sep 17 00:00:00 2001 From: SinuS Von SifriduS Date: Tue, 22 Sep 2026 08:49:40 +0200 Subject: [PATCH] 0.3.5-alpha.10 --- Cargo.toml | 5 +- README.md | 6 +- .../Cargo.toml | 20 + .../src/main.rs | 514 ++++++++++++++++++ deltas/0.3.5/alpha.10.md | 177 ++++++ docs/000-README.md | 3 +- .../005-V0_3_5_WEBTRANSPORT_QUIC_POC_PLAN.md | 24 +- docs/studies/000-README.md | 3 +- ...7-V0_3_5_REALTIME_TRANSPORT_MEASUREMENT.md | 191 +++++++ history/0.3.5/alpha.9.fix.1.md | 78 +++ scripts/audit_distribution_layout.py | 41 +- 11 files changed, 1046 insertions(+), 16 deletions(-) create mode 100644 crates/apps/game-realtime-transport-measure/Cargo.toml create mode 100644 crates/apps/game-realtime-transport-measure/src/main.rs create mode 100644 deltas/0.3.5/alpha.10.md create mode 100644 docs/studies/027-V0_3_5_REALTIME_TRANSPORT_MEASUREMENT.md create mode 100644 history/0.3.5/alpha.9.fix.1.md diff --git a/Cargo.toml b/Cargo.toml index 2c7ac78..b8bdb3d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,5 @@ # file: Cargo.toml -# version: 108 +# version: 109 [workspace] resolver = "3" @@ -10,6 +10,7 @@ members = [ "crates/games/game-reflex-poc", "crates/games/game-snake-poc", "crates/apps/game-realtime-transport-fallback-smoke", + "crates/apps/game-realtime-transport-measure", "crates/apps/game-realtime-websocket-smoke", "crates/apps/game-realtime-webtransport-browser-smoke", "crates/apps/game-realtime-webtransport-datagram-smoke", @@ -29,7 +30,7 @@ members = [ ] [workspace.package] -version = "0.3.5-alpha.9.fix.1" +version = "0.3.5-alpha.10" edition = "2024" license = "MIT" repository = "https://git.sasedev.com/Sasedev/games" diff --git a/README.md b/README.md index 234330e..5e76db8 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ - + # games.sasedev @@ -27,9 +27,9 @@ Workspace expérimental puis productif pour des jeux multiplateformes principale Version stable de référence : `0.3.4`. -Version active : `0.3.5-alpha.9.fix.1`. `0.3.2` reste différée. +Version active : `0.3.5-alpha.10`. `0.3.2` reste différée. -La stable `0.3.4` livre la première baseline realtime : contrat binaire transport-neutral, backend WebSocket Tokio/tokio-tungstenite, limites et deadlines, tests loopback/robustesse, smoke runtime localhost public et frontières de dépendances empêchant moteurs et gameplay de dépendre d'un backend concret. `0.3.5-alpha.9.fix.1` conserve les chemins WebTransport natif et navigateur validés, la composition WebTransport-first avec fallback WebSocket strictement classifié, puis ajoute le POC datagram backend-spécifique isolé de `alpha.9` avec son correctif de documentation du crate de test d’intégration. Les datagrams restent hors du contrat `RealtimeConnection` : ils sont non fiables, non ordonnés et bornés par la taille négociée de la session. Les mesures comparatives restent réservées à la tranche suivante. +La stable `0.3.4` livre la première baseline realtime : contrat binaire transport-neutral, backend WebSocket Tokio/tokio-tungstenite, limites et deadlines, tests loopback/robustesse, smoke runtime localhost public et frontières de dépendances empêchant moteurs et gameplay de dépendre d'un backend concret. `0.3.5-alpha.10` conserve les chemins WebTransport natif et navigateur validés, la composition WebTransport-first avec fallback WebSocket strictement classifié et le POC datagram backend-spécifique, puis ajoute un outil borné de caractérisation loopback WebSocket/WebTransport. Les mesures d’établissement, RTT, throughput et fenêtre de messages en vol restent descriptives ; le datagram est mesuré séparément et n’entre toujours pas dans `RealtimeConnection`. La conclusion technique provisoire reste de conserver WebTransport comme second backend à côté de WebSocket, sans prétendre qu’un résultat loopback prédit les performances Internet/mobile. Les deux premiers jeux sont des POC structurels : `game-reflex-poc` et `game-snake-poc`. Ils existent d'abord pour valider les frontières du workspace, le moteur, les assets et le packaging multiplateforme. diff --git a/crates/apps/game-realtime-transport-measure/Cargo.toml b/crates/apps/game-realtime-transport-measure/Cargo.toml new file mode 100644 index 0000000..2fe4885 --- /dev/null +++ b/crates/apps/game-realtime-transport-measure/Cargo.toml @@ -0,0 +1,20 @@ +# file: crates/apps/game-realtime-transport-measure/Cargo.toml +# version: 1 + +[package] +name = "game-realtime-transport-measure" +version.workspace = true +edition.workspace = true +license.workspace = true +repository.workspace = true +authors.workspace = true +publish.workspace = true + +[dependencies] +game-realtime-transport-lib = { path = "../../common/game-realtime-transport-lib" } +game-realtime-websocket-lib = { path = "../../common/game-realtime-websocket-lib" } +game-realtime-webtransport-lib = { path = "../../common/game-realtime-webtransport-lib" } +tokio = { workspace = true, features = ["macros", "rt", "time"] } + +[lints] +workspace = true diff --git a/crates/apps/game-realtime-transport-measure/src/main.rs b/crates/apps/game-realtime-transport-measure/src/main.rs new file mode 100644 index 0000000..77d3ed1 --- /dev/null +++ b/crates/apps/game-realtime-transport-measure/src/main.rs @@ -0,0 +1,514 @@ +// file: crates/apps/game-realtime-transport-measure/src/main.rs +// version: 1 + +#![warn(missing_docs)] +#![deny(unreachable_pub)] +#![forbid(unsafe_code)] + +//! Bounded localhost characterization tool for WebSocket and WebTransport realtime backends. + +use game_realtime_transport_lib::RealtimeConnection; // rust-rules: trait-import +use game_realtime_transport_lib::RealtimeReceiver; // rust-rules: trait-import +use game_realtime_transport_lib::RealtimeSender; // rust-rules: trait-import + +const ACK_PAYLOAD: &[u8] = b"ack"; +const DATAGRAM_PAYLOAD_BYTES: usize = 256; +const DATAGRAM_RECEIVE_TIMEOUT: std::time::Duration = std::time::Duration::from_millis(250); +const DATAGRAM_SAMPLES: usize = 64; +const ESTABLISHMENT_SAMPLES: usize = 8; +const IN_FLIGHT_MESSAGES: usize = 64; +const IN_FLIGHT_PAYLOAD_BYTES: usize = 1024; +const MEASUREMENT_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(30); +const RTT_PAYLOAD_BYTES: usize = 32; +const RTT_SAMPLES: usize = 128; +const RTT_WARMUP: usize = 16; +const THROUGHPUT_MESSAGES: usize = 128; +const THROUGHPUT_PAYLOAD_BYTES: usize = 64 * 1024; + +#[derive(Clone, Copy)] +struct LatencySummary { + min_us: u128, + median_us: u128, + p95_us: u128, + max_us: u128, +} + +struct TransferSummary { + elapsed: std::time::Duration, + messages: usize, + payload_bytes: usize, +} + +struct DatagramSummary { + elapsed: std::time::Duration, + attempted: usize, + received: usize, + payload_bytes: usize, + client_max: usize, + server_max: usize, +} + +#[tokio::main(flavor = "current_thread")] +async fn main() -> std::process::ExitCode { + let result = tokio::time::timeout(MEASUREMENT_TIMEOUT, run_measurements()).await; + return match result { + std::result::Result::Ok(std::result::Result::Ok(())) => { + println!("CONCLUSION webtransport=retain scope=second-backend reason=reliable-browser-fallback-datagram-capabilities"); + println!("game-realtime-transport-measure: PASS"); + std::process::ExitCode::SUCCESS + }, + std::result::Result::Ok(std::result::Result::Err(error)) => { + eprintln!("game-realtime-transport-measure: FAIL: {error}"); + std::process::ExitCode::FAILURE + }, + std::result::Result::Err(_) => { + eprintln!("game-realtime-transport-measure: FAIL: measurement timeout after {} ms", MEASUREMENT_TIMEOUT.as_millis()); + std::process::ExitCode::FAILURE + }, + }; +} + +async fn run_measurements() -> std::result::Result<(), String> { + let websocket_listener = match game_realtime_websocket_lib::WebSocketListener::bind(std::net::SocketAddr::from(([127, 0, 0, 1], 0))).await { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(format!("WebSocket measurement listener bind failed: {error}")), + }; + let websocket_endpoint = format!("ws://{}/measure", websocket_listener.local_addr()); + let identity = match game_realtime_webtransport_lib::WebTransportServerIdentity::generate_loopback() { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(format!("WebTransport measurement identity generation failed: {error}")), + }; + let certificate_hash = identity.certificate_hash().clone(); + let server_config = game_realtime_webtransport_lib::WebTransportServerConfig::new(std::net::SocketAddr::from(([127, 0, 0, 1], 0)), identity); + let mut webtransport_listener = match game_realtime_webtransport_lib::WebTransportListener::bind(server_config) { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(format!("WebTransport measurement listener bind failed: {error}")), + }; + let webtransport_endpoint = format!("https://{}/measure", webtransport_listener.local_addr()); + let webtransport_client_config = match game_realtime_webtransport_lib::WebTransportClientConfig::new(webtransport_endpoint.as_str(), certificate_hash) { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(format!("WebTransport measurement client configuration failed: {error}")), + }; + let websocket_establishment = match measure_websocket_establishment(&websocket_listener, websocket_endpoint.as_str()).await { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(error), + }; + print_latency("websocket", "establishment", ESTABLISHMENT_SAMPLES, 0, websocket_establishment); + let webtransport_establishment = match measure_webtransport_establishment(&mut webtransport_listener, &webtransport_client_config).await { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(error), + }; + print_latency("webtransport", "establishment", ESTABLISHMENT_SAMPLES, 0, webtransport_establishment); + let (websocket_client, websocket_server) = match establish_websocket_pair(&websocket_listener, websocket_endpoint.as_str()).await { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(error), + }; + let websocket_rtt = match measure_rtt(websocket_client, websocket_server).await { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(error), + }; + print_latency("websocket", "rtt", RTT_SAMPLES, RTT_PAYLOAD_BYTES, websocket_rtt); + let (webtransport_client, webtransport_server) = match establish_webtransport_pair(&mut webtransport_listener, &webtransport_client_config).await { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(error), + }; + let webtransport_rtt = match measure_rtt(webtransport_client, webtransport_server).await { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(error), + }; + print_latency("webtransport", "rtt", RTT_SAMPLES, RTT_PAYLOAD_BYTES, webtransport_rtt); + let (websocket_client, websocket_server) = match establish_websocket_pair(&websocket_listener, websocket_endpoint.as_str()).await { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(error), + }; + let websocket_throughput = match measure_transfer(websocket_client, websocket_server, THROUGHPUT_MESSAGES, THROUGHPUT_PAYLOAD_BYTES).await { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(error), + }; + print_transfer("websocket", "throughput", &websocket_throughput); + let (webtransport_client, webtransport_server) = match establish_webtransport_pair(&mut webtransport_listener, &webtransport_client_config).await { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(error), + }; + let webtransport_throughput = match measure_transfer(webtransport_client, webtransport_server, THROUGHPUT_MESSAGES, THROUGHPUT_PAYLOAD_BYTES).await { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(error), + }; + print_transfer("webtransport", "throughput", &webtransport_throughput); + let (websocket_client, websocket_server) = match establish_websocket_pair(&websocket_listener, websocket_endpoint.as_str()).await { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(error), + }; + let websocket_in_flight = match measure_transfer(websocket_client, websocket_server, IN_FLIGHT_MESSAGES, IN_FLIGHT_PAYLOAD_BYTES).await { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(error), + }; + print_transfer("websocket", "in_flight_window", &websocket_in_flight); + let (webtransport_client, webtransport_server) = match establish_webtransport_pair(&mut webtransport_listener, &webtransport_client_config).await { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(error), + }; + let webtransport_in_flight = match measure_transfer(webtransport_client, webtransport_server, IN_FLIGHT_MESSAGES, IN_FLIGHT_PAYLOAD_BYTES).await { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(error), + }; + print_transfer("webtransport", "in_flight_window", &webtransport_in_flight); + let datagram = match measure_webtransport_datagrams(&mut webtransport_listener, &webtransport_client_config).await { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(error), + }; + print_datagram(&datagram); + return std::result::Result::Ok(()); +} + +async fn measure_websocket_establishment( + listener: &game_realtime_websocket_lib::WebSocketListener, + endpoint: &str, +) -> std::result::Result { + let mut samples = Vec::with_capacity(ESTABLISHMENT_SAMPLES); + for _ in 0..ESTABLISHMENT_SAMPLES { + let started = std::time::Instant::now(); + let (server, client) = tokio::join!(listener.accept(), game_realtime_websocket_lib::connect(endpoint)); + let _server = match server { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(format!("WebSocket establishment server failed: {error}")), + }; + let _client = match client { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(format!("WebSocket establishment client failed: {error}")), + }; + samples.push(started.elapsed()); + } + return summarize_latency(samples.as_slice()); +} + +async fn measure_webtransport_establishment( + listener: &mut game_realtime_webtransport_lib::WebTransportListener, + client_config: &game_realtime_webtransport_lib::WebTransportClientConfig, +) -> std::result::Result { + let mut samples = Vec::with_capacity(ESTABLISHMENT_SAMPLES); + for _ in 0..ESTABLISHMENT_SAMPLES { + let started = std::time::Instant::now(); + let _pair = match establish_webtransport_pair(listener, client_config).await { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(error), + }; + samples.push(started.elapsed()); + } + return summarize_latency(samples.as_slice()); +} + +async fn establish_websocket_pair( + listener: &game_realtime_websocket_lib::WebSocketListener, + endpoint: &str, +) -> std::result::Result<(game_realtime_websocket_lib::WebSocketConnection, game_realtime_websocket_lib::WebSocketConnection), String> { + let (server, client) = tokio::join!(listener.accept(), game_realtime_websocket_lib::connect(endpoint)); + let server = match server { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(format!("WebSocket server establishment failed: {error}")), + }; + let client = match client { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(format!("WebSocket client establishment failed: {error}")), + }; + return std::result::Result::Ok((client, server)); +} + +async fn establish_webtransport_pair( + listener: &mut game_realtime_webtransport_lib::WebTransportListener, + client_config: &game_realtime_webtransport_lib::WebTransportClientConfig, +) -> std::result::Result<(game_realtime_webtransport_lib::WebTransportConnection, game_realtime_webtransport_lib::WebTransportConnection), String> { + let server = async { + let session = match listener.accept().await { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(format!("WebTransport server session establishment failed: {error}")), + }; + return match session.accept_primary_connection().await { + std::result::Result::Ok(value) => std::result::Result::Ok(value), + std::result::Result::Err(error) => std::result::Result::Err(format!("WebTransport server primary stream establishment failed: {error}")), + }; + }; + let client = async { + let session = match game_realtime_webtransport_lib::connect(client_config).await { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(format!("WebTransport client session establishment failed: {error}")), + }; + return match session.open_primary_connection().await { + std::result::Result::Ok(value) => std::result::Result::Ok(value), + std::result::Result::Err(error) => std::result::Result::Err(format!("WebTransport client primary stream establishment failed: {error}")), + }; + }; + let (server, client) = tokio::join!(server, client); + return match (client, server) { + (std::result::Result::Ok(client), std::result::Result::Ok(server)) => std::result::Result::Ok((client, server)), + (std::result::Result::Err(error), _) => std::result::Result::Err(error), + (_, std::result::Result::Err(error)) => std::result::Result::Err(error), + }; +} + +async fn measure_rtt(client: ClientConnection, server: ServerConnection) -> std::result::Result +where + ClientConnection: game_realtime_transport_lib::RealtimeConnection, + ServerConnection: game_realtime_transport_lib::RealtimeConnection, +{ + let total_messages = RTT_WARMUP + RTT_SAMPLES; + let payload = vec![0x52_u8; RTT_PAYLOAD_BYTES]; + let (mut client_sender, mut client_receiver) = client.split(); + let (mut server_sender, mut server_receiver) = server.split(); + let server = async move { + for _ in 0..total_messages { + let message = match server_receiver.receive().await { + std::result::Result::Ok(game_realtime_transport_lib::TransportReceive::Message(value)) => value, + std::result::Result::Ok(game_realtime_transport_lib::TransportReceive::Closed) => { + return std::result::Result::Err(String::from("RTT server observed an early clean close")); + }, + std::result::Result::Err(error) => return std::result::Result::Err(format!("RTT server receive failed: {error}")), + }; + if let std::result::Result::Err(error) = server_sender.send(message).await { + return std::result::Result::Err(format!("RTT server echo failed: {error}")); + } + } + let client_close = match server_receiver.receive().await { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(format!("RTT server close observation failed: {error}")), + }; + if client_close != game_realtime_transport_lib::TransportReceive::Closed { + return std::result::Result::Err(String::from("RTT server did not observe the client close")); + } + if let std::result::Result::Err(error) = server_sender.close().await { + return std::result::Result::Err(format!("RTT server close failed: {error}")); + } + return std::result::Result::Ok(()); + }; + let client = async move { + let mut samples = Vec::with_capacity(RTT_SAMPLES); + for index in 0..total_messages { + let started = std::time::Instant::now(); + if let std::result::Result::Err(error) = client_sender.send(game_realtime_transport_lib::TransportMessage::new(payload.clone())).await { + return std::result::Result::Err(format!("RTT client send failed: {error}")); + } + let response = match client_receiver.receive().await { + std::result::Result::Ok(game_realtime_transport_lib::TransportReceive::Message(value)) => value, + std::result::Result::Ok(game_realtime_transport_lib::TransportReceive::Closed) => { + return std::result::Result::Err(String::from("RTT client observed an early clean close")); + }, + std::result::Result::Err(error) => return std::result::Result::Err(format!("RTT client receive failed: {error}")), + }; + if response.as_bytes() != payload.as_slice() { + return std::result::Result::Err(String::from("RTT echo payload mismatch")); + } + if index >= RTT_WARMUP { + samples.push(started.elapsed()); + } + } + if let std::result::Result::Err(error) = client_sender.close().await { + return std::result::Result::Err(format!("RTT client close failed: {error}")); + } + let server_close = match client_receiver.receive().await { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(format!("RTT client close observation failed: {error}")), + }; + if server_close != game_realtime_transport_lib::TransportReceive::Closed { + return std::result::Result::Err(String::from("RTT client did not observe the server close")); + } + return summarize_latency(samples.as_slice()); + }; + let (server, client) = tokio::join!(server, client); + match server { + std::result::Result::Ok(()) => {}, + std::result::Result::Err(error) => return std::result::Result::Err(error), + } + return client; +} + +async fn measure_transfer( + client: ClientConnection, + server: ServerConnection, + messages: usize, + payload_bytes: usize, +) -> std::result::Result +where + ClientConnection: game_realtime_transport_lib::RealtimeConnection, + ServerConnection: game_realtime_transport_lib::RealtimeConnection, +{ + let payload = vec![0x54_u8; payload_bytes]; + let (mut client_sender, mut client_receiver) = client.split(); + let (mut server_sender, mut server_receiver) = server.split(); + let server = async move { + for _ in 0..messages { + let received = match server_receiver.receive().await { + std::result::Result::Ok(game_realtime_transport_lib::TransportReceive::Message(value)) => value, + std::result::Result::Ok(game_realtime_transport_lib::TransportReceive::Closed) => { + return std::result::Result::Err(String::from("transfer server observed an early clean close")); + }, + std::result::Result::Err(error) => return std::result::Result::Err(format!("transfer server receive failed: {error}")), + }; + if received.len() != payload_bytes { + return std::result::Result::Err(format!("transfer server payload size mismatch: expected {payload_bytes}, got {}", received.len())); + } + } + if let std::result::Result::Err(error) = server_sender.send(game_realtime_transport_lib::TransportMessage::new(ACK_PAYLOAD.to_vec())).await { + return std::result::Result::Err(format!("transfer server ACK failed: {error}")); + } + let client_close = match server_receiver.receive().await { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(format!("transfer server close observation failed: {error}")), + }; + if client_close != game_realtime_transport_lib::TransportReceive::Closed { + return std::result::Result::Err(String::from("transfer server did not observe the client close")); + } + if let std::result::Result::Err(error) = server_sender.close().await { + return std::result::Result::Err(format!("transfer server close failed: {error}")); + } + return std::result::Result::Ok(()); + }; + let client = async move { + let started = std::time::Instant::now(); + for _ in 0..messages { + if let std::result::Result::Err(error) = client_sender.send(game_realtime_transport_lib::TransportMessage::new(payload.clone())).await { + return std::result::Result::Err(format!("transfer client send failed: {error}")); + } + } + let ack = match client_receiver.receive().await { + std::result::Result::Ok(game_realtime_transport_lib::TransportReceive::Message(value)) => value, + std::result::Result::Ok(game_realtime_transport_lib::TransportReceive::Closed) => { + return std::result::Result::Err(String::from("transfer client observed close before ACK")); + }, + std::result::Result::Err(error) => return std::result::Result::Err(format!("transfer client ACK receive failed: {error}")), + }; + if ack.as_bytes() != ACK_PAYLOAD { + return std::result::Result::Err(String::from("transfer ACK payload mismatch")); + } + let elapsed = started.elapsed(); + if let std::result::Result::Err(error) = client_sender.close().await { + return std::result::Result::Err(format!("transfer client close failed: {error}")); + } + let server_close = match client_receiver.receive().await { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(format!("transfer client close observation failed: {error}")), + }; + if server_close != game_realtime_transport_lib::TransportReceive::Closed { + return std::result::Result::Err(String::from("transfer client did not observe the server close")); + } + return std::result::Result::Ok(TransferSummary { elapsed, messages, payload_bytes }); + }; + let (server, client) = tokio::join!(server, client); + match server { + std::result::Result::Ok(()) => {}, + std::result::Result::Err(error) => return std::result::Result::Err(error), + } + return client; +} + +async fn measure_webtransport_datagrams( + listener: &mut game_realtime_webtransport_lib::WebTransportListener, + client_config: &game_realtime_webtransport_lib::WebTransportClientConfig, +) -> std::result::Result { + let (server, client) = tokio::join!(listener.accept(), game_realtime_webtransport_lib::connect(client_config)); + let server = match server { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(format!("datagram measurement server session failed: {error}")), + }; + let client = match client { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(format!("datagram measurement client session failed: {error}")), + }; + let client_max = client.max_datagram_size(); + let server_max = server.max_datagram_size(); + let negotiated_max = core::cmp::min(client_max, server_max); + if negotiated_max == 0 { + return std::result::Result::Err(String::from("datagram measurement negotiated a zero-byte payload bound")); + } + let payload_bytes = core::cmp::min(DATAGRAM_PAYLOAD_BYTES, negotiated_max); + let payload = vec![0x44_u8; payload_bytes]; + let started = std::time::Instant::now(); + for _ in 0..DATAGRAM_SAMPLES { + if let std::result::Result::Err(error) = client.send_datagram(payload.as_slice()) { + return std::result::Result::Err(format!("datagram measurement send failed: {error}")); + } + } + let mut received = 0_usize; + while received < DATAGRAM_SAMPLES { + let datagram = tokio::time::timeout(DATAGRAM_RECEIVE_TIMEOUT, server.receive_datagram()).await; + match datagram { + std::result::Result::Ok(std::result::Result::Ok(value)) => { + if value.len() != payload_bytes { + return std::result::Result::Err(format!("datagram measurement payload size mismatch: expected {payload_bytes}, got {}", value.len())); + } + received += 1; + }, + std::result::Result::Ok(std::result::Result::Err(error)) => { + return std::result::Result::Err(format!("datagram measurement receive failed: {error}")); + }, + std::result::Result::Err(_) => break, + } + } + return std::result::Result::Ok(DatagramSummary { + elapsed: started.elapsed(), + attempted: DATAGRAM_SAMPLES, + received, + payload_bytes, + client_max, + server_max, + }); +} + +fn summarize_latency(samples: &[std::time::Duration]) -> std::result::Result { + if samples.is_empty() { + return std::result::Result::Err(String::from("latency summary requires at least one sample")); + } + let mut values: Vec = samples.iter().map(std::time::Duration::as_micros).collect(); + values.sort_unstable(); + let min_us = match values.first() { + Some(value) => *value, + None => return std::result::Result::Err(String::from("latency summary unexpectedly lost all samples")), + }; + let max_us = match values.last() { + Some(value) => *value, + None => return std::result::Result::Err(String::from("latency summary unexpectedly lost all samples")), + }; + let median_us = match values.get(values.len() / 2) { + Some(value) => *value, + None => return std::result::Result::Err(String::from("latency median index is invalid")), + }; + let p95_rank = values.len().saturating_mul(95).div_ceil(100); + let p95_index = p95_rank.saturating_sub(1); + let p95_us = match values.get(p95_index) { + Some(value) => *value, + None => return std::result::Result::Err(String::from("latency p95 index is invalid")), + }; + return std::result::Result::Ok(LatencySummary { min_us, median_us, p95_us, max_us }); +} + +fn print_latency(transport: &str, metric: &str, samples: usize, payload_bytes: usize, summary: LatencySummary) { + println!( + "MEASURE transport={transport} metric={metric} samples={samples} payload_bytes={payload_bytes} min_us={} median_us={} p95_us={} max_us={}", + summary.min_us, summary.median_us, summary.p95_us, summary.max_us + ); +} + +fn print_transfer(transport: &str, metric: &str, summary: &TransferSummary) { + let elapsed_seconds = summary.elapsed.as_secs_f64(); + let total_bytes = summary.messages.saturating_mul(summary.payload_bytes); + let mib_per_second = if elapsed_seconds > 0.0 { (total_bytes as f64 / (1024.0 * 1024.0)) / elapsed_seconds } else { 0.0 }; + let messages_per_second = if elapsed_seconds > 0.0 { summary.messages as f64 / elapsed_seconds } else { 0.0 }; + println!( + "MEASURE transport={transport} metric={metric} messages={} payload_bytes={} total_bytes={total_bytes} elapsed_us={} mib_per_s={mib_per_second:.3} messages_per_s={messages_per_second:.3}", + summary.messages, + summary.payload_bytes, + summary.elapsed.as_micros() + ); +} + +fn print_datagram(summary: &DatagramSummary) { + let ratio = if summary.attempted == 0 { 0.0 } else { summary.received as f64 / summary.attempted as f64 }; + println!( + "MEASURE transport=webtransport-datagram metric=bounded_burst attempted={} received={} receive_ratio={ratio:.6} payload_bytes={} client_max={} server_max={} elapsed_us={}", + summary.attempted, + summary.received, + summary.payload_bytes, + summary.client_max, + summary.server_max, + summary.elapsed.as_micros() + ); +} diff --git a/deltas/0.3.5/alpha.10.md b/deltas/0.3.5/alpha.10.md new file mode 100644 index 0000000..f66f310 --- /dev/null +++ b/deltas/0.3.5/alpha.10.md @@ -0,0 +1,177 @@ + + + +# Delta 0.3.5-alpha.10 + +## Base requise + +`0.3.5-alpha.9.fix.1`, validée par l'utilisateur le 2026-09-22. + +La gate finale confirme fmt, audits, `cargo check --workspace`, Clippy strict, `22/22` tests WebTransport, smoke datagram natif `PASS` et check/Clippy/build WASM. La preuve est enregistrée dans `history/0.3.5/alpha.9.fix.1.md`. + +## Objectif + +Caractériser de manière bornée les backends realtime déjà retenus sans introduire de nouveau contrat produit : + +- établissement WebSocket vs WebTransport fiable ; +- RTT applicatif ; +- throughput fiable ; +- fenêtre bornée de messages sans ACK intermédiaire ; +- datagram WebTransport mesuré séparément ; +- méthode et limites documentées ; +- conclusion technique provisoire `retain` sans classement automatique. + +## Version + +La version workspace passe à : + +```text +0.3.5-alpha.10 +``` + +## Nouveau launcher + +Nouvelle application technique : + +```text +crates/apps/game-realtime-transport-measure +``` + +Elle dépend uniquement : + +```text +game-realtime-transport-lib +game-realtime-websocket-lib +game-realtime-webtransport-lib +tokio +``` + +Elle ne dépend d'aucun engine ni gameplay et ne modifie aucune crate backend. + +## Mesures fiables + +La comparaison utilise uniquement `RealtimeConnection` pour les deux backends. + +### Établissement + +```text +8 échantillons +WebSocket : handshake jusqu'à connexion prête +WebTransport : session + stream primaire jusqu'à connexion fiable prête +``` + +### RTT + +```text +16 warmups +128 échantillons +32 octets par ping/pong +``` + +### Throughput + +```text +128 messages +64 KiB par message +8 MiB utiles +ACK final après drainage serveur +``` + +### Fenêtre applicative en vol + +```text +64 messages +1 KiB par message +aucun ACK applicatif intermédiaire +ACK unique après drainage +``` + +Cette dernière mesure ne prétend pas mesurer directement le nombre de paquets réseau en vol ; elle caractérise une fenêtre applicative bornée de messages non acquittés individuellement. + +## Datagram séparé + +WebTransport est également caractérisé hors `RealtimeConnection` : + +```text +64 datagrams +payload = min(256, client_max, server_max) +réception jusqu'à 64 ou 250 ms sans nouveau datagram +``` + +Le launcher rapporte `attempted`, `received`, `receive_ratio`, les bornes négociées et le temps écoulé. Il n'interprète pas une réception complète comme une garantie de fiabilité. + +## Sortie + +Chaque résultat est imprimé sous une forme stable : + +```text +MEASURE transport=<...> metric=<...> ... +``` + +La fin normale est : + +```text +CONCLUSION webtransport=retain scope=second-backend reason=reliable-browser-fallback-datagram-capabilities +game-realtime-transport-measure: PASS +``` + +Le `retain` signifie uniquement conserver WebTransport comme second backend à côté de WebSocket. Aucun seuil de victoire et aucune affirmation « plus rapide » n'est codé dans l'outil. + +## Documentation + +La méthode et ses limites sont figées dans : + +```text +docs/studies/027-V0_3_5_REALTIME_TRANSPORT_MEASUREMENT.md +``` + +Les valeurs de la machine de validation seront enregistrées dans `history/0.3.5/alpha.10.md` après la gate utilisateur plutôt que figées comme vérité générale dans la documentation durable. + +## Fichiers principaux + +Nouveaux : + +```text +crates/apps/game-realtime-transport-measure/Cargo.toml +crates/apps/game-realtime-transport-measure/src/main.rs +deltas/0.3.5/alpha.10.md +docs/studies/027-V0_3_5_REALTIME_TRANSPORT_MEASUREMENT.md +history/0.3.5/alpha.9.fix.1.md +``` + +Modifiés : + +```text +Cargo.toml +README.md +docs/plans/005-V0_3_5_WEBTRANSPORT_QUIC_POC_PLAN.md +scripts/audit_distribution_layout.py +``` + +## Validation attendue + +```bash +cargo fmt --all +cargo fmt --all -- --check + +python3 scripts/audit_rust_workspace_rules.py +python3 scripts/audit_markdown_tables.py README.md RULES.md ROADMAP.md CHANGELOG.md docs prompts crates Android Web deltas history +python3 scripts/audit_distribution_layout.py + +cargo check --workspace +cargo clippy --workspace --all-targets --all-features -- -D warnings + +cargo test -p game-realtime-transport-lib --all-targets --all-features +cargo test -p game-realtime-websocket-lib --all-targets --all-features +cargo test -p game-realtime-webtransport-lib --all-targets --all-features + +cargo run --release -p game-realtime-transport-measure + +cargo tree -p game-realtime-transport-measure --edges normal +cargo tree -i game-realtime-websocket-lib --workspace --edges normal +cargo tree -i game-realtime-webtransport-lib --workspace --edges normal +``` + +## Après validation + +Enregistrer les lignes `MEASURE ...` réelles dans `history/0.3.5/alpha.10.md`, puis passer à `0.3.5-beta.1` pour la validation large. Un défaut fermé de `alpha.10` doit produire `alpha.10.fix.N` avant `beta.1`. diff --git a/docs/000-README.md b/docs/000-README.md index f89838b..9f35d0a 100644 --- a/docs/000-README.md +++ b/docs/000-README.md @@ -1,5 +1,5 @@ - + # Documentation games.sasedev @@ -12,6 +12,7 @@ - [`ideas/000-README.md`](ideas/000-README.md) — rôle des idées non engagées et règles de maturation. - [`studies/000-README.md`](studies/000-README.md) — rôle des études comparatives non normatives. - [`studies/026-V0_3_5_WEBTRANSPORT_QUIC_STACK_AUDIT.md`](studies/026-V0_3_5_WEBTRANSPORT_QUIC_STACK_AUDIT.md) — audit `0.3.5` des stacks WebTransport/QUIC, du contrat transport, de TLS et des plateformes. +- [`studies/027-V0_3_5_REALTIME_TRANSPORT_MEASUREMENT.md`](studies/027-V0_3_5_REALTIME_TRANSPORT_MEASUREMENT.md) — méthode `0.3.5-alpha.10` de caractérisation loopback WebSocket/WebTransport fiable et datagram séparé. ## Plans diff --git a/docs/plans/005-V0_3_5_WEBTRANSPORT_QUIC_POC_PLAN.md b/docs/plans/005-V0_3_5_WEBTRANSPORT_QUIC_POC_PLAN.md index 1d44dff..9ac0756 100644 --- a/docs/plans/005-V0_3_5_WEBTRANSPORT_QUIC_POC_PLAN.md +++ b/docs/plans/005-V0_3_5_WEBTRANSPORT_QUIC_POC_PLAN.md @@ -1,11 +1,11 @@ - + # Plan 0.3.5 — POC WebTransport/QUIC et fallback WebSocket ## Statut -Plan actif créé pendant `0.3.5-alpha.1` à partir de l'archive taggée `v0.3.4`, puis réconcilié pour l'implémentation native de `0.3.5-alpha.2`, son correctif de validation `0.3.5-alpha.2.fix.1`, le chemin fiable validé de `0.3.5-alpha.3`, la robustesse validée par `0.3.5-alpha.4.fix.1`, le smoke natif validé de `0.3.5-alpha.5`, le chemin client WASM validé de `0.3.5-alpha.6`, le smoke navigateur validé par `0.3.5-alpha.7.fix.2`, la composition/fallback WebSocket validée par `0.3.5-alpha.8.fix.1` et le POC datagram backend-spécifique de `0.3.5-alpha.9`, corrigé par `0.3.5-alpha.9.fix.1` après la gate Clippy. +Plan actif créé pendant `0.3.5-alpha.1` à partir de l'archive taggée `v0.3.4`, puis réconcilié pour l'implémentation native de `0.3.5-alpha.2`, son correctif de validation `0.3.5-alpha.2.fix.1`, le chemin fiable validé de `0.3.5-alpha.3`, la robustesse validée par `0.3.5-alpha.4.fix.1`, le smoke natif validé de `0.3.5-alpha.5`, le chemin client WASM validé de `0.3.5-alpha.6`, le smoke navigateur validé par `0.3.5-alpha.7.fix.2`, la composition/fallback WebSocket validée par `0.3.5-alpha.8.fix.1`, le POC datagram backend-spécifique validé par `0.3.5-alpha.9.fix.1`, puis la caractérisation comparative bornée de `0.3.5-alpha.10`. Le cadrage détaillé et la comparaison des stacks actuelles sont conservés dans `docs/studies/026-V0_3_5_WEBTRANSPORT_QUIC_STACK_AUDIT.md`. Le présent document porte les décisions opérationnelles, le scope, les gates et le forecast vivant de la version. @@ -555,12 +555,20 @@ Correctif `0.3.5-alpha.9.fix.1` : la gate utilisateur de `alpha.9` confirme fmt/ ### `0.3.5-alpha.10` — mesures comparatives bornées -- launcher/outil minimal de mesure ; -- WebSocket vs WebTransport fiable ; -- établissement, RTT, throughput, messages en vol ; -- datagram seulement s'il existe réellement ; -- résultats et limites de méthode documentés ; -- conclusion technique provisoire `retain/defer/reject`. +La tranche ajoute `game-realtime-transport-measure`, un outil localhost déterministe qui ne modifie aucun backend et ne publie aucune API commune nouvelle. La méthode est figée ainsi : + +- 8 établissements complets par backend ; WebTransport inclut la session QUIC/HTTP3 et l'ouverture du stream primaire, WebSocket inclut le handshake ; +- RTT fiable : 16 warmups puis 128 ping/pong de 32 octets ; +- throughput fiable : 128 messages de 64 KiB, soit 8 MiB utiles, avec ACK final après drainage côté serveur ; +- fenêtre applicative sans ACK intermédiaire : 64 messages de 1 KiB ; +- datagram WebTransport séparé : 64 envois de taille `min(256, max_datagram_size client, max_datagram_size serveur)`, nombre reçu observé sous deadline locale ; +- sortie textuelle stable `MEASURE ...` suivie de `game-realtime-transport-measure: PASS`. + +Les métriques fiables utilisent strictement `RealtimeConnection`; le chemin datagram reste sur `WebTransportSession`. Les chiffres sont une caractérisation loopback de la machine qui exécute la gate, pas un benchmark Internet/mobile ni une preuve de supériorité d'un protocole. Aucun seuil arbitraire de victoire n'est introduit. + +La conclusion technique provisoire est **`retain`** : conserver WebTransport comme second backend à côté de WebSocket pour la suite du projet, compte tenu du chemin natif+navigateur validé, du fallback classifié et de la capacité datagram optionnelle. Cette conclusion ne signifie pas « WebTransport est plus rapide » ; les mesures de la gate utilisateur seront enregistrées dans l'historique après exécution. + +La méthode, le format des résultats et leurs limites sont détaillés dans `docs/studies/027-V0_3_5_REALTIME_TRANSPORT_MEASUREMENT.md`. ### `0.3.5-beta.1` — validation large diff --git a/docs/studies/000-README.md b/docs/studies/000-README.md index 977e65e..6713edc 100644 --- a/docs/studies/000-README.md +++ b/docs/studies/000-README.md @@ -1,5 +1,5 @@ - + # Études @@ -69,3 +69,4 @@ Ces documents étudient les POC à réaliser ; ils ne lancent encore aucune impl ## Étude de cadrage 0.3.5 - [`026-V0_3_5_WEBTRANSPORT_QUIC_STACK_AUDIT.md`](026-V0_3_5_WEBTRANSPORT_QUIC_STACK_AUDIT.md) — audit de la stable `0.3.4`, comparaison des stacks WebTransport/QUIC actuelles, compatibilité du contrat realtime, TLS, navigateurs et sizing de `0.3.5`. +- [`027-V0_3_5_REALTIME_TRANSPORT_MEASUREMENT.md`](027-V0_3_5_REALTIME_TRANSPORT_MEASUREMENT.md) — méthode bornée de mesure loopback WebSocket/WebTransport de `alpha.10`, datagrams séparés et limites d’interprétation. diff --git a/docs/studies/027-V0_3_5_REALTIME_TRANSPORT_MEASUREMENT.md b/docs/studies/027-V0_3_5_REALTIME_TRANSPORT_MEASUREMENT.md new file mode 100644 index 0000000..4c645b2 --- /dev/null +++ b/docs/studies/027-V0_3_5_REALTIME_TRANSPORT_MEASUREMENT.md @@ -0,0 +1,191 @@ + + + +# Étude 0.3.5 — caractérisation bornée WebSocket / WebTransport + +## Objet + +Cette étude définit la méthode de `0.3.5-alpha.10`. Elle ne cherche pas à produire un benchmark général de WebSocket contre WebTransport, mais à caractériser les deux implémentations **dans games.sasedev**, sur loopback, avec le même contrat fiable et des charges bornées. + +Les valeurs dépendent de la machine, du kernel, du scheduler, du profil Cargo et de l'état local du système. Elles ne doivent pas être extrapolées à un réseau mobile, à Internet, à un navigateur distant ou à une charge serveur multi-utilisateur. + +## Outil + +Le launcher technique est : + +```text +crates/apps/game-realtime-transport-measure +``` + +Exécution de référence : + +```bash +cargo run --release -p game-realtime-transport-measure +``` + +Le profil `--release` est recommandé pour réduire le poids du code de mesure lui-même. La gate peut aussi exécuter le profil debug pour vérifier le fonctionnement, mais les chiffres debug ne doivent pas être comparés à des chiffres release. + +## Préparation commune + +Les deux chemins fiables utilisent leurs configurations produit par défaut et le contrat `RealtimeConnection`. + +WebSocket : + +```text +TCP loopback +ws://127.0.0.1:/measure +Tokio + tokio-tungstenite +``` + +WebTransport : + +```text +UDP loopback +https://127.0.0.1:/measure +QUIC + HTTP/3 +certificat P-256 loopback en mémoire +pin SHA-256 exact +stream bidirectionnel primaire +``` + +L'outil ne configure ni loss artificiel, ni jitter, ni bandwidth shaping. + +## Établissement + +Pour chaque backend : + +```text +samples = 8 +``` + +WebSocket mesure du début du connect/accept jusqu'à la connexion établie après handshake. + +WebTransport mesure jusqu'au chemin fiable utilisable : session WebTransport établie **et** stream bidirectionnel primaire ouvert/accepté. + +Le rapport fournit : + +```text +min_us +median_us +p95_us +max_us +``` + +Les premières connexions peuvent inclure des coûts froids ; aucun échantillon d'établissement n'est supprimé. + +## RTT fiable + +Charge : + +```text +warmup = 16 +samples = 128 +payload = 32 octets +``` + +Le client envoie un message et attend l'écho exact avant d'envoyer le suivant. Seuls les 128 échantillons post-warmup entrent dans le résumé. + +Le résultat mesure donc le RTT applicatif du contrat `RealtimeConnection`, pas le RTT brut TCP/QUIC. + +## Throughput fiable + +Charge : + +```text +messages = 128 +payload = 64 KiB +volume utile = 8 MiB +``` + +Le client envoie les messages sans ACK applicatif intermédiaire. Le serveur draine exactement les 128 messages puis renvoie un ACK final. Le chronomètre client s'arrête à réception de cet ACK. + +Le résultat fournit : + +```text +elapsed_us +mib_per_s +messages_per_s +``` + +Il inclut le framing du backend, les copies/allocation du POC et la backpressure réellement appliquée par les wrappers. + +## Fenêtre de messages en vol + +Charge : + +```text +messages = 64 +payload = 1 KiB +``` + +Le client émet 64 messages sans ACK applicatif intermédiaire, puis attend un ACK unique après drainage côté serveur. + +Cette mesure ne prétend pas connaître le nombre exact de paquets réseau simultanément en vol. Elle valide une **fenêtre applicative bornée de 64 messages non acquittés individuellement** et rapporte son temps de complétion. + +## Datagram WebTransport + +Le datagram est mesuré séparément du contrat fiable : + +```text +attempted = 64 +payload = min(256, client_max_datagram_size, server_max_datagram_size) +``` + +Le client émet les 64 datagrams puis le serveur les reçoit jusqu'à atteindre 64 ou jusqu'à une attente locale de 250 ms sans nouveau datagram. + +Le rapport fournit : + +```text +attempted +received +receive_ratio +payload_bytes +client_max +server_max +elapsed_us +``` + +`receive_ratio = 1.0` sur loopback ne transforme pas les datagrams en transport fiable. Une perte observée n'est pas automatiquement un défaut de protocole : elle doit être interprétée avec la sémantique non fiable de WebTransport. + +## Format de sortie + +Chaque mesure utilise une ligne stable : + +```text +MEASURE transport=<...> metric=<...> ... +``` + +La fin normale est : + +```text +CONCLUSION webtransport=retain scope=second-backend reason=reliable-browser-fallback-datagram-capabilities +game-realtime-transport-measure: PASS +``` + +## Lecture des résultats + +Aucun seuil automatique « gagnant/perdant » n'est défini. Une différence loopback de quelques microsecondes ou quelques pourcents n'a pas de valeur produit suffisante à elle seule. + +Les dimensions utiles sont : + +- absence d'échec ou de timeout inattendu ; +- ordre de grandeur de l'établissement ; +- stabilité médiane/p95 du RTT ; +- absence d'effondrement évident du throughput fiable ; +- capacité à soutenir la fenêtre applicative bornée ; +- observation distincte de la capacité datagram. + +## Conclusion technique provisoire + +Décision `alpha.10` : **retain**. + +Cela signifie : conserver WebTransport comme second backend realtime expérimental/évolutif à côté de WebSocket. Cette décision repose sur l'ensemble des preuves `0.3.5` déjà acquises : + +- backend natif fiable ; +- client navigateur/WASM ; +- smoke navigateur réel ; +- robustesse et deadlines ; +- fallback WebSocket strictement classifié ; +- datagrams optionnels backend-spécifiques. + +Cette conclusion **ne signifie pas** que WebTransport est déclaré plus rapide que WebSocket. Les chiffres de `alpha.10` servent à détecter des écarts grossiers et à documenter la baseline locale avant la validation large `beta.1`. diff --git a/history/0.3.5/alpha.9.fix.1.md b/history/0.3.5/alpha.9.fix.1.md new file mode 100644 index 0000000..ffdc92d --- /dev/null +++ b/history/0.3.5/alpha.9.fix.1.md @@ -0,0 +1,78 @@ + + + +# Historique 0.3.5-alpha.9.fix.1 + +## Statut + +`0.3.5-alpha.9.fix.1` a été validée par l'utilisateur le 2026-09-22. Le POC datagram WebTransport backend-spécifique est fermé sans promotion de cette sémantique dans le contrat `RealtimeConnection`. + +## Gates + +Les commandes utilisateur ont terminé proprement : + +```text +cargo fmt --all +cargo fmt --all -- --check +python3 scripts/audit_rust_workspace_rules.py +python3 scripts/audit_markdown_tables.py README.md RULES.md ROADMAP.md CHANGELOG.md docs prompts crates Android Web deltas history +python3 scripts/audit_distribution_layout.py +cargo check --workspace +cargo clippy --workspace --all-targets --all-features -- -D warnings +cargo test -p game-realtime-webtransport-lib --all-targets --all-features +cargo run -p game-realtime-webtransport-datagram-smoke +cargo check -p game-realtime-webtransport-lib --target wasm32-unknown-unknown +cargo clippy -p game-realtime-webtransport-lib --target wasm32-unknown-unknown --lib -- -D warnings +cargo build -p game-realtime-webtransport-lib --target wasm32-unknown-unknown +``` + +La sortie confirme : + +```text +General Rust rule audit: clean +Rust export completeness audit: 0 candidate(s) +games.sasedev workspace audit: clean +Markdown table audit: clean (5 table(s), 306 file(s)) +Distribution layout audit: clean (64 required path(s), 8 forbidden path(s) absent) +cargo check --workspace: clean +cargo clippy --workspace --all-targets --all-features -- -D warnings: clean +WebTransport tests: 22 passed; 0 failed +wasm32 check/clippy/build: clean +``` + +## Datagram natif + +Le smoke réel termine par : + +```text +game-realtime-webtransport-datagram-smoke: PASS +``` + +Cette exécution a négocié des bornes de payload différentes selon les deux sessions : + +```text +client_max=1350 +server_max=1287 +``` + +Le smoke utilise la borne commune minimale pour ses payloads. Cette variation confirme qu'une taille datagram ne doit pas être codée en dur et doit rester une capacité négociée/backend-spécifique. + +## Tests datagram + +Les tests d'intégration valident : + +```text +backend_specific_datagrams_round_trip_without_entering_the_common_contract ... ok +oversized_datagram_is_rejected_before_backend_send ... ok +``` + +Aucune garantie de livraison ou d'ordre n'est déduite de ce succès loopback. + +## Conséquence + +`alpha.10` peut mesurer séparément : + +- le chemin fiable commun WebSocket/WebTransport via `RealtimeConnection` ; +- le datagram WebTransport comme capacité distincte. + +Les mesures doivent rester bornées, reproductibles et descriptives ; elles ne doivent pas introduire un classement arbitraire ni extrapoler le loopback à Internet/mobile. diff --git a/scripts/audit_distribution_layout.py b/scripts/audit_distribution_layout.py index 06cf765..56f63e6 100644 --- a/scripts/audit_distribution_layout.py +++ b/scripts/audit_distribution_layout.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # file: scripts/audit_distribution_layout.py -# version: 23 +# version: 24 """Audit the static distribution layout expected by supported POCs.""" @@ -27,6 +27,8 @@ REQUIRED_PATHS = ( "crates/apps/game-realtime-transport-fallback-smoke/Cargo.toml", "crates/apps/game-realtime-transport-fallback-smoke/src/main.rs", "crates/apps/game-realtime-transport-fallback-smoke/unit_tests/fallback.rs", + "crates/apps/game-realtime-transport-measure/Cargo.toml", + "crates/apps/game-realtime-transport-measure/src/main.rs", "crates/apps/game-realtime-websocket-smoke/Cargo.toml", "crates/apps/game-realtime-webtransport-browser-smoke/Cargo.toml", "crates/apps/game-realtime-webtransport-datagram-smoke/Cargo.toml", @@ -396,6 +398,42 @@ def audit_realtime_fallback_smoke(root: pathlib.Path) -> list[str]: return violations +def audit_realtime_transport_measure(root: pathlib.Path) -> list[str]: + """Validate the bounded realtime transport characterization boundary.""" + + violations: list[str] = [] + manifest_path = root / "crates/apps/game-realtime-transport-measure/Cargo.toml" + main_path = root / "crates/apps/game-realtime-transport-measure/src/main.rs" + study_path = root / "docs/studies/027-V0_3_5_REALTIME_TRANSPORT_MEASUREMENT.md" + required = (manifest_path, main_path, study_path) + if not all(path.exists() for path in required): + return violations + manifest = manifest_path.read_text(encoding="utf-8") + main = main_path.read_text(encoding="utf-8") + study = study_path.read_text(encoding="utf-8") + required_fragments = ( + (manifest, 'game-realtime-transport-lib = { path = "../../common/game-realtime-transport-lib" }', "DIST-LAYOUT-102: measurement tool must use the transport-neutral contract"), + (manifest, 'game-realtime-websocket-lib = { path = "../../common/game-realtime-websocket-lib" }', "DIST-LAYOUT-103: measurement tool must consume the WebSocket backend directly"), + (manifest, 'game-realtime-webtransport-lib = { path = "../../common/game-realtime-webtransport-lib" }', "DIST-LAYOUT-104: measurement tool must consume the WebTransport backend directly"), + (main, 'const ESTABLISHMENT_SAMPLES: usize = 8;', "DIST-LAYOUT-105: measurement tool must keep establishment samples bounded"), + (main, 'const RTT_SAMPLES: usize = 128;', "DIST-LAYOUT-106: measurement tool must keep RTT samples bounded"), + (main, 'const THROUGHPUT_MESSAGES: usize = 128;', "DIST-LAYOUT-107: measurement tool must keep throughput messages bounded"), + (main, 'const IN_FLIGHT_MESSAGES: usize = 64;', "DIST-LAYOUT-108: measurement tool must keep the application in-flight window bounded"), + (main, 'MEASURE transport=webtransport-datagram metric=bounded_burst', "DIST-LAYOUT-109: measurement tool must report datagrams separately from the reliable contract"), + (main, 'CONCLUSION webtransport=retain scope=second-backend', "DIST-LAYOUT-110: measurement tool must expose the provisional retain conclusion without declaring a speed winner"), + (main, 'game-realtime-transport-measure: PASS', "DIST-LAYOUT-111: measurement tool must expose a deterministic PASS verdict"), + (study, 'Cette conclusion **ne signifie pas** que WebTransport est déclaré plus rapide que WebSocket.', "DIST-LAYOUT-112: measurement study must forbid interpreting retain as a performance winner"), + ) + for content, fragment, message in required_fragments: + if fragment not in content: + violations.append(message) + for fragment in ("engine-v1-", "game-reflex-poc", "game-snake-poc"): + if fragment in manifest: + violations.append(f"DIST-LAYOUT-113: measurement tool must not depend on engine/gameplay package: {fragment}") + return violations + + + def audit_webtransport_datagram_smoke(root: pathlib.Path) -> list[str]: """Validate the isolated backend-specific WebTransport datagram proof.""" @@ -524,6 +562,7 @@ def main() -> int: contract_violations.extend(audit_snake_tauri_generated_android(root)) contract_violations.extend(audit_android_native_gradle(root)) contract_violations.extend(audit_realtime_fallback_smoke(root)) + contract_violations.extend(audit_realtime_transport_measure(root)) contract_violations.extend(audit_webtransport_browser_smoke(root)) contract_violations.extend(audit_webtransport_datagram_smoke(root)) if missing: