mod common;
use std::io::{self, Read};
use ytsaurus_job::{Event, JobReader};
const TOTAL_BYTES: u64 = 2 * 1024 * 1024 * 1024;
const RSS_LIMIT_BYTES: u64 = 256 * 1024 * 1024;
const RSS_GROWTH_LIMIT_BYTES: u64 = 32 * 1024 * 1024;
struct SyntheticInput {
produced: u64,
target: u64,
record: Vec<u8>,
offset: usize,
counter: u64,
}
impl SyntheticInput {
fn new(target: u64) -> Self {
Self {
produced: 0,
target,
record: Vec::new(),
offset: 0,
counter: 0,
}
}
fn refill(&mut self) {
use common::{bin_i64, bin_string};
self.record.clear();
self.record.push(b'{');
bin_string(b"key", &mut self.record);
self.record.push(b'=');
bin_string(
format!("user_{:012}", self.counter).as_bytes(),
&mut self.record,
);
self.record.push(b';');
bin_string(b"value", &mut self.record);
self.record.push(b'=');
bin_i64(self.counter as i64, &mut self.record);
self.record.push(b';');
bin_string(b"payload", &mut self.record);
self.record.push(b'=');
bin_string(&[b'x'; 200], &mut self.record);
self.record.push(b'}');
self.record.push(b';');
self.offset = 0;
self.counter += 1;
}
}
impl Read for SyntheticInput {
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
if self.produced >= self.target {
return Ok(0);
}
if self.offset >= self.record.len() {
self.refill();
}
let n = (self.record.len() - self.offset).min(buf.len());
buf[..n].copy_from_slice(&self.record[self.offset..self.offset + n]);
self.offset += n;
self.produced += n as u64;
Ok(n)
}
}
#[cfg(unix)]
fn peak_rss_bytes() -> Option<u64> {
#[repr(C)]
#[derive(Default)]
struct Timeval {
tv_sec: i64,
tv_usec: i64,
}
#[repr(C)]
#[derive(Default)]
struct Rusage {
ru_utime: Timeval,
ru_stime: Timeval,
ru_maxrss: i64,
ru_ixrss: i64,
ru_idrss: i64,
ru_isrss: i64,
ru_minflt: i64,
ru_majflt: i64,
ru_nswap: i64,
ru_inblock: i64,
ru_oublock: i64,
ru_msgsnd: i64,
ru_msgrcv: i64,
ru_nsignals: i64,
ru_nvcsw: i64,
ru_nivcsw: i64,
}
unsafe extern "C" {
fn getrusage(who: i32, usage: *mut Rusage) -> i32;
}
let mut usage = Rusage::default();
let rc = unsafe {
getrusage(0 , &raw mut usage)
};
if rc != 0 {
return None;
}
let raw = u64::try_from(usage.ru_maxrss).ok()?;
Some(if cfg!(target_os = "linux") {
raw * 1024
} else {
raw
})
}
#[cfg(not(unix))]
fn peak_rss_bytes() -> Option<u64> {
None
}
#[test]
#[ignore = "moves 2 GB; run explicitly"]
fn two_gigabytes_of_input_stay_within_the_memory_budget() {
let before = peak_rss_bytes();
let mut reader = JobReader::binary(SyntheticInput::new(TOTAL_BYTES));
let mut rows: u64 = 0;
let mut bytes: u64 = 0;
while let Some(event) = reader.next_event().expect("stream reads cleanly") {
if let Event::Row(row) = event {
rows += 1;
bytes += row.raw().len() as u64;
}
}
let after = peak_rss_bytes();
assert!(rows > 0, "no rows were read");
assert!(
bytes > TOTAL_BYTES / 2,
"expected to see most of the input, saw {bytes} bytes across {rows} rows"
);
println!("read {rows} rows / {bytes} bytes");
match (before, after) {
(Some(before), Some(after)) => {
let growth = after.saturating_sub(before);
println!(
"peak RSS: {:.1} MiB -> {:.1} MiB (grew {:.1} MiB)",
before as f64 / 1048576.0,
after as f64 / 1048576.0,
growth as f64 / 1048576.0,
);
assert!(
after < RSS_LIMIT_BYTES,
"peak RSS {after} bytes exceeded the {RSS_LIMIT_BYTES} byte budget; \
the reader is accumulating input instead of streaming it"
);
assert!(
growth < RSS_GROWTH_LIMIT_BYTES,
"peak RSS grew by {growth} bytes while streaming {TOTAL_BYTES} bytes; \
the reader should hold a bounded buffer regardless of input size"
);
}
_ => println!("peak RSS unavailable on this platform; skipped the memory assertion"),
}
}
#[test]
fn a_hundred_megabytes_stay_within_the_memory_budget() {
const HUNDRED_MB: u64 = 100 * 1024 * 1024;
let mut reader = JobReader::binary(SyntheticInput::new(HUNDRED_MB));
let mut rows = 0u64;
while let Some(event) = reader.next_event().expect("stream reads cleanly") {
if matches!(event, Event::Row(_)) {
rows += 1;
}
}
assert!(rows > 100_000, "expected many rows, got {rows}");
if let Some(peak) = peak_rss_bytes() {
assert!(
peak < RSS_LIMIT_BYTES,
"peak RSS {peak} bytes exceeded the budget after only 100 MB of input"
);
}
}