use super::fixture::*;
use crate::{
Body, BodySource, Headers, HttpConfig, Method, Status,
h2::{frame::Frame, settings::H2Settings},
headers::hpack::PseudoHeaders,
};
use futures_lite::io::AsyncRead;
use std::{
io,
pin::Pin,
task::{Context, Poll},
};
const RESPONSE_LEN: usize = 15_700;
const STREAMS: u32 = 32;
const LARGE_RESPONSE_LEN: usize = 4 * 1024 * 1024;
struct FixedBody {
remaining: usize,
}
impl AsyncRead for FixedBody {
fn poll_read(
self: Pin<&mut Self>,
_cx: &mut Context<'_>,
buf: &mut [u8],
) -> Poll<io::Result<usize>> {
let this = self.get_mut();
let n = this.remaining.min(buf.len());
buf[..n].fill(b'x');
this.remaining -= n;
Poll::Ready(Ok(n))
}
}
impl BodySource for FixedBody {
fn trailers(self: Pin<&mut Self>) -> Option<Headers> {
None
}
}
#[derive(Debug)]
struct DrainStats {
ticks: usize,
data_bytes: u64,
data_frames: usize,
max_frame_payload: u32,
}
impl DrainStats {
fn bytes_per_tick(&self) -> u64 {
self.data_bytes / self.ticks.max(1) as u64
}
}
fn drain_responses(config: HttpConfig, response_len: usize) -> DrainStats {
let window = u32::try_from(response_len * STREAMS as usize * 2).expect("window fits u32");
let mut fx = DriverFixture::new_server_with_config(config);
fx.complete_handshake_with_peer_settings(
H2Settings::default().with_initial_window_size(window),
);
fx.peer_window_update(0, window);
let stream_ids: Vec<u32> = (0..STREAMS).map(|i| 1 + i * 2).collect();
for &id in &stream_ids {
fx.peer_open_stream(id, Method::Get, "/static/app.js", true);
}
let mut conns = Vec::new();
for _ in 0..stream_ids.len() * 2 {
if let Poll::Ready(Some(Ok(conn))) = fx.tick() {
conns.push(conn);
}
}
assert_eq!(
conns.len(),
stream_ids.len(),
"expected one Conn per opened stream",
);
let _ = fx.next_outbound_bytes();
let _submits: Vec<_> = stream_ids
.iter()
.map(|&id| {
let body = Body::new_with_trailers(
FixedBody {
remaining: response_len,
},
Some(response_len as u64),
);
let pseudos = PseudoHeaders::default().with_status(Status::Ok);
fx.connection
.submit_send(id, pseudos, Headers::new(), Some(body))
})
.collect();
let expected = response_len as u64 * u64::from(STREAMS);
let mut stats = DrainStats {
ticks: 0,
data_bytes: 0,
data_frames: 0,
max_frame_payload: 0,
};
for _ in 0..4096 {
if stats.data_bytes >= expected {
break;
}
let _ = fx.tick();
stats.ticks += 1;
for frame in fx.next_outbound_frames() {
if let Frame::Data { data_length, .. } = frame {
stats.data_bytes += u64::from(data_length);
stats.data_frames += 1;
stats.max_frame_payload = stats.max_frame_payload.max(data_length);
}
}
}
assert_eq!(
stats.data_bytes, expected,
"drain did not complete: {stats:?} (expected {expected} body bytes)",
);
stats
}
#[test]
fn work_per_drive_call_across_configs() {
let cases = [
("defaults", HttpConfig::default()),
(
"copy_loops=4",
HttpConfig::default().with_copy_loops_per_yield(4),
),
(
"copy_loops=64",
HttpConfig::default().with_copy_loops_per_yield(64),
),
(
"copy_loops=256",
HttpConfig::default().with_copy_loops_per_yield(256),
),
(
"h2_max_frame_size=64K",
HttpConfig::default().with_h2_max_frame_size(64 * 1024),
),
(
"body_write_chunk_len=64K",
HttpConfig::default().with_body_write_chunk_len(64 * 1024),
),
(
"response_buffer_len=8K",
HttpConfig::default().with_response_buffer_len(8 * 1024),
),
];
for (name, len) in [
("static-h2 shape (15.7 KB x 32)", RESPONSE_LEN),
("large bodies (4 MB x 32)", LARGE_RESPONSE_LEN),
] {
println!(
"\n{name}\n{:<26} {:>7} {:>12} {:>13} {:>8} {:>11}",
"config", "ticks", "data bytes", "bytes/tick", "frames", "max frame"
);
for (label, config) in &cases {
let s = drain_responses(*config, len);
println!(
"{:<26} {:>7} {:>12} {:>13} {:>8} {:>11}",
label,
s.ticks,
s.data_bytes,
s.bytes_per_tick(),
s.data_frames,
s.max_frame_payload
);
}
}
}
#[test]
fn yield_budget_does_not_bound_the_send_path() {
for len in [RESPONSE_LEN, LARGE_RESPONSE_LEN] {
for loops in [4, 16, 64, 256] {
let s = drain_responses(HttpConfig::default().with_copy_loops_per_yield(loops), len);
assert_eq!(
s.ticks, 1,
"copy_loops_per_yield={loops} should still drain {STREAMS} x {len}B in one drive \
call; got {s:?}",
);
}
}
}
#[test]
fn own_max_frame_size_does_not_govern_outbound_framing() {
let default = drain_responses(HttpConfig::default(), LARGE_RESPONSE_LEN);
let raised = drain_responses(
HttpConfig::default().with_h2_max_frame_size(64 * 1024),
LARGE_RESPONSE_LEN,
);
assert_eq!(
default.max_frame_payload, raised.max_frame_payload,
"h2_max_frame_size is the inbound limit; raising it changed outbound DATA framing from \
{default:?} to {raised:?}",
);
}