The full-table profiling script took its peers as unconfigured ones under
the permissive policy, which is not how a router is set up. Now it writes
a peer block for each of them, with `expected_routes` set to the table's
size, so that the default import policy and anything that sizes itself by
the expected routes are in the measurement. The few bogon prefixes in the
synthetic table are refused by that policy, as the output says.
After the table is in, the script also prints
`bgp_kernel_route_changes_total`, so that a run shows how many installs
and removals the kernel was asked for and not only how many routes it
ended up with.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Before this change there was no way to see where the daemon's CPU time
goes while it receives a full Internet routing table: the benchmarks in
crates/bench time the routing table in-process, without the sessions,
the policy, the kernel task or the production allocator. Profiling the
daemon itself showed that a third of its CPU time during a full-table
load went into `Plan::blackholes()`, which walked every installed route
to count the blackhole ones each time the kernel task published its
status, after every batch of changes; with 1.2 million routes
installed that was 3.9 seconds from the first route to a synced kernel
instead of 2.0.
This change:
* Keeps counts of blackhole routes and of routes kept unconfirmed from
an earlier run in `Plan`, in step with the map through `Plan::put`
and `Plan::take`, so that `Plan::blackholes()` and `Plan::kept()`
cost nothing. `kept()` had the same scan; it ran only while routes of
an earlier run were being kept, which is exactly during a full-table
load after a restart.
* Adds `scripts/profile/full-table.sh`, which builds the daemon with the
new `profiling` Cargo profile (release with line tables) and frame
pointers, runs it in a network namespace joined by a veth pair to a
second namespace, feeds it the synthetic table from there as one or
more neighbors, samples it with `perf`, and writes a flame graph
(inferno), flat profiles by self and inclusive time, the time until
the routes are in the table and in the kernel, and the daemon's
resident memory. `PEERS`, `KERNEL`, `SCALE_DOWN`, `HOLD_SECS`, `FREQ`
and `OUT` control it. From macOS it runs in the Linux VM through
scripts/linux-vm.sh.
* Adds the load generator `crates/bench/examples/feed.rs`: it connects
from the peers' addresses, completes the handshakes, writes each
neighbor's table, drains what the daemon sends back and holds the
sessions open with KEEPALIVEs. `TestPeer::into_stream` gives it the
connection after the handshake.
* Updates the testing guide: the current `full_table` numbers, a
"Profiling" section, and the daemon's numbers in the VM.
Measured in the Linux VM (8 vCPUs) with the synthetic table of 1.23
million prefixes: one neighbor's table is in the routing table after
0.7 s and in the kernel after 2.0 s (before: 3.9 s); four neighbors'
4.9 million routes are in the table after 1.1 s and the kernel is
synced after 3.0 s; 520 MB resident with one table, 680 MB with four.
Without the kernel one session takes in 2.0 million routes per second
on one core. A profile also attributed 14% of the session's time to
`Journal::tidy`; inlining its early return changed nothing in an A/B
run of the benchmark, so that was a sampling artefact and is left as it
is.
Testing: a unit test checks the counts against a scan of the entries
through adopt, best, reset, synced, release, withdrawal, refusal and
clear; the kernel test suite and the workspace tests pass; the script
was run with one and four neighbors, with and without the kernel.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>