0.3.5-alpha.10

This commit is contained in:
2026-09-22 08:49:40 +02:00
parent c5806e4f78
commit 3f8693c929
11 changed files with 1046 additions and 16 deletions

View File

@@ -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"

View File

@@ -1,5 +1,5 @@
<!-- file: README.md -->
<!-- version: 76 -->
<!-- version: 77 -->
# 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 dinté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 nentre toujours pas dans `RealtimeConnection`. La conclusion technique provisoire reste de conserver WebTransport comme second backend à côté de WebSocket, sans prétendre quun 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.

View File

@@ -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

View File

@@ -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<LatencySummary, String> {
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<LatencySummary, String> {
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<ClientConnection, ServerConnection>(client: ClientConnection, server: ServerConnection) -> std::result::Result<LatencySummary, String>
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<ClientConnection, ServerConnection>(
client: ClientConnection,
server: ServerConnection,
messages: usize,
payload_bytes: usize,
) -> std::result::Result<TransferSummary, String>
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<DatagramSummary, String> {
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<LatencySummary, String> {
if samples.is_empty() {
return std::result::Result::Err(String::from("latency summary requires at least one sample"));
}
let mut values: Vec<u128> = 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()
);
}

177
deltas/0.3.5/alpha.10.md Normal file
View File

@@ -0,0 +1,177 @@
<!-- file: deltas/0.3.5/alpha.10.md -->
<!-- version: 1 -->
# 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`.

View File

@@ -1,5 +1,5 @@
<!-- file: docs/000-README.md -->
<!-- version: 35 -->
<!-- version: 36 -->
# 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

View File

@@ -1,11 +1,11 @@
<!-- file: docs/plans/005-V0_3_5_WEBTRANSPORT_QUIC_POC_PLAN.md -->
<!-- version: 15 -->
<!-- version: 16 -->
# 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

View File

@@ -1,5 +1,5 @@
<!-- file: docs/studies/000-README.md -->
<!-- version: 12 -->
<!-- version: 13 -->
# É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 dinterprétation.

View File

@@ -0,0 +1,191 @@
<!-- file: docs/studies/027-V0_3_5_REALTIME_TRANSPORT_MEASUREMENT.md -->
<!-- version: 1 -->
# É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:<port>/measure
Tokio + tokio-tungstenite
```
WebTransport :
```text
UDP loopback
https://127.0.0.1:<port>/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`.

View File

@@ -0,0 +1,78 @@
<!-- file: history/0.3.5/alpha.9.fix.1.md -->
<!-- version: 1 -->
# 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.

View File

@@ -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: