use std::hint::black_box;
use std::io::Write;
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
use criterion::{Criterion, criterion_group, criterion_main};
use libmaxminddb_rs::{MetadataBuilder, MmdbDecode, Reader, Writer};
use tempfile::NamedTempFile;
#[allow(dead_code)]
#[derive(Debug, MmdbDecode)]
struct BenchRecord<'a> {
id: u32,
name: &'a str,
}
#[allow(dead_code)]
#[derive(Debug, MmdbDecode)]
struct CityNames<'a> {
en: &'a str,
fr: Option<&'a str>,
}
#[allow(dead_code)]
#[derive(Debug, MmdbDecode)]
struct CityCountry<'a> {
geoname_id: u32,
iso_code: &'a str,
is_in_european_union: bool,
names: CityNames<'a>,
}
#[allow(dead_code)]
#[derive(Debug, MmdbDecode)]
struct CityLocation {
latitude: f64,
longitude: f64,
accuracy_radius: u16,
}
#[allow(dead_code)]
#[derive(Debug, MmdbDecode)]
struct CityRecord<'a> {
country: CityCountry<'a>,
location: CityLocation,
}
fn fixture() -> Vec<u8> {
let metadata = MetadataBuilder::new()
.database_type("bench")
.ip_version(6)
.build()
.unwrap();
let mut writer = Writer::with_metadata(metadata);
for i in 0_u32..4096 {
let a = (i >> 8) as u8;
let b = i as u8;
let network = format!("10.{a}.{b}.0/24").parse().unwrap();
writer
.insert(
network,
&serde_json::json!({"id": i, "name": "benchmark", "tags": ["a", "b"]}),
)
.unwrap();
}
for i in 0_u32..1024 {
writer
.insert(
format!("2001:db8:0:{i:x}::/64").parse().unwrap(),
&serde_json::json!({"id": 10000 + i, "name": "ipv6"}),
)
.unwrap();
}
writer.finish().unwrap()
}
fn fixture_file(bytes: &[u8]) -> NamedTempFile {
let mut file = NamedTempFile::new().unwrap();
file.write_all(bytes).unwrap();
file.flush().unwrap();
file
}
fn city_record_json() -> serde_json::Value {
serde_json::json!({
"country": {
"geoname_id": 2635167,
"is_in_european_union": true,
"iso_code": "GB",
"names": {"en": "United Kingdom", "fr": "Royaume-Uni"}
},
"location": {
"accuracy_radius": 50,
"latitude": 51.5074,
"longitude": -0.1278
}
})
}
fn city_fixture() -> Vec<u8> {
let metadata = MetadataBuilder::new()
.database_type("city-bench")
.ip_version(4)
.build()
.unwrap();
let mut writer = Writer::with_metadata(metadata);
writer
.insert("10.11.0.0/24".parse().unwrap(), &city_record_json())
.unwrap();
writer.finish().unwrap()
}
fn lookup_ips_v4() -> Vec<IpAddr> {
(0_u32..4096)
.map(|i| {
let shuffled = i.wrapping_mul(2654435761) & 4095;
let a = (shuffled >> 8) as u8;
let b = shuffled as u8;
IpAddr::V4(Ipv4Addr::new(10, a, b, 42))
})
.collect()
}
fn lookup_ips_v6() -> Vec<IpAddr> {
(0_u32..1024)
.map(|i| {
let shuffled = (i.wrapping_mul(2654435761) & 1023) as u16;
IpAddr::V6(Ipv6Addr::new(0x2001, 0x0db8, 0, shuffled, 0, 0, 0, 1))
})
.collect()
}
fn benches(c: &mut Criterion) {
let bytes = fixture();
let file = fixture_file(&bytes);
let reader = Reader::from_bytes(&bytes).unwrap();
let ipv4_hit: IpAddr = "10.7.3.42".parse().unwrap();
let ipv6_hit: IpAddr = "2001:db8:0:1a::1".parse().unwrap();
let ipv4_miss: IpAddr = "192.0.2.1".parse().unwrap();
let ipv6_miss: IpAddr = "3001::1".parse().unwrap();
let ips_v4 = lookup_ips_v4();
let ips_v6 = lookup_ips_v6();
let batch_ips: Vec<IpAddr> = ips_v4.iter().copied().take(256).collect();
c.bench_function("reader/lookup_borrowed_ipv4_hot", |b| {
b.iter(|| {
black_box(
reader
.lookup_borrowed::<BenchRecord<'_>>(black_box(ipv4_hit))
.unwrap(),
)
})
});
c.bench_function("reader/lookup_borrowed_ipv6_hot", |b| {
b.iter(|| {
black_box(
reader
.lookup_borrowed::<BenchRecord<'_>>(black_box(ipv6_hit))
.unwrap(),
)
})
});
c.bench_function("reader/lookup_borrowed_ipv4_miss", |b| {
b.iter(|| {
black_box(
reader
.lookup_borrowed::<BenchRecord<'_>>(black_box(ipv4_miss))
.is_err(),
)
})
});
c.bench_function("reader/lookup_borrowed_ipv6_miss", |b| {
b.iter(|| {
black_box(
reader
.lookup_borrowed::<BenchRecord<'_>>(black_box(ipv6_miss))
.is_err(),
)
})
});
c.bench_function("reader/lookup_borrowed_map_ipv6_miss", |b| {
b.iter(|| {
black_box(
reader
.lookup_borrowed_map(black_box(ipv6_miss), |record: BenchRecord<'_>| {
black_box(record.id)
})
.unwrap(),
)
})
});
c.bench_function("reader/lookup_borrowed_map_ipv6_hit", |b| {
b.iter(|| {
black_box(
reader
.lookup_borrowed_map(black_box(ipv6_hit), |record: BenchRecord<'_>| {
black_box(record.id)
})
.unwrap(),
)
})
});
c.bench_function("reader/lookup_borrowed_ipv4_random", |b| {
let mut index = 0_usize;
let mask = ips_v4.len() - 1;
b.iter(|| {
let ip = ips_v4[index & mask];
index = index.wrapping_add(1);
black_box(
reader
.lookup_borrowed::<BenchRecord<'_>>(black_box(ip))
.unwrap(),
)
})
});
c.bench_function("reader/lookup_borrowed_ipv6_random", |b| {
let mut index = 0_usize;
let mask = ips_v6.len() - 1;
b.iter(|| {
let ip = ips_v6[index & mask];
index = index.wrapping_add(1);
black_box(
reader
.lookup_borrowed::<BenchRecord<'_>>(black_box(ip))
.unwrap(),
)
})
});
c.bench_function("reader/lookup_exists_ipv4", |b| {
b.iter(|| black_box(reader.lookup_exists(black_box(ipv4_hit))))
});
c.bench_function("reader/decode_borrowed_struct", |b| {
b.iter(|| {
let value: BenchRecord<'_> = reader.lookup_borrowed(black_box(ipv4_hit)).unwrap();
black_box((value.id, value.name))
})
});
let city_bytes = city_fixture();
let city_reader = Reader::from_bytes(&city_bytes).unwrap();
let city_ip: IpAddr = "10.11.0.42".parse().unwrap();
c.bench_function("reader/city_lookup_value", |b| {
b.iter(|| black_box(city_reader.lookup_value(black_box(city_ip)).unwrap()))
});
c.bench_function("reader/city_lookup_borrowed", |b| {
b.iter(|| {
let record: CityRecord<'_> = city_reader.lookup_borrowed(black_box(city_ip)).unwrap();
black_box(record)
})
});
c.bench_function("reader/lookup_many_256", |b| {
b.iter(|| black_box(reader.lookup_many(black_box(&batch_ips))))
});
c.bench_function("reader/open_from_bytes", |b| {
b.iter(|| black_box(Reader::from_bytes(black_box(&bytes)).unwrap()))
});
c.bench_function("reader/open_owned_file", |b| {
b.iter(|| black_box(Reader::open(black_box(file.path())).unwrap()))
});
c.bench_function("reader/open_mmap", |b| {
b.iter(|| {
black_box(unsafe { Reader::open_mmap(black_box(file.path())).unwrap() })
})
});
}
fn configured_criterion() -> Criterion {
Criterion::default()
.warm_up_time(std::time::Duration::from_millis(100))
.measurement_time(std::time::Duration::from_millis(250))
.sample_size(15)
}
criterion_group! {
name = reader_benches;
config = configured_criterion();
targets = benches
}
criterion_main!(reader_benches);