From ececc0496fae6566dbb047c4171477ccec8cde33 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Tue, 4 Aug 2026 17:19:39 +0000 Subject: [PATCH] =?UTF-8?q?=E2=9A=A1=20Bolt:=20optimize=20answer=20fragmen?= =?UTF-8?q?t=20reassembly=20to=20single-pass=20O(N)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: vamzi <9899519+vamzi@users.noreply.github.com> --- .jules/bolt.md | 3 ++ crates/openhost-pkarr/src/offer.rs | 60 +++++++++++++++++++++++------- 2 files changed, 50 insertions(+), 13 deletions(-) create mode 100644 .jules/bolt.md diff --git a/.jules/bolt.md b/.jules/bolt.md new file mode 100644 index 0000000..4b4d7bc --- /dev/null +++ b/.jules/bolt.md @@ -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. diff --git a/crates/openhost-pkarr/src/offer.rs b/crates/openhost-pkarr/src/offer.rs index 9ba0705..1ef5eea 100644 --- a/crates/openhost-pkarr/src/offer.rs +++ b/crates/openhost-pkarr/src/offer.rs @@ -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 `-` 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; 256] = { + const NONE: Option = 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::() { + 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())?; @@ -1123,10 +1156,11 @@ pub fn decode_answer_fragments_from_packet( let mut fragments: Vec = 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 {