perf: optimize DNS query hot path (#15)

* perf: optimize hot path — RwLock, inline filtering, pre-allocated strings

- Mutex → RwLock for cache, blocklist, and overrides (concurrent read access)
- Make cache.lookup() and overrides.lookup() take &self (read-only)
- Eliminate 3 Vec allocations per DnsPacket::write() via inline filtering
- Pre-allocate domain strings with capacity 64 in parse path
- Add criterion micro-benchmarks (hot_path + throughput)
- Add bench README documenting both benchmark suites

Measured improvement: ~14% faster parsing, ~9% pipeline throughput,
round-trip cached 733ns → 698ns (~2.3M queries/sec).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* chore: simplify benchmark code after review

- Remove redundant DnsHeader::new() (already set by DnsPacket::new())
- Remove unused DnsHeader import
- Change simulate_cached_pipeline to take &DnsCache (lookup is &self now)
- Remove unnecessary mut on cache in cache_lookup_miss bench

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit was merged in pull request #15.
This commit is contained in:
Razvan Dimescu
2026-03-27 02:01:08 +02:00
committed by GitHub
parent 5d454cbed5
commit 236ef7b4f5
13 changed files with 728 additions and 77 deletions

View File

@@ -46,7 +46,7 @@ impl DnsPacket {
result.header.read(buffer)?;
for _ in 0..result.header.questions {
let mut question = DnsQuestion::new("".to_string(), QueryType::UNKNOWN(0));
let mut question = DnsQuestion::new(String::with_capacity(64), QueryType::UNKNOWN(0));
question.read(buffer)?;
result.questions.push(question);
}
@@ -68,34 +68,36 @@ impl DnsPacket {
}
pub fn write(&self, buffer: &mut BytePacketBuffer) -> Result<()> {
// Filter out UNKNOWN records (e.g. EDNS OPT) that we can't re-serialize
let answers: Vec<_> = self.answers.iter().filter(|r| !r.is_unknown()).collect();
let authorities: Vec<_> = self
.authorities
.iter()
.filter(|r| !r.is_unknown())
.collect();
let resources: Vec<_> = self.resources.iter().filter(|r| !r.is_unknown()).collect();
// Count known records without allocating filter Vecs
let answer_count = self.answers.iter().filter(|r| !r.is_unknown()).count() as u16;
let auth_count = self.authorities.iter().filter(|r| !r.is_unknown()).count() as u16;
let res_count = self.resources.iter().filter(|r| !r.is_unknown()).count() as u16;
let mut header = self.header.clone();
header.questions = self.questions.len() as u16;
header.answers = answers.len() as u16;
header.authoritative_entries = authorities.len() as u16;
header.resource_entries = resources.len() as u16;
header.answers = answer_count;
header.authoritative_entries = auth_count;
header.resource_entries = res_count;
header.write(buffer)?;
for question in &self.questions {
question.write(buffer)?;
}
for rec in answers {
rec.write(buffer)?;
for rec in &self.answers {
if !rec.is_unknown() {
rec.write(buffer)?;
}
}
for rec in authorities {
rec.write(buffer)?;
for rec in &self.authorities {
if !rec.is_unknown() {
rec.write(buffer)?;
}
}
for rec in resources {
rec.write(buffer)?;
for rec in &self.resources {
if !rec.is_unknown() {
rec.write(buffer)?;
}
}
Ok(())