use std::time::Instant;
use lazily::Context;
const LADDER: [usize; 13] = [
32,
64,
96,
128,
129,
160,
256,
1024,
4096,
65_536,
1_000_000,
10_000_000,
100_000_000,
];
const DEFAULT_MAX_WIDTH: usize = 65_536;
const NOTIFICATION_BUDGET: usize = 400_000;
const DEFAULT_MEM_FLOOR_GIB: f64 = 16.0;
const CHURN_CYCLES: usize = 200_000;
const CHURN_LIVE_WIDTH: usize = 64;
struct Rng(u64);
impl Rng {
fn below(&mut self, bound: usize) -> usize {
self.0 = self
.0
.wrapping_mul(6364136223846793005)
.wrapping_add(1442695040888963407);
((self.0 >> 33) % bound as u64) as usize
}
}
fn meminfo_kib(key: &str) -> Option<u64> {
let meminfo = std::fs::read_to_string("/proc/meminfo").ok()?;
meminfo.lines().find_map(|line| {
line.strip_prefix(key)?
.trim_start_matches(':')
.split_whitespace()
.next()?
.parse()
.ok()
})
}
fn rss_kib() -> u64 {
std::fs::read_to_string("/proc/self/status")
.ok()
.and_then(|status| {
status.lines().find_map(|line| {
line.strip_prefix("VmRSS:")?
.split_whitespace()
.next()?
.parse()
.ok()
})
})
.unwrap_or(0)
}
fn env_usize(key: &str, default: usize) -> usize {
std::env::var(key)
.ok()
.and_then(|raw| raw.parse().ok())
.unwrap_or(default)
}
struct Rung {
build_ns_each: f64,
notify_ns_each: f64,
unread_publish_ns: f64,
bytes_each: f64,
}
fn sweep_width(width: usize) -> Rung {
let rss_before = rss_kib();
let ctx = Context::new();
let topic = ctx.source(0u64);
let build_start = Instant::now();
let subscriber_a: Vec<_> = (0..width)
.map(|i| {
let slot = ctx.computed(move |ctx| ctx.get(&topic) + i as u64);
ctx.get(&slot);
slot
})
.collect();
let build_ns_each = build_start.elapsed().as_nanos() as f64 / width as f64;
let rss_after_build = rss_kib();
let publishes = (NOTIFICATION_BUDGET / width).max(1);
let notify_start = Instant::now();
for publish in 1..=publishes {
ctx.set(&topic, publish as u64);
for slot in &subscriber_a {
std::hint::black_box(ctx.get(slot));
}
}
let notify_ns_each = notify_start.elapsed().as_nanos() as f64 / (publishes * width) as f64;
let unread_publishes = if width > 1_000_000 { 5 } else { 200 };
ctx.set(&topic, 0);
let unread_start = Instant::now();
for publish in 1..=unread_publishes {
ctx.set(&topic, (publish + 1_000_000) as u64);
}
let unread_publish_ns = unread_start.elapsed().as_nanos() as f64 / unread_publishes as f64;
Rung {
build_ns_each,
notify_ns_each,
unread_publish_ns,
bytes_each: (rss_after_build.saturating_sub(rss_before)) as f64 * 1024.0 / width as f64,
}
}
fn churn_soak() {
let ctx = Context::new();
let topic = ctx.source(0u64);
let mut subscriber_a: Vec<_> = (0..CHURN_LIVE_WIDTH)
.map(|i| {
let slot = ctx.computed(move |ctx| ctx.get(&topic) + i as u64);
ctx.get(&slot);
slot
})
.collect();
ctx.set(&topic, 1);
for slot in &subscriber_a {
std::hint::black_box(ctx.get(slot));
}
let rss_baseline = rss_kib();
let mut rng = Rng(0x5EED);
let start = Instant::now();
for cycle in 0..CHURN_CYCLES {
let victim = rng.below(subscriber_a.len());
ctx.dispose_slot(&subscriber_a.swap_remove(victim));
let salt = cycle as u64;
let slot = ctx.computed(move |ctx| ctx.get(&topic) + salt);
ctx.get(&slot);
subscriber_a.push(slot);
if cycle % 64 == 0 {
ctx.set(&topic, cycle as u64);
for slot in &subscriber_a {
std::hint::black_box(ctx.get(slot));
}
}
}
let elapsed = start.elapsed();
let rss_after = rss_kib();
let sentinel = 9_999_999u64;
ctx.set(&topic, sentinel);
for slot in &subscriber_a {
let observed = ctx.get(slot);
assert!(
observed >= sentinel,
"subscriber missed a publish after churn: {observed} < {sentinel}"
);
}
println!(
"\nchurn soak: {CHURN_CYCLES} cycles at width {CHURN_LIVE_WIDTH} in {:.2}s ({:.0} ns/cycle)",
elapsed.as_secs_f64(),
elapsed.as_nanos() as f64 / CHURN_CYCLES as f64,
);
println!(
"rss {:.1} -> {:.1} MiB ({:+.2} MiB, {:.2} bytes/cycle); all {} survivors saw the final publish",
rss_baseline as f64 / 1024.0,
rss_after as f64 / 1024.0,
(rss_after as f64 - rss_baseline as f64) / 1024.0,
(rss_after.saturating_sub(rss_baseline)) as f64 * 1024.0 / CHURN_CYCLES as f64,
subscriber_a.len(),
);
}
fn main() {
let max_width = env_usize("LAZILY_PUBSUB_MAX_WIDTH", DEFAULT_MAX_WIDTH);
let mem_floor_gib = std::env::var("LAZILY_PUBSUB_MEM_FLOOR_GIB")
.ok()
.and_then(|raw| raw.parse().ok())
.unwrap_or(DEFAULT_MEM_FLOOR_GIB);
let total_gib = meminfo_kib("MemTotal").unwrap_or(0) as f64 / 1024.0 / 1024.0;
println!(
"fan-out ladder to width {max_width} (LAZILY_PUBSUB_MAX_WIDTH), \
{total_gib:.0} GiB total, floor {mem_floor_gib:.0} GiB"
);
println!("EDGE_INDEX_THRESHOLD is 32; per-subscriber cost should stay flat across it\n");
println!(
"{:>12}{:>14}{:>14}{:>18}{:>12}{:>12}",
"width", "build ns/sub", "notify ns/sub", "unread pub ns", "bytes/sub", "est total"
);
sweep_width(8);
let mut bytes_each_last: Option<f64> = None;
for width in LADDER {
if width > max_width {
println!(
"\nstopped at the LAZILY_PUBSUB_MAX_WIDTH ceiling; \
rerun with LAZILY_PUBSUB_MAX_WIDTH={width} to continue"
);
break;
}
if let Some(bytes_each) = bytes_each_last {
let projected_gib = bytes_each * width as f64 / 1024.0 / 1024.0 / 1024.0;
let available_gib = meminfo_kib("MemAvailable").unwrap_or(0) as f64 / 1024.0 / 1024.0;
if projected_gib > available_gib - mem_floor_gib {
println!(
"\nstopping before width {width}: projected {projected_gib:.1} GiB \
from the last rung's {bytes_each:.0} B/subscriber, \
but only {available_gib:.1} GiB available with a {mem_floor_gib:.0} GiB floor"
);
break;
}
}
let rung = sweep_width(width);
let total_mib = rung.bytes_each * width as f64 / 1024.0 / 1024.0;
println!(
"{:>12}{:>14.1}{:>14.1}{:>18.0}{:>12.0}{:>10.0} MiB",
width,
rung.build_ns_each,
rung.notify_ns_each,
rung.unread_publish_ns,
rung.bytes_each,
total_mib
);
bytes_each_last = Some(rung.bytes_each.max(1.0));
}
churn_soak();
}