feat(core): add performance profiles, transport observability, and edge stream budget controls

This commit is contained in:
2026-04-26 12:09:58 +00:00
parent 5304bbb486
commit e709e40404
14 changed files with 708 additions and 138 deletions
+75 -30
View File
@@ -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);
}
}