use std::fs::File;
use std::io::Write as _;
use std::net::{TcpListener, TcpStream};
use std::path::Path;
use std::sync::atomic::{AtomicUsize, Ordering};
use ffsync::chunking::PipelineConfig;
use ffsync::protocol::sync::{Progress, ReceiveRequest, SendRequest, receive, send};
static LIVE_BYTES: AtomicUsize = AtomicUsize::new(0);
struct CountingAllocator;
unsafe impl std::alloc::GlobalAlloc for CountingAllocator {
unsafe fn alloc(&self, layout: std::alloc::Layout) -> *mut u8 {
unsafe {
let ptr = std::alloc::System.alloc(layout);
if !ptr.is_null() {
LIVE_BYTES.fetch_add(layout.size(), Ordering::Relaxed);
}
ptr
}
}
unsafe fn dealloc(&self, ptr: *mut u8, layout: std::alloc::Layout) {
unsafe {
std::alloc::System.dealloc(ptr, layout);
LIVE_BYTES.fetch_sub(layout.size(), Ordering::Relaxed);
}
}
unsafe fn realloc(&self, ptr: *mut u8, layout: std::alloc::Layout, new_size: usize) -> *mut u8 {
unsafe {
let new_ptr = std::alloc::System.realloc(ptr, layout, new_size);
if !new_ptr.is_null() {
if new_ptr == ptr {
if new_size > layout.size() {
LIVE_BYTES.fetch_add(new_size - layout.size(), Ordering::Relaxed);
} else {
LIVE_BYTES.fetch_sub(layout.size() - new_size, Ordering::Relaxed);
}
} else {
LIVE_BYTES.fetch_sub(layout.size(), Ordering::Relaxed);
LIVE_BYTES.fetch_add(new_size, Ordering::Relaxed);
}
}
new_ptr
}
}
}
#[global_allocator]
static ALLOCATOR: CountingAllocator = CountingAllocator;
fn create_file(path: &Path, data: &[u8]) {
let mut f = File::create(path).unwrap();
f.write_all(data).unwrap();
}
fn read_file(path: &Path) -> Vec<u8> {
std::fs::read(path).unwrap()
}
fn run_session(send_req: SendRequest, recv_req: ReceiveRequest) {
let listener = TcpListener::bind(("127.0.0.1", 0)).unwrap();
let port = listener.local_addr().unwrap().port();
let recv_handle = std::thread::spawn(move || {
let (mut stream, _) = listener.accept().unwrap();
let mut progress = |_p: Progress| {};
receive(
&mut stream,
&recv_req,
&PipelineConfig::default(),
&mut progress,
)
});
let send_handle = std::thread::spawn(move || {
let mut stream = TcpStream::connect(("127.0.0.1", port)).unwrap();
let mut progress = |_p: Progress| {};
send(
&mut stream,
&send_req,
&PipelineConfig::default(),
&mut progress,
)
});
let s = send_handle.join().unwrap();
let r = recv_handle.join().unwrap();
s.expect("sender ok");
r.expect("receiver ok");
}
#[test]
#[ignore = "soak: run explicitly with `cargo test --test soak -- --ignored`"]
fn hundred_sessions_do_not_accumulate_memory() {
let dir = std::env::temp_dir().join("ffsync_soak");
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).unwrap();
let mut src_data = vec![0u8; 2 * 1024 * 1024];
for (i, b) in src_data.iter_mut().enumerate() {
*b = (i as u8).wrapping_mul(17).wrapping_add((i / 65536) as u8);
}
let source = dir.join("source.bin");
create_file(&source, &src_data);
let target = dir.join("target.bin");
create_file(&target, &src_data[..1024 * 1024]);
let start_live = LIVE_BYTES.load(Ordering::Relaxed);
let sessions = 100;
for i in 0..sessions {
let tail_len = i % 257;
let mut data = src_data.clone();
data.extend(std::iter::repeat_n(0xA5u8, tail_len));
let flip_at = (i * 977) % (data.len() - 4096);
for b in &mut data[flip_at..flip_at + 4096] {
*b ^= 0x5A;
}
create_file(&source, &data);
let send_req = SendRequest {
path: source.clone(),
display_path: "source.bin".into(),
mtime: None,
force_checksum: false,
tree: false,
dry_run: false,
};
let recv_req = ReceiveRequest {
path: target.clone(),
local_source: target.clone(),
force_checksum: false,
tree: false,
apply_mode: None,
apply_mtime: None,
};
run_session(send_req, recv_req);
assert_eq!(
read_file(&target),
data,
"session {i}: target did not converge"
);
}
let end_live = LIVE_BYTES.load(Ordering::Relaxed);
let grown = end_live as i64 - start_live as i64;
println!(
"live heap: start {start_live} B, end {end_live} B, delta {grown} B over {sessions} sessions"
);
assert!(
grown <= 1024 * 1024,
"net live heap grew {grown} B across {sessions} sessions — accumulating state"
);
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
#[ignore = "soak: run explicitly with `cargo test --test soak -- --ignored`"]
fn hundred_sessions_alternating_deltas_and_full_transfers() {
let dir = std::env::temp_dir().join("ffsync_soak_alt");
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).unwrap();
let mut src_data = vec![0u8; 512 * 1024];
for (i, b) in src_data.iter_mut().enumerate() {
*b = (i as u8).wrapping_mul(31).wrapping_add((i / 4096) as u8);
}
let source = dir.join("source.bin");
let target = dir.join("target.bin");
let sessions = 100;
for i in 0..sessions {
let full = i % 2 == 0;
let mut data = if full {
let mut fresh = vec![0u8; 512 * 1024];
for (j, b) in fresh.iter_mut().enumerate() {
*b = (j as u8)
.wrapping_mul(7)
.wrapping_add((j / 8192) as u8)
.wrapping_add(i as u8);
}
fresh
} else {
let mut d = src_data.clone();
let at = (i * 4099) % (d.len() - 8192);
for b in &mut d[at..at + 8192] {
*b ^= 0xFF;
}
d
};
data.extend(std::iter::repeat_n(i as u8, i % 33));
create_file(&source, &data);
if i == 0 {
create_file(&target, &src_data[..256 * 1024]);
}
let send_req = SendRequest {
path: source.clone(),
display_path: "source.bin".into(),
mtime: None,
force_checksum: false,
tree: false,
dry_run: false,
};
let recv_req = ReceiveRequest {
path: target.clone(),
local_source: target.clone(),
force_checksum: false,
tree: false,
apply_mode: None,
apply_mtime: None,
};
run_session(send_req, recv_req);
assert_eq!(read_file(&target), data, "session {i}: no convergence");
}
let _ = std::fs::remove_dir_all(&dir);
}