From 92b9bdd3c58971d011ae9e152e502bc704316376 Mon Sep 17 00:00:00 2001 From: Rayhaan Jaufeerally Date: Sat, 10 Oct 2026 14:19:02 +0200 Subject: [PATCH] export: keep the peer reading while the next batch is dressed An exporter following the table took up to 65,536 changes at a time, dressed them all, and only then sent them; and before each batch it slept the 50 ms that lets a run of changes be gathered, whether or not the table was already far ahead of it. The session's queue holds about a thousand UPDATEs, so once the batch went out the exporter waited on the peer reading it, and while the next batch was dressed the peer had nothing to read. A profile of two routers exchanging their tables, sampled in 25 ms windows, showed the two stages taking turns: the exporters at two cores while the sessions were at none, then the other way round, with a gap of 50 to 75 ms at the start of each cycle for the sleep. Each task ran at about half a core. A batch is now 4,096 changes, so that what one batch sends is being read by the peer while the next is being dressed, and the gathering sleep is skipped while the reader is behind the table: a run is in hand already. A reader says whether its last poll caught up. Testing: the server tests, the integration tests, the kernel tests and every lab phase pass. On the mesh benchmark in the Linux VM, three rounds each alternating with the previous commit, the larger table (4.9 million routes) is across in 5.6 s instead of 6.7 s and the smaller (2.5 million) in 4.4 s instead of 6.4 s; the whole exchange takes 7.3 s of wall-clock time instead of 8.6 s, and 18.9 s of CPU time instead of 20.5 s. Co-Authored-By: Claude Fable 5.1 --- crates/rib/src/table/journal.rs | 5 +++++ crates/server/src/advertise.rs | 19 ++++++++++++++++--- 2 files changed, 21 insertions(+), 3 deletions(-) diff --git a/crates/rib/src/table/journal.rs b/crates/rib/src/table/journal.rs index d65c90e..3b17d97 100644 --- a/crates/rib/src/table/journal.rs +++ b/crates/rib/src/table/journal.rs @@ -706,6 +706,11 @@ impl Reader { Progress { events, caught_up } } + /// Whether the last [`Reader::poll`] read everything there was. + pub fn caught_up(&self) -> bool { + self.caught_up + } + /// Waits until there may be something to read. Returns at once if the /// last [`Reader::poll`] did not catch up. pub async fn changed(&mut self) { diff --git a/crates/server/src/advertise.rs b/crates/server/src/advertise.rs index 2fdd3f2..11c6544 100644 --- a/crates/server/src/advertise.rs +++ b/crates/server/src/advertise.rs @@ -245,7 +245,10 @@ struct Exporter { } /// How many changes of best route are taken from the table at a time. -const EXPORT_BATCH: usize = 65_536; +/// Small enough that what one batch sends is being read by the peer +/// while the next is being dressed: a batch is dressed whole before any +/// of it goes, and the session's queue holds the exporter once full. +const EXPORT_BATCH: usize = 4_096; /// How long changes are gathered before they go out, once there are any. const COALESCE: Duration = Duration::from_millis(50); @@ -649,8 +652,11 @@ impl Exporter { // Changes come in runs: a table arriving, a peer // going. Gathered for a moment, the routes that go out // alike go in one message, and the whole run costs one - // look at what each peer is told. - tokio::time::sleep(COALESCE).await; + // look at what each peer is told. Not while the table + // is ahead of this exporter: there is a run in hand. + if self.caught_up() { + tokio::time::sleep(COALESCE).await; + } self.follow().await?; self.end_of_rib().await?; } @@ -732,6 +738,13 @@ impl Exporter { /// Takes the next changes of best route from the table and tells the /// peer of those that are for it. + /// Whether the last look at the table saw everything in it. + fn caught_up(&self) -> bool { + self.learned + .as_ref() + .is_none_or(|learned| learned.reader.caught_up()) + } + async fn follow(&mut self) -> Result<(), Finished> { let Some(learned) = &mut self.learned else { return Ok(());