A code-quality pass over the periphery. The packet crate frames a message with `begin_message` and `finish_message`, which the exporter's hand-written UPDATE header can use instead of a bare type code, and keeps the NLRI reader and writer together as a pair; the rule that the body's prefixes are IPv4 unicast lives once; a session with two-octet AS numbers no longer reserves the attributes' wire copy it never keeps. The TCP diagnostics read netlink with the netlink module's own attribute walk and helpers instead of copies of them. The kernel follower learns what the kernel has in one named step used at start and on relearning, keeps its batch buffers between wakes, and builds its task in a constructor. The mesh benchmark, which had grown six phases in one main, is a `Mesh` with named phases and a `Receiver`, with one report of the sessions for both deadlines; the collector capabilities, the KEEPALIVE bytes, the draining of a session and the printing of a table row are helpers shared by the benches, the examples and the integration tests. The MRT reader uses the packet crate's attribute splitter. Testing: a message framed by hand is the one `to_wire` writes; a short tcp_info from an old kernel leaves the later fields zero; the synthetic table is the same every run and every announcement decodes to itself. The packing example gives the same figures on the RIS dump before and after. The crates' tests, the integration tests, the kernel tests and every lab phase pass. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
203 lines
7.9 KiB
Rust
203 lines
7.9 KiB
Rust
//! What it costs to take in the Internet routing table, from the bytes on
|
|
//! the wire to a chosen best route.
|
|
//!
|
|
//! Run with `cargo bench -p bgp-bench --bench full_table`. Set
|
|
//! `BENCH_PEERS` (default 4) and `BENCH_SCALE_DOWN` (default 1; 10 makes the
|
|
//! table ten times smaller) to change the size. `BENCH_PRESIZE=0` leaves
|
|
//! the table to grow as it fills, as it does without `expected_routes`. The table is synthetic (see
|
|
//! the crate documentation); `real_table` runs on a real one.
|
|
//!
|
|
//! This is a stopwatch around whole operations at full size, not a
|
|
//! statistical micro-benchmark: the questions are "how long until a full
|
|
//! table is in" and "how much memory does it take", and their answers are
|
|
//! seconds and gigabytes.
|
|
|
|
use std::hint::black_box;
|
|
use std::net::Ipv4Addr;
|
|
use std::sync::atomic::{AtomicUsize, Ordering};
|
|
use std::time::{Duration, Instant};
|
|
|
|
use bgp_bench::memory::{Region, describe};
|
|
use bgp_bench::report::{mib, per_second, row};
|
|
use bgp_bench::{Prefixes, Shape, announcements, encoded, env, wire_context};
|
|
use bgp_integration_tests::test_peer::TestPeer;
|
|
use bgp_integration_tests::test_server::TestServer;
|
|
use bgp_packet::message::{Message, PathAttribute};
|
|
use bgp_primitives::ip_prefix::IpPrefix;
|
|
use bgp_rib::table::{PeerIndex, PeerInfo, RibTable};
|
|
use bytes::BytesMut;
|
|
|
|
fn main() {
|
|
let peers = env("BENCH_PEERS", 4) as u32;
|
|
let shape = Shape::INTERNET.scaled_down(env("BENCH_SCALE_DOWN", 1));
|
|
let threads = std::thread::available_parallelism().map_or(1, |n| n.get());
|
|
|
|
println!(
|
|
"== Table: {} IPv4 + {} IPv6 prefixes, from each of {peers} peers",
|
|
shape.ipv4_prefixes, shape.ipv6_prefixes
|
|
);
|
|
println!(
|
|
"(IpPrefix is {} bytes, PathAttribute {})",
|
|
size_of::<IpPrefix>(),
|
|
size_of::<PathAttribute>()
|
|
);
|
|
let prefixes = Prefixes::generate(shape);
|
|
let wire: Vec<Vec<BytesMut>> = (1..=peers)
|
|
.map(|peer| encoded(&announcements(&prefixes, shape, peer)))
|
|
.collect();
|
|
let updates: usize = wire.iter().map(Vec::len).sum();
|
|
let routes = shape.prefixes() * peers as usize;
|
|
let wire_bytes: usize = wire.iter().flatten().map(BytesMut::len).sum();
|
|
println!(
|
|
"{updates} UPDATE messages, {:.1} prefixes each, {:.1} MiB on the wire, {routes} routes",
|
|
routes as f64 / updates as f64,
|
|
mib(wire_bytes),
|
|
);
|
|
|
|
println!("\n== Reading the messages (one thread)");
|
|
let ctx = wire_context();
|
|
let started = Instant::now();
|
|
for message in wire.iter().flatten() {
|
|
black_box(Message::decode(&ctx, message).expect("what we wrote is well formed"));
|
|
}
|
|
let elapsed = started.elapsed();
|
|
row(
|
|
"decode UPDATEs",
|
|
elapsed,
|
|
format!(
|
|
"{} routes, {} messages, {:.0} MiB/s",
|
|
per_second(routes, elapsed),
|
|
per_second(updates, elapsed),
|
|
mib(wire_bytes) / elapsed.as_secs_f64()
|
|
),
|
|
);
|
|
|
|
println!("\n== From bytes to best routes, without the network");
|
|
for threads in [1, threads] {
|
|
into_table(&wire, routes, threads);
|
|
}
|
|
|
|
println!("\n== End to end: {peers} peers send the table over TCP at once");
|
|
let runtime = tokio::runtime::Builder::new_multi_thread()
|
|
.enable_all()
|
|
.build()
|
|
.expect("a runtime");
|
|
runtime.block_on(async {
|
|
// An AS number that is in none of the generated paths: a route
|
|
// with the server's own AS in its path is refused as a loop.
|
|
let server = TestServer::start(4_200_000_000).await.expect("a server");
|
|
let mut senders = Vec::new();
|
|
for (index, messages) in wire.iter().enumerate() {
|
|
let peer = index as u32 + 1;
|
|
let mut session = TestPeer::connect(server.addr).await.expect("a connection");
|
|
session
|
|
.handshake(64512 + peer, Ipv4Addr::new(10, 0, 0, peer as u8), 90)
|
|
.await
|
|
.expect("a session");
|
|
// The same bytes `table_stream` would make, from the messages
|
|
// already encoded for the decoding above.
|
|
let stream: Vec<u8> = messages.iter().flat_map(|m| m.iter().copied()).collect();
|
|
senders.push((session, stream));
|
|
}
|
|
let started = Instant::now();
|
|
let sending: Vec<_> = senders
|
|
.into_iter()
|
|
.map(|(mut session, stream)| {
|
|
tokio::spawn(async move {
|
|
session.send_raw(&stream).await.expect("the server reads");
|
|
// Held open until the table is in.
|
|
session
|
|
})
|
|
})
|
|
.collect();
|
|
while server.rib.len() < routes {
|
|
tokio::time::sleep(Duration::from_millis(2)).await;
|
|
}
|
|
let elapsed = started.elapsed();
|
|
row(
|
|
"sessions, state machine, table",
|
|
elapsed,
|
|
format!(
|
|
"{} routes; {} prefixes with a best route",
|
|
per_second(routes, elapsed),
|
|
server.rib.prefix_count()
|
|
),
|
|
);
|
|
for session in sending {
|
|
drop(session.await);
|
|
}
|
|
});
|
|
}
|
|
|
|
/// What a session does with each message, without the session: decode it,
|
|
/// find its attribute set, put its routes in the table, choose the best.
|
|
/// `threads` threads each take whole peers.
|
|
fn into_table(wire: &[Vec<BytesMut>], routes: usize, threads: usize) {
|
|
let region = Region::begin();
|
|
let table = RibTable::new(threads * 16);
|
|
if env("BENCH_PRESIZE", 1) == 1 {
|
|
table.expect_prefixes(routes / wire.len());
|
|
}
|
|
let peers: Vec<PeerIndex> = (0..wire.len())
|
|
.map(|peer| {
|
|
table
|
|
.add_peer(PeerInfo {
|
|
asn: 64512 + peer as u32 + 1,
|
|
router_id: Ipv4Addr::from(peer as u32 + 1),
|
|
address: Ipv4Addr::new(192, 0, 2, peer as u8 + 1).into(),
|
|
interface: 0,
|
|
ibgp: false,
|
|
reflector_client: false,
|
|
})
|
|
.expect("room for the peer")
|
|
})
|
|
.collect();
|
|
let ctx = wire_context();
|
|
let next = AtomicUsize::new(0);
|
|
let started = Instant::now();
|
|
std::thread::scope(|scope| {
|
|
for _ in 0..threads {
|
|
scope.spawn(|| {
|
|
loop {
|
|
let at = next.fetch_add(1, Ordering::Relaxed);
|
|
let Some(messages) = wire.get(at) else {
|
|
break;
|
|
};
|
|
for message in messages {
|
|
let decoded =
|
|
Message::decode_with_report(&ctx, message).expect("well formed");
|
|
let Message::Update(update) = decoded.message else {
|
|
continue;
|
|
};
|
|
let wire = decoded.attributes_wire.as_deref().expect("kept");
|
|
if !update.announced_routes.is_empty() {
|
|
let attrs = table.attrs().intern_received(wire, None).expect("valid");
|
|
table.announce(peers[at], &attrs, &update.announced_routes);
|
|
}
|
|
for attribute in &update.path_attributes {
|
|
if let PathAttribute::MpReachNlri(mp) = attribute {
|
|
let attrs = table
|
|
.attrs()
|
|
.intern_received(wire, Some(&mp.next_hop))
|
|
.expect("valid");
|
|
table.announce(peers[at], &attrs, &mp.prefixes);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
});
|
|
}
|
|
});
|
|
let elapsed = started.elapsed();
|
|
assert_eq!(table.len(), routes);
|
|
row(
|
|
&format!("decode, intern, insert, choose: {threads} thread(s)"),
|
|
elapsed,
|
|
format!(
|
|
"{} routes; {}",
|
|
per_second(routes, elapsed),
|
|
describe(region.held(), routes, "route")
|
|
),
|
|
);
|
|
}
|