use std::cmp::max;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct SliceMap {
pub fetch_index: usize,
pub offset_in_fetch: usize,
pub len: usize,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct MergedFetch {
pub offset: u64,
pub len: u64,
}
#[derive(Debug, Clone)]
pub struct CoalescePlan {
pub fetches: Vec<MergedFetch>,
pub slices: Vec<SliceMap>,
pub total_input_bytes: u64,
pub total_fetch_bytes: u64,
}
impl CoalescePlan {
pub fn wasted_bytes(&self) -> u64 {
self.total_fetch_bytes
.saturating_sub(self.total_input_bytes)
}
}
pub const NO_FETCH: usize = usize::MAX;
pub fn plan(ranges: &[(u64, u64)], gap: u64, max_bytes: u64) -> CoalescePlan {
let n = ranges.len();
let max_bytes = max_bytes.max(1);
let mut slices: Vec<SliceMap> = vec![
SliceMap {
fetch_index: NO_FETCH,
offset_in_fetch: 0,
len: 0,
};
n
];
let total_input_bytes: u64 = ranges.iter().map(|(_, l)| *l).sum();
let mut sorted: Vec<(usize, u64, u64)> = ranges
.iter()
.enumerate()
.filter(|(_, (_, l))| *l > 0)
.map(|(i, &(o, l))| (i, o, l))
.collect();
if sorted.is_empty() {
for (i, &(_, l)) in ranges.iter().enumerate() {
slices[i].len = l as usize;
}
return CoalescePlan {
fetches: Vec::new(),
slices,
total_input_bytes,
total_fetch_bytes: 0,
};
}
sorted.sort_by_key(|(_, off, _)| *off);
let mut fetches: Vec<MergedFetch> = Vec::with_capacity(sorted.len());
let (first_orig_idx, first_off, first_len) = sorted[0];
fetches.push(MergedFetch {
offset: first_off,
len: first_len,
});
slices[first_orig_idx] = SliceMap {
fetch_index: 0,
offset_in_fetch: 0,
len: first_len as usize,
};
for &(orig_idx, off, len) in &sorted[1..] {
let last_idx = fetches.len() - 1;
let cur_offset = fetches[last_idx].offset;
let cur_len = fetches[last_idx].len;
let cur_end = cur_offset + cur_len;
let candidate_end = max(cur_end, off + len);
let candidate_len = candidate_end - cur_offset;
let within_gap = off <= cur_end.saturating_add(gap);
let effective_cap = max(max_bytes, cur_len);
let within_cap = candidate_len <= effective_cap;
if within_gap && within_cap {
fetches[last_idx].len = candidate_len;
slices[orig_idx] = SliceMap {
fetch_index: last_idx,
offset_in_fetch: (off - cur_offset) as usize,
len: len as usize,
};
} else {
fetches.push(MergedFetch { offset: off, len });
let fetch_idx = fetches.len() - 1;
slices[orig_idx] = SliceMap {
fetch_index: fetch_idx,
offset_in_fetch: 0,
len: len as usize,
};
}
}
for (i, &(_, l)) in ranges.iter().enumerate() {
if l == 0 {
slices[i].len = 0;
}
}
let total_fetch_bytes: u64 = fetches.iter().map(|f| f.len).sum();
CoalescePlan {
fetches,
slices,
total_input_bytes,
total_fetch_bytes,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn empty_input_produces_empty_plan() {
let p = plan(&[], 64 * 1024, 4 * 1024 * 1024);
assert!(p.fetches.is_empty());
assert!(p.slices.is_empty());
assert_eq!(p.total_input_bytes, 0);
assert_eq!(p.total_fetch_bytes, 0);
assert_eq!(p.wasted_bytes(), 0);
}
#[test]
fn all_zero_len_inputs_produce_no_fetches_but_preserve_slots() {
let p = plan(&[(0, 0), (100, 0), (7, 0)], 64 * 1024, 4 * 1024 * 1024);
assert!(p.fetches.is_empty());
assert_eq!(p.slices.len(), 3);
for s in &p.slices {
assert_eq!(s.fetch_index, NO_FETCH);
assert_eq!(s.len, 0);
}
}
#[test]
fn zero_len_mixed_with_real_ranges_preserves_order_and_indices() {
let inputs = &[(100, 0), (200, 10), (0, 0), (500, 20)];
let p = plan(inputs, 0, 1024);
assert_eq!(p.slices.len(), 4);
assert_eq!(p.slices[0].fetch_index, NO_FETCH);
assert_eq!(p.slices[0].len, 0);
assert_eq!(p.slices[2].fetch_index, NO_FETCH);
assert_eq!(p.slices[2].len, 0);
assert_eq!(p.fetches.len(), 2);
assert_eq!(p.slices[1].len, 10);
assert_eq!(p.slices[3].len, 20);
}
#[test]
fn adjacent_ranges_merge_when_gap_permits() {
let p = plan(&[(0, 10), (10, 10)], 0, 1024);
assert_eq!(p.fetches.len(), 1);
assert_eq!(p.fetches[0], MergedFetch { offset: 0, len: 20 });
assert_eq!(p.slices[0].offset_in_fetch, 0);
assert_eq!(p.slices[1].offset_in_fetch, 10);
assert_eq!(p.wasted_bytes(), 0);
}
#[test]
fn ranges_within_gap_merge() {
let p = plan(&[(0, 10), (15, 10)], 8, 1024);
assert_eq!(p.fetches.len(), 1);
assert_eq!(p.fetches[0], MergedFetch { offset: 0, len: 25 });
assert_eq!(p.slices[1].offset_in_fetch, 15);
assert_eq!(p.wasted_bytes(), 5);
}
#[test]
fn ranges_beyond_gap_do_not_merge() {
let p = plan(&[(0, 10), (20, 10)], 4, 1024);
assert_eq!(p.fetches.len(), 2);
assert_eq!(p.wasted_bytes(), 0);
}
#[test]
fn overlapping_ranges_merge_and_map_correctly() {
let p = plan(&[(0, 20), (10, 15)], 0, 1024);
assert_eq!(p.fetches.len(), 1);
assert_eq!(p.fetches[0], MergedFetch { offset: 0, len: 25 });
assert_eq!(p.slices[0].offset_in_fetch, 0);
assert_eq!(p.slices[0].len, 20);
assert_eq!(p.slices[1].offset_in_fetch, 10);
assert_eq!(p.slices[1].len, 15);
}
#[test]
fn max_bytes_cap_forces_split_even_when_gap_permits() {
let p = plan(&[(0, 100), (100, 100)], 1024, 100);
assert_eq!(p.fetches.len(), 2);
assert_eq!(p.fetches[0].len, 100);
assert_eq!(
p.fetches[1],
MergedFetch {
offset: 100,
len: 100
}
);
}
#[test]
fn max_bytes_zero_is_clamped_to_one() {
let p = plan(&[(0, 5), (10, 5)], 1024, 0);
assert_eq!(p.fetches.len(), 2);
}
#[test]
fn cap_larger_than_gap_still_respects_gap() {
let p = plan(&[(0, 10), (1000, 10)], 100, 1_000_000);
assert_eq!(p.fetches.len(), 2);
}
#[test]
fn caller_order_is_preserved_across_reorders() {
let inputs = &[(200, 10), (0, 20), (100, 5)];
let p = plan(inputs, 0, 4096);
assert_eq!(p.fetches.len(), 3);
for (i, &(_, l)) in inputs.iter().enumerate() {
assert_eq!(p.slices[i].len as u64, l);
}
let s0 = p.slices[0];
assert_eq!(p.fetches[s0.fetch_index].offset, 200);
assert_eq!(s0.offset_in_fetch, 0);
let s1 = p.slices[1];
assert_eq!(p.fetches[s1.fetch_index].offset, 0);
assert_eq!(s1.offset_in_fetch, 0);
}
#[test]
fn slice_mapping_is_byte_equivalent_on_random_inputs() {
use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};
fn simulated_byte(off: u64) -> u8 {
let mut h = DefaultHasher::new();
off.hash(&mut h);
(h.finish() & 0xff) as u8
}
fn read_range(off: u64, len: u64) -> Vec<u8> {
(0..len).map(|i| simulated_byte(off + i)).collect()
}
let cases: &[Vec<(u64, u64)>] = &[
vec![(0, 0)],
vec![(0, 10), (10, 10), (30, 5)],
vec![(100, 50), (0, 20), (200, 10), (155, 5)],
vec![(0, 1), (2, 1), (4, 1), (6, 1), (8, 1)],
vec![(0, 1000), (500, 10)], vec![(0, 100), (100, 100), (200, 100), (300, 100)], ];
for gap in [0u64, 1, 4, 64, 4096] {
for max_bytes in [1u64, 8, 128, 4096, u64::MAX] {
for input in cases {
let p = plan(input, gap, max_bytes);
let fetch_bufs: Vec<Vec<u8>> = p
.fetches
.iter()
.map(|f| read_range(f.offset, f.len))
.collect();
for (i, &(off, len)) in input.iter().enumerate() {
let expected = read_range(off, len);
let s = p.slices[i];
let got: Vec<u8> = if s.fetch_index == NO_FETCH {
assert_eq!(len, 0, "NO_FETCH must only be used for empty ranges");
Vec::new()
} else {
let buf = &fetch_bufs[s.fetch_index];
buf[s.offset_in_fetch..s.offset_in_fetch + s.len].to_vec()
};
assert_eq!(
got, expected,
"byte mismatch at input #{i} = ({off},{len}), gap={gap}, cap={max_bytes}"
);
}
let cap = max_bytes.max(1);
let mut per_fetch_max_input: Vec<u64> = vec![0; p.fetches.len()];
for (i, &(_, l)) in input.iter().enumerate() {
let s = p.slices[i];
if s.fetch_index != NO_FETCH {
per_fetch_max_input[s.fetch_index] =
per_fetch_max_input[s.fetch_index].max(l);
}
}
for (fi, f) in p.fetches.iter().enumerate() {
let allowed = cap.max(per_fetch_max_input[fi]);
assert!(
f.len <= allowed,
"fetch {:?} exceeds cap-or-largest-input {}",
f,
allowed
);
}
}
}
}
}
}