Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .jules/bolt.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
## 2026-07-20 - O(N) single-pass bucket-sort fragment reassembly optimization
**Learning:** Fragmented Pkarr packet records can cause O(N^2) CPU overhead if we probe each index sequentially with a full linear scan over all resource records in the packet. We can optimize this by doing a single pass over `all_resource_records()` and bucket-sorting the extracted indexes.
**Action:** Use single-pass loops over resource records with index-based array lookups to avoid sequential DNS record probes.
60 changes: 47 additions & 13 deletions crates/openhost-pkarr/src/offer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1097,18 +1097,51 @@ pub fn decode_answer_fragments_from_packet(
"{ANSWER_TXT_PREFIX}{}",
zbase32::encode_full_bytes(&client_hash)
);
let prefix = format!("{}-", base);
let prefix_bytes = prefix.as_bytes();

// TODO(perf): replace the per-fragment `collect_single_txt` probes
// with a single pass over `packet.all_resource_records()` that
// bucket-sorts matching names by their numeric `-<idx>` suffix.
// Today this walks the packet's RR list `chunk_total` times — fine
// for the 1–3 fragments we see in practice, O(N²) in the
// pathological MAX_FRAGMENT_TOTAL=255 case. Not a hotpath (one
// reassembly per dial attempt) so the refactor is deferred.
let mut buckets: [Option<String>; 256] = {
const NONE: Option<String> = None;
[NONE; 256]
};

// Single pass over packet's all resource records to bucket-sort them by numeric idx suffix.
for rr in packet.all_resource_records() {
let first_label = rr.name.get_labels().first();
if let Some(label) = first_label {
let label_bytes = label.as_ref();
if label_bytes.starts_with(prefix_bytes) {
let suffix = &label_bytes[prefix.len()..];
if let Ok(suffix_str) = std::str::from_utf8(suffix) {
if let Ok(idx) = suffix_str.parse::<u8>() {
if let RData::TXT(txt) = &rr.rdata {
if buckets[idx as usize].is_some() {
return Err(PkarrError::MultipleOpenhostRecords);
}
let mut out = String::new();
for (key, value) in txt.iter_raw() {
out.push_str(
core::str::from_utf8(key)
.map_err(|_| PkarrError::InvalidUtf8)?,
);
if let Some(v) = value {
out.push('=');
out.push_str(
core::str::from_utf8(v)
.map_err(|_| PkarrError::InvalidUtf8)?,
);
}
}
buckets[idx as usize] = Some(out);
}
}
}
}
}
}

// Probe idx = 0 first. Missing zero-fragment ⇒ no answer for us.
let first_name = format!("{base}-0");
let Some(first_text) = collect_single_txt(packet, &first_name)? else {
let Some(first_text) = &buckets[0] else {
return Ok(None);
};
let first_bytes = URL_SAFE_NO_PAD.decode(first_text.as_bytes())?;
Expand All @@ -1123,10 +1156,11 @@ pub fn decode_answer_fragments_from_packet(
let mut fragments: Vec<DecodedFragment> = Vec::with_capacity(total as usize);
fragments.push(first);
for i in 1..total {
let name = format!("{base}-{i}");
let text = collect_single_txt(packet, &name)?.ok_or(PkarrError::MalformedCanonical(
"answer fragment set is missing an idx",
))?;
let text = buckets[i as usize]
.as_ref()
.ok_or(PkarrError::MalformedCanonical(
"answer fragment set is missing an idx",
))?;
let bytes = URL_SAFE_NO_PAD.decode(text.as_bytes())?;
let frag = decode_fragment(&bytes)?;
if frag.total != total {
Expand Down
Loading