0.3.5-alpha.10
This commit is contained in:
@@ -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"
|
||||
|
||||
@@ -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 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.
|
||||
|
||||
|
||||
20
crates/apps/game-realtime-transport-measure/Cargo.toml
Normal file
20
crates/apps/game-realtime-transport-measure/Cargo.toml
Normal 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
|
||||
514
crates/apps/game-realtime-transport-measure/src/main.rs
Normal file
514
crates/apps/game-realtime-transport-measure/src/main.rs
Normal 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
177
deltas/0.3.5/alpha.10.md
Normal 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`.
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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 d’interprétation.
|
||||
|
||||
191
docs/studies/027-V0_3_5_REALTIME_TRANSPORT_MEASUREMENT.md
Normal file
191
docs/studies/027-V0_3_5_REALTIME_TRANSPORT_MEASUREMENT.md
Normal 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`.
|
||||
78
history/0.3.5/alpha.9.fix.1.md
Normal file
78
history/0.3.5/alpha.9.fix.1.md
Normal 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.
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user