use std::time::Duration;
use lmflow::{register_kernel, BufferData, Builtin, Graph, Kernel, KernelCtx, Packet, Timestamp};
const PACKET_BYTES: usize = 128 * 1024;
const MAX_QUEUED_PACKETS: usize = 32;
const DEFAULT_PACKETS: usize = 2000;
const DIAMOND_MAX_QUEUED_PACKETS: usize = 64;
fn packets() -> usize {
std::env::var("LMFLOW_SOAK_PACKETS")
.ok()
.and_then(|s| s.parse().ok())
.unwrap_or(DEFAULT_PACKETS)
}
fn rss_kb() -> Option<u64> {
#[cfg(target_os = "linux")]
{
let s = std::fs::read_to_string("/proc/self/status").ok()?;
s.lines()
.find_map(|l| l.strip_prefix("VmRSS:"))
.and_then(|v| v.split_whitespace().next())
.and_then(|n| n.parse().ok())
}
#[cfg(not(target_os = "linux"))]
{
None
}
}
#[derive(Default)]
struct SlowConsumer;
impl Kernel for SlowConsumer {
fn process(&mut self, cc: &mut KernelCtx) -> lmflow::Result<()> {
let _pkt = cc.take_input(0);
std::thread::sleep(Duration::from_millis(1));
Ok(())
}
}
fn big_packet(i: usize) -> Packet {
let mut bd = BufferData::new(&[PACKET_BYTES as i64], 0 ).unwrap();
bd.bytes = vec![(i & 0xFF) as u8; PACKET_BYTES];
Packet::from_builtin(Builtin::Buffer(bd)).at(Timestamp(i as i64))
}
#[derive(Default)]
struct SlowRelay;
impl Kernel for SlowRelay {
fn process(&mut self, cc: &mut KernelCtx) -> lmflow::Result<()> {
let pkt = cc.take_input(0);
std::thread::sleep(Duration::from_millis(2));
cc.emit(0, pkt)
}
}
#[derive(Default)]
struct FastRelay;
impl Kernel for FastRelay {
fn process(&mut self, cc: &mut KernelCtx) -> lmflow::Result<()> {
cc.forward(0, 0)
}
}
#[derive(Default)]
struct JoinSink {
seen: u64,
}
impl Kernel for JoinSink {
fn process(&mut self, cc: &mut KernelCtx) -> lmflow::Result<()> {
let _a = cc.take_input(0);
let _b = cc.take_input(1);
self.seen += 1;
cc.counter_add("join.fired", 1);
Ok(())
}
}
#[test]
#[ignore = "长跑压测:默认不进常规套件,用 --ignored 显式跑"]
fn watermark_bounds_memory_over_long_run() {
let _ = register_kernel::<SlowConsumer>("SoakSlowConsumer");
let n = packets();
let cfg = format!(
r#"
executors:
- {{ name: "cpu", type: "ThreadPoolExecutor", num_threads: 1 }}
nodes:
- {{ name: "c", kernel: "SoakSlowConsumer", executor: "cpu", input_ports: ["in"], output_ports: [] }}
input_ports: ["in"]
max_queue_size: 100000
max_queued_packets: {MAX_QUEUED_PACKETS}
"#
);
let graph = Graph::from_yaml(&cfg).unwrap();
graph.start().unwrap();
let input = graph.input("in").unwrap();
let shared = graph.shared_for_inspection();
for i in 0..32 {
input.send(big_packet(i)).unwrap();
}
std::thread::sleep(Duration::from_millis(50));
let baseline = rss_kb();
let mut curve: Vec<u64> = Vec::new();
let mut peak_queued: usize = 0;
let mut peak_queued_bytes: u64 = 0;
std::thread::scope(|scope| {
let input_ref = &input;
let sender = scope.spawn(move || {
for i in 32..n {
input_ref
.send(big_packet(i))
.unwrap_or_else(|e| panic!("send #{i} failed: {e}"));
}
});
let mut next_rss_sample = std::time::Instant::now();
while !sender.is_finished() {
peak_queued = peak_queued.max(shared.total_queued());
peak_queued_bytes = peak_queued_bytes.max(shared.total_queued_bytes());
if std::time::Instant::now() >= next_rss_sample {
if let Some(kb) = rss_kb() {
curve.push(kb);
}
next_rss_sample += Duration::from_millis(100);
}
std::thread::sleep(Duration::from_micros(25));
}
sender.join().unwrap();
});
graph.close_all_inputs();
graph
.wait_done_timeout(Duration::from_secs(300))
.expect("soak run should terminate");
let pushed_mib = (n as u64 * PACKET_BYTES as u64) / (1024 * 1024);
println!("soak: {n} packets × {PACKET_BYTES} B = {pushed_mib} MiB pushed");
println!("soak: peak total_queued = {peak_queued} packets (watermark {MAX_QUEUED_PACKETS})");
println!(
"soak: observed peak total_queued_bytes = {} KiB",
peak_queued_bytes / 1024,
);
let watermark = input.backpressure_stats();
println!(
"soak: watermark blocks={} total={}us",
watermark.block_events, watermark.total_blocked_us
);
assert!(
watermark.block_events > 0,
"水位从未触发背压—— 本次运行没有形成积压,故内存断言不具说服力;\
请增大 LMFLOW_SOAK_PACKETS 或调慢消费者"
);
let slack = 8;
assert!(
peak_queued <= MAX_QUEUED_PACKETS + slack,
"积压包数峰值 {peak_queued} 超出水位 {MAX_QUEUED_PACKETS} 太多\
(宽容 {slack} 包)—— 背压没有真正生效"
);
let processed = graph.node_stats(0).unwrap().processed;
assert_eq!(processed, n as u64, "背压路径不该丢包");
let Some(base) = baseline else {
println!("soak: 非 Linux 平台,跳过 RSS 曲线断言(水位与丢包断言仍然有效)");
return;
};
let peak_rss = curve.iter().copied().max().unwrap_or(base);
let growth_kb = peak_rss.saturating_sub(base);
println!("soak: RSS 基线 {base} KiB → 峰值 {peak_rss} KiB(增长 {growth_kb} KiB)");
println!("soak: RSS 曲线(KiB){curve:?}");
let allowed_kb = (MAX_QUEUED_PACKETS * PACKET_BYTES / 1024) as u64 + 32 * 1024;
assert!(
growth_kb <= allowed_kb,
"RSS 增长 {growth_kb} KiB 超过允许的 {allowed_kb} KiB。\
本次共推入 {pushed_mib} MiB —— 若增长与吞吐同量级,说明包没有被释放\
(水位读数可能仍然正常:那意味着某条路径漏了 on_dequeue 之外的释放)"
);
}
#[test]
#[ignore = "长跑压测:默认不进常规套件,用 --ignored 显式跑"]
fn watermark_bounds_memory_in_diamond_topology() {
let _ = register_kernel::<SlowRelay>("SoakSlowRelay");
let _ = register_kernel::<FastRelay>("SoakFastRelay");
let _ = register_kernel::<JoinSink>("SoakJoinSink");
let n = packets();
let cfg = format!(
r#"
executors:
- {{ name: "cpu", type: "ThreadPoolExecutor", num_threads: 3 }}
nodes:
- {{ name: "slow", kernel: "SoakSlowRelay", executor: "cpu", input_ports: ["in"], output_ports: ["s"] }}
- {{ name: "fast", kernel: "SoakFastRelay", executor: "cpu", input_ports: ["in"], output_ports: ["f"] }}
- name: "join"
kernel: "SoakJoinSink"
executor: "cpu"
input_ports: ["s", "f"]
output_ports: []
input_queues:
packets: 2
input_ports: ["in"]
max_queue_size: 100000
max_queued_packets: {DIAMOND_MAX_QUEUED_PACKETS}
"#
);
let graph = Graph::from_yaml(&cfg).unwrap();
graph.start().unwrap();
let input = graph.input("in").unwrap();
let shared = graph.shared_for_inspection();
for i in 0..32 {
input.send(big_packet(i)).unwrap();
}
std::thread::sleep(Duration::from_millis(50));
let baseline = rss_kb();
let mut curve: Vec<u64> = Vec::new();
let mut peak_queued: usize = 0;
std::thread::scope(|scope| {
let input_ref = &input;
let sender = scope.spawn(move || {
for i in 32..n {
input_ref
.send(big_packet(i))
.unwrap_or_else(|e| panic!("send #{i} failed: {e}"));
}
});
let mut next_rss_sample = std::time::Instant::now();
while !sender.is_finished() {
peak_queued = peak_queued.max(shared.total_queued());
if std::time::Instant::now() >= next_rss_sample {
if let Some(kb) = rss_kb() {
curve.push(kb);
}
next_rss_sample += Duration::from_millis(100);
}
std::thread::sleep(Duration::from_micros(25));
}
sender.join().unwrap();
});
graph.close_all_inputs();
graph
.wait_done_timeout(Duration::from_secs(300))
.expect("diamond 必须能跑完 —— 挂住就说明扇出汇合形成了循环等待");
let pushed_mib = (n as u64 * PACKET_BYTES as u64) / (1024 * 1024);
let fired = graph.counter_value("join.fired");
println!("diamond: {n} packets × {PACKET_BYTES} B = {pushed_mib} MiB pushed");
println!("diamond: join 触发 {fired} 次(每次消费两路各一包)");
println!("diamond: peak total_queued = {peak_queued} 槽(水位 64)");
for port in 0..2 {
let stats = graph.input_queue_stats(2, port).unwrap();
println!(
"diamond: join.{} peak={}/{}B blocks={} total={}us",
stats.port_name,
stats.peak_queued_packets,
stats.peak_queued_bytes,
stats.block_events,
stats.total_blocked_us,
);
assert!(stats.peak_queued_packets <= 2);
assert!(stats.peak_queued_bytes <= (2 * PACKET_BYTES) as u64);
}
let watermark = input.backpressure_stats();
println!(
"diamond: watermark blocks={} total={}us",
watermark.block_events, watermark.total_blocked_us
);
assert!(
watermark.block_events > 0,
"长跑慢分支 diamond 应触发图输入水位背压"
);
assert_eq!(
fired, n as i64,
"sync 汇合应对齐处理全部 {n} 个时间戳,实际 {fired}"
);
let Some(base) = baseline else {
println!("diamond: 非 Linux,跳过 RSS 断言(活性与无丢失断言仍有效)");
return;
};
let peak_rss = curve.iter().copied().max().unwrap_or(base);
let growth_kb = peak_rss.saturating_sub(base);
println!("diamond: RSS 基线 {base} KiB → 峰值 {peak_rss} KiB(增长 {growth_kb} KiB)");
println!("diamond: RSS 曲线(KiB){curve:?}");
let allowed_kb = DIAMOND_MAX_QUEUED_PACKETS as u64 * (PACKET_BYTES as u64 / 1024) + 32 * 1024;
assert!(
growth_kb <= allowed_kb,
"diamond 下 RSS 增长 {growth_kb} KiB 超过允许的 {allowed_kb} KiB。\
本次推入 {pushed_mib} MiB —— 若增长与吞吐同量级,说明扇出/汇合路径上有包没被释放"
);
}