feat(core): add performance profiles, transport observability, and edge stream budget controls
This commit is contained in:
@@ -32,12 +32,18 @@ pub const FRAME_HEADER_SIZE: usize = 9;
|
||||
pub const MAX_PAYLOAD_SIZE: u32 = 16 * 1024 * 1024;
|
||||
|
||||
// Per-stream flow control constants
|
||||
/// Initial (and maximum) per-stream window size (4 MB).
|
||||
pub const INITIAL_STREAM_WINDOW: u32 = 4 * 1024 * 1024;
|
||||
/// Send WINDOW_UPDATE after consuming this many bytes (half the initial window).
|
||||
/// Default maximum per-stream window size (8 MB).
|
||||
pub const INITIAL_STREAM_WINDOW: u32 = 8 * 1024 * 1024;
|
||||
/// Minimum safe window size used when strict budget pressure requires going below the configured floor.
|
||||
pub const ABSOLUTE_MIN_STREAM_WINDOW: u32 = 16 * 1024;
|
||||
/// Default total TCP/TLS flow-control budget per edge connection (256 MB).
|
||||
pub const DEFAULT_TOTAL_WINDOW_BUDGET: u64 = 256 * 1024 * 1024;
|
||||
/// Default preferred minimum stream window (128 KB). The total budget still wins above this.
|
||||
pub const DEFAULT_MIN_STREAM_WINDOW: u32 = 128 * 1024;
|
||||
/// Send WINDOW_UPDATE after consuming this many bytes when no dynamic window is available.
|
||||
pub const WINDOW_UPDATE_THRESHOLD: u32 = INITIAL_STREAM_WINDOW / 2;
|
||||
/// Maximum window size to prevent overflow.
|
||||
pub const MAX_WINDOW_SIZE: u32 = 4 * 1024 * 1024;
|
||||
pub const MAX_WINDOW_SIZE: u32 = 32 * 1024 * 1024;
|
||||
|
||||
// Sustained stream classification constants
|
||||
/// Throughput threshold for sustained classification (2.5 MB/s = 20 Mbit/s).
|
||||
@@ -55,11 +61,37 @@ pub fn encode_window_update(stream_id: u32, frame_type: u8, increment: u32) -> B
|
||||
}
|
||||
|
||||
/// Compute the target per-stream window size based on the number of active streams.
|
||||
/// Total memory budget is ~200MB shared across all streams. Up to 50 streams get the
|
||||
/// full 4MB window; above that the window scales down to a 1MB floor at 200+ streams.
|
||||
/// The total budget is authoritative: the configured minimum is a preference, not
|
||||
/// permission to exceed the edge-level memory budget under very high concurrency.
|
||||
pub fn compute_window_for_stream_count(active: u32) -> u32 {
|
||||
let per_stream = (200 * 1024 * 1024u64) / (active.max(1) as u64);
|
||||
per_stream.clamp(1 * 1024 * 1024, INITIAL_STREAM_WINDOW as u64) as u32
|
||||
compute_window_for_limits(
|
||||
active,
|
||||
DEFAULT_TOTAL_WINDOW_BUDGET,
|
||||
DEFAULT_MIN_STREAM_WINDOW,
|
||||
INITIAL_STREAM_WINDOW,
|
||||
)
|
||||
}
|
||||
|
||||
pub fn compute_window_for_limits(
|
||||
active: u32,
|
||||
total_budget_bytes: u64,
|
||||
min_window_bytes: u32,
|
||||
max_window_bytes: u32,
|
||||
) -> u32 {
|
||||
let active = active.max(1) as u64;
|
||||
let max_window = max_window_bytes.max(ABSOLUTE_MIN_STREAM_WINDOW);
|
||||
let preferred_min = min_window_bytes
|
||||
.max(ABSOLUTE_MIN_STREAM_WINDOW)
|
||||
.min(max_window);
|
||||
let per_stream_budget = total_budget_bytes
|
||||
.max(ABSOLUTE_MIN_STREAM_WINDOW as u64)
|
||||
/ active;
|
||||
let bounded = per_stream_budget.min(max_window as u64);
|
||||
if bounded >= preferred_min as u64 {
|
||||
bounded as u32
|
||||
} else {
|
||||
bounded.max(ABSOLUTE_MIN_STREAM_WINDOW as u64) as u32
|
||||
}
|
||||
}
|
||||
|
||||
/// Decode a WINDOW_UPDATE payload into a byte increment. Returns None if payload is malformed.
|
||||
@@ -307,6 +339,13 @@ pub struct TunnelIo<S> {
|
||||
write: WriteState,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, Default)]
|
||||
pub struct TunnelQueueDepths {
|
||||
pub ctrl: usize,
|
||||
pub data: usize,
|
||||
pub sustained: usize,
|
||||
}
|
||||
|
||||
impl<S: AsyncRead + AsyncWrite + Unpin> TunnelIo<S> {
|
||||
pub fn new(stream: S, initial_data: Vec<u8>) -> Self {
|
||||
let read_pos = initial_data.len();
|
||||
@@ -346,6 +385,14 @@ impl<S: AsyncRead + AsyncWrite + Unpin> TunnelIo<S> {
|
||||
self.write.sustained_queue.push_back(frame);
|
||||
}
|
||||
|
||||
pub fn queue_depths(&self) -> TunnelQueueDepths {
|
||||
TunnelQueueDepths {
|
||||
ctrl: self.write.ctrl_queue.len(),
|
||||
data: self.write.data_queue.len(),
|
||||
sustained: self.write.sustained_queue.len(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Try to parse a complete frame from the read buffer.
|
||||
/// Uses a parse_pos cursor to avoid drain() on every frame.
|
||||
pub fn try_parse_frame(&mut self) -> Option<Result<Frame, std::io::Error>> {
|
||||
@@ -910,7 +957,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_adaptive_window_zero_streams() {
|
||||
// 0 streams treated as 1: 200MB/1 -> clamped to 4MB max
|
||||
// 0 streams treated as 1: budget/1 -> clamped to max
|
||||
assert_eq!(compute_window_for_stream_count(0), INITIAL_STREAM_WINDOW);
|
||||
}
|
||||
|
||||
@@ -920,47 +967,44 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_adaptive_window_50_streams_full() {
|
||||
// 200MB/50 = 4MB = exactly INITIAL_STREAM_WINDOW
|
||||
assert_eq!(compute_window_for_stream_count(50), INITIAL_STREAM_WINDOW);
|
||||
fn test_adaptive_window_32_streams_full() {
|
||||
// 256MB/32 = 8MB = exactly INITIAL_STREAM_WINDOW
|
||||
assert_eq!(compute_window_for_stream_count(32), INITIAL_STREAM_WINDOW);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_adaptive_window_51_streams_starts_scaling() {
|
||||
// 200MB/51 < 4MB — first value below max
|
||||
let w = compute_window_for_stream_count(51);
|
||||
fn test_adaptive_window_33_streams_starts_scaling() {
|
||||
// 256MB/33 < 8MB — first value below max
|
||||
let w = compute_window_for_stream_count(33);
|
||||
assert!(w < INITIAL_STREAM_WINDOW);
|
||||
assert_eq!(w, (200 * 1024 * 1024u64 / 51) as u32);
|
||||
assert_eq!(w, (DEFAULT_TOTAL_WINDOW_BUDGET / 33) as u32);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_adaptive_window_100_streams() {
|
||||
// 200MB/100 = 2MB
|
||||
assert_eq!(compute_window_for_stream_count(100), 2 * 1024 * 1024);
|
||||
assert_eq!(compute_window_for_stream_count(100), (DEFAULT_TOTAL_WINDOW_BUDGET / 100) as u32);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_adaptive_window_200_streams_at_floor() {
|
||||
// 200MB/200 = 1MB = exactly the floor
|
||||
assert_eq!(compute_window_for_stream_count(200), 1 * 1024 * 1024);
|
||||
fn test_adaptive_window_200_streams_uses_budget() {
|
||||
assert_eq!(compute_window_for_stream_count(200), (DEFAULT_TOTAL_WINDOW_BUDGET / 200) as u32);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_adaptive_window_500_streams_clamped() {
|
||||
// 200MB/500 = 0.4MB -> clamped up to 1MB floor
|
||||
assert_eq!(compute_window_for_stream_count(500), 1 * 1024 * 1024);
|
||||
fn test_adaptive_window_500_streams_stays_under_budget() {
|
||||
assert_eq!(compute_window_for_stream_count(500), (DEFAULT_TOTAL_WINDOW_BUDGET / 500) as u32);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_adaptive_window_max_u32() {
|
||||
// Extreme: u32::MAX streams -> tiny value -> clamped to 1MB
|
||||
assert_eq!(compute_window_for_stream_count(u32::MAX), 1 * 1024 * 1024);
|
||||
// Extreme: u32::MAX streams -> tiny value -> clamped to absolute minimum.
|
||||
assert_eq!(compute_window_for_stream_count(u32::MAX), ABSOLUTE_MIN_STREAM_WINDOW);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_adaptive_window_monotonically_decreasing() {
|
||||
let mut prev = compute_window_for_stream_count(1);
|
||||
for n in [2, 10, 50, 51, 100, 200, 500, 1000] {
|
||||
for n in [2, 10, 32, 33, 100, 200, 500, 1000] {
|
||||
let w = compute_window_for_stream_count(n);
|
||||
assert!(w <= prev, "window increased from {} to {} at n={}", prev, w, n);
|
||||
prev = w;
|
||||
@@ -969,11 +1013,12 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_adaptive_window_total_budget_bounded() {
|
||||
// active x per_stream_window should never exceed 200MB (+ clamp overhead for high N)
|
||||
for n in [1, 10, 50, 100, 200] {
|
||||
// active x per_stream_window should never exceed the configured budget while the
|
||||
// budget can still provide at least the absolute minimum per stream.
|
||||
for n in [1, 10, 32, 33, 100, 200, 500, 1000] {
|
||||
let w = compute_window_for_stream_count(n);
|
||||
let total = w as u64 * n as u64;
|
||||
assert!(total <= 200 * 1024 * 1024, "total {}MB exceeds budget at n={}", total / (1024*1024), n);
|
||||
assert!(total <= DEFAULT_TOTAL_WINDOW_BUDGET, "total {}MB exceeds budget at n={}", total / (1024*1024), n);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user