use std::alloc::{GlobalAlloc, Layout, System};
use std::sync::atomic::{AtomicU64, Ordering};
use filt_rs::{Filter, FilterValue, Filterable};
struct CountingAllocator;
static ALLOCATIONS: AtomicU64 = AtomicU64::new(0);
unsafe impl GlobalAlloc for CountingAllocator {
unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
ALLOCATIONS.fetch_add(1, Ordering::SeqCst);
unsafe { System.alloc(layout) }
}
unsafe fn alloc_zeroed(&self, layout: Layout) -> *mut u8 {
ALLOCATIONS.fetch_add(1, Ordering::SeqCst);
unsafe { System.alloc_zeroed(layout) }
}
unsafe fn realloc(&self, ptr: *mut u8, layout: Layout, new_size: usize) -> *mut u8 {
ALLOCATIONS.fetch_add(1, Ordering::SeqCst);
unsafe { System.realloc(ptr, layout, new_size) }
}
unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) {
unsafe { System.dealloc(ptr, layout) }
}
}
#[global_allocator]
static ALLOCATOR: CountingAllocator = CountingAllocator;
fn count_allocations(f: impl FnOnce()) -> u64 {
let before = ALLOCATIONS.load(Ordering::SeqCst);
f();
ALLOCATIONS.load(Ordering::SeqCst) - before
}
struct Branch(&'static str);
impl Filterable for Branch {
fn get(&self, key: &str) -> FilterValue<'_> {
match key {
"branch.name" => self.0.into(),
"branch.protected" => true.into(),
_ => FilterValue::Null,
}
}
}
const ITERATIONS: u64 = 1_000;
fn allocations_for(filter: &Filter, target: &Branch, expected: bool) -> u64 {
count_allocations(|| {
for _ in 0..ITERATIONS {
assert_eq!(
filter.matches(target).expect("the filter should evaluate"),
expected
);
}
})
}
fn like_adds_no_allocations_over_property_resolution() {
let baseline = Filter::new("branch.name").expect("parse the baseline filter");
let hit = Filter::new(r#"branch.name like "feat/*""#).expect("parse the like filter");
let miss = Filter::new(r#"branch.name like "*a*ab""#).expect("parse the like filter");
let target = Branch("feat/login");
assert!(baseline.matches(&target).expect("evaluate the baseline"));
assert!(hit.matches(&target).expect("evaluate the hit filter"));
assert!(!miss.matches(&target).expect("evaluate the miss filter"));
let baseline_allocs = allocations_for(&baseline, &target, true);
let hit_allocs = allocations_for(&hit, &target, true);
let miss_allocs = allocations_for(&miss, &target, false);
assert_eq!(
hit_allocs, baseline_allocs,
"a matching 'like' evaluation should allocate exactly as much as resolving the property"
);
assert_eq!(
miss_allocs, baseline_allocs,
"a backtracking 'like' miss should allocate exactly as much as resolving the property"
);
}
fn trim_adds_no_allocations_over_property_resolution() {
let baseline = Filter::new("branch.name").expect("parse the baseline filter");
let trimmed =
Filter::new(r#"trim(branch.name) == "feat/login""#).expect("parse the trim filter");
let target = Branch(" feat/login ");
assert!(baseline.matches(&target).expect("evaluate the baseline"));
assert!(trimmed.matches(&target).expect("evaluate the trim filter"));
let baseline_allocs = allocations_for(&baseline, &target, true);
let trimmed_allocs = allocations_for(&trimmed, &target, true);
assert_eq!(
trimmed_allocs, baseline_allocs,
"trimming a borrowed string should allocate exactly as much as resolving the property"
);
}
fn like_against_non_string_properties_is_absolutely_allocation_free() {
let filter = Filter::new(r#"branch.protected like "*""#).expect("parse the filter");
let target = Branch("feat/login");
assert!(!filter.matches(&target).expect("evaluate the filter"));
let allocs = allocations_for(&filter, &target, false);
assert_eq!(allocs, 0, "expected zero allocations, found {allocs}");
}
#[cfg(feature = "regex")]
fn matches_adds_no_allocations_over_property_resolution_after_warmup() {
let baseline = Filter::new("branch.name").expect("parse the baseline filter");
let hit = Filter::new(r#"branch.name matches r"^release/v\d+(\.\d+){2}$""#)
.expect("parse the matches filter");
let miss =
Filter::new(r#"branch.name matches r"^feat/.+$""#).expect("parse the matches filter");
let target = Branch("release/v1.2.3");
for _ in 0..16 {
assert!(baseline.matches(&target).expect("evaluate the baseline"));
assert!(hit.matches(&target).expect("evaluate the hit filter"));
assert!(!miss.matches(&target).expect("evaluate the miss filter"));
}
let baseline_allocs = allocations_for(&baseline, &target, true);
let hit_allocs = allocations_for(&hit, &target, true);
let miss_allocs = allocations_for(&miss, &target, false);
assert_eq!(
hit_allocs, baseline_allocs,
"a matching 'matches' evaluation should allocate exactly as much as resolving the property"
);
assert_eq!(
miss_allocs, baseline_allocs,
"a missing 'matches' evaluation should allocate exactly as much as resolving the property"
);
}
fn main() {
like_adds_no_allocations_over_property_resolution();
println!("ok - like adds no allocations over property resolution");
trim_adds_no_allocations_over_property_resolution();
println!("ok - trim adds no allocations over property resolution");
like_against_non_string_properties_is_absolutely_allocation_free();
println!("ok - like against non-string properties is absolutely allocation-free");
#[cfg(feature = "regex")]
{
matches_adds_no_allocations_over_property_resolution_after_warmup();
println!("ok - matches adds no allocations over property resolution (after warmup)");
}
}