Skip to content

pgo - #1825

Draft
daniel-noland wants to merge 10 commits into
pr/daniel-noland/profilingfrom
pr/daniel-noland/pgo-bolt
Draft

pgo#1825
daniel-noland wants to merge 10 commits into
pr/daniel-noland/profilingfrom
pr/daniel-noland/pgo-bolt

Conversation

@daniel-noland

Copy link
Copy Markdown
Collaborator

scratch: don't yet review please

🤖 Generated with Claude Code

@daniel-noland daniel-noland added the dont-merge Do not merge this Pull Request label Sep 15, 2026
@coderabbitai

coderabbitai Bot commented Sep 15, 2026 •

Copy link
Copy Markdown

Important

Draft PR not reviewed

Draft PRs are not automatically reviewed by default.

  • Trigger a manual review

To automatically review draft PRs, update your CodeRabbit configuration:

reviews:
  auto_review:
    drafts: true
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Comment @coderabbitai help to get the list of available commands.

@daniel-noland daniel-noland changed the title (23) pgo pgo Sep 15, 2026
@daniel-noland
daniel-noland force-pushed the pr/daniel-noland/pgo-bolt branch from 5d2ac08 to 9606b0e Compare October 3, 2026 03:28
@daniel-noland
daniel-noland force-pushed the pr/daniel-noland/dataplane-perf branch 10 times, most recently from 54624a4 to 76fd1ef Compare October 4, 2026 06:05
Base automatically changed from pr/daniel-noland/dataplane-perf to split/dpdk-core/08-probes October 4, 2026 06:05
@daniel-noland
daniel-noland force-pushed the split/dpdk-core/08-probes branch from 6e3983c to fcb1b62 Compare October 7, 2026 03:09
@daniel-noland
daniel-noland force-pushed the pr/daniel-noland/pgo-bolt branch from 9606b0e to 8cc6c76 Compare October 7, 2026 03:09
@daniel-noland
daniel-noland changed the base branch from split/dpdk-core/08-probes to pr/daniel-noland/profiling October 7, 2026 03:09
@codecov

codecov Bot commented Oct 7, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 70.75472% with 62 lines in your changes missing coverage. Please review.
✅ All tests successful. No failed tests found.

Files with missing lines Patch % Lines
interface-manager/src/monitor/mod.rs 60.93% 25 Missing ⚠️
init/src/supervisor.rs 83.33% 10 Missing and 3 partials ⚠️
init/src/main.rs 61.53% 10 Missing ⚠️
dataplane/src/drivers/supervisor.rs 0.00% 9 Missing ⚠️
dataplane/src/profile.rs 0.00% 3 Missing ⚠️
dataplane/src/runtime.rs 0.00% 2 Missing ⚠️

📢 Thoughts on this report? Let us know!

@daniel-noland
daniel-noland force-pushed the pr/daniel-noland/profiling branch from 7085834 to d75c833 Compare October 8, 2026 00:41
@daniel-noland
daniel-noland force-pushed the pr/daniel-noland/pgo-bolt branch 2 times, most recently from 7c5a00b to bb0e9a7 Compare October 8, 2026 01:53
@daniel-noland
daniel-noland force-pushed the pr/daniel-noland/profiling branch 2 times, most recently from 6b444e9 to b0247ce Compare October 8, 2026 03:32
@daniel-noland
daniel-noland force-pushed the pr/daniel-noland/pgo-bolt branch from bb0e9a7 to 9d2a932 Compare October 8, 2026 03:32
@daniel-noland
daniel-noland force-pushed the pr/daniel-noland/profiling branch from b0247ce to a8a70e1 Compare October 8, 2026 07:16
@daniel-noland
daniel-noland force-pushed the pr/daniel-noland/pgo-bolt branch from 9d2a932 to 8e6b420 Compare October 8, 2026 07:16
@daniel-noland
daniel-noland force-pushed the pr/daniel-noland/pgo-bolt branch from 8e6b420 to 8ad5c3b Compare October 16, 2026 05:47
@daniel-noland
daniel-noland force-pushed the pr/daniel-noland/profiling branch from 687c8b6 to 2bea7fc Compare October 16, 2026 08:43
@daniel-noland
daniel-noland force-pushed the pr/daniel-noland/pgo-bolt branch from 8ad5c3b to 1cca809 Compare October 16, 2026 08:43
@daniel-noland
daniel-noland force-pushed the pr/daniel-noland/profiling branch from 2bea7fc to ee6d8cf Compare October 17, 2026 03:26
@daniel-noland
daniel-noland force-pushed the pr/daniel-noland/pgo-bolt branch from 1cca809 to 18d1203 Compare October 17, 2026 03:26
@daniel-noland
daniel-noland force-pushed the pr/daniel-noland/profiling branch from ee6d8cf to f1e0f51 Compare October 17, 2026 05:59
@daniel-noland
daniel-noland force-pushed the pr/daniel-noland/pgo-bolt branch from 18d1203 to 1031436 Compare October 17, 2026 05:59
daniel-noland and others added 10 commits October 17, 2026 00:17
…nges

The monitor subscribed to netlink link multicast and nothing else, so it
learned an interface's MAC only if the address changed *while it was
listening*. Two ways that loses, and both stick forever because a MAC does not
change twice:

1. The change lands before the router has the interface in its table.
   `handle_ifevent` returns early for an unknown ifindex (ctl.rs:268) and the
   event is gone.
2. The change happens before the monitor is listening at all -- which is the
   normal case, since init creates the control-plane tap and gives it the DPDK
   port's address during startup.

The consequence is severe and silent. The datapath tests every arriving frame's
destination against the table's address, so a stale entry means every unicast
frame is dropped as `MacNotForUs`: ARP never resolves, BFD fails, BGP tears
down, every route is withdrawn, and the gateway forwards nothing while looking
healthy by every other measure -- process up, config applied, BGP configured.
Observed repeatedly on the bench, where the table held a tap's random birth
address (`0a:..`, `8a:..`) against a port MAC of `58:a2:e1:b3:3d:94`.

So read the state instead of waiting to be told about it:

- `resync` dumps every tracked link via RTM_GETLINK and emits the current
  state through the same channel. Run once at startup, before any notification
  is processed.
- Re-run every 5s. A notification dropped downstream is then costly for
  seconds rather than for the life of the process. This is the level-triggered
  backstop; the multicast subscription remains the low-latency path.
- `link_to_event` is split out of `netlink_to_event` so a dumped `LinkMessage`
  and a multicast notification share one conversion.

This is the smaller half of closing this loop. The larger one -- making
interface-manager authoritative for observed state, so configuration carries
intent only and never a MAC -- is a separate change, and this is a strict
subset of it rather than work that has to be undone.

Tests cover the conversion resync depends on, and were break-tested: ignoring
the address attribute fails `a_dumped_link_yields_its_current_mac`, and
dropping every link fails that plus
`a_link_without_an_address_still_reports_state`.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`DriverStatus`'s `Display` read `DriverKernel::MAX_RX_PKT_BATCH` (128)
unconditionally, so a gateway running the DPDK driver reported a receive batch
of 128 when the real burst is `MBUF_BURST` (64).

Nothing depended on the value, which is why it survived. It is still worth
fixing: the number was read off a live gateway, used to size the per-burst
cache working set, and produced a confident and wrong claim that the working
set exceeded L1d (50.2 KB against 48 KB). The real figure is ~33 KB and fits.
A wrong number on a status page became a wrong conclusion about cache pressure.

`DriverStatus` now carries a `DriverLimits` that the publishing driver fills
in, so each reports its own batch size and watchdog periods rather than the
display assuming one driver's constants.

The test asserts both that the publisher's value is shown and that the other
driver's does not leak in, and opens with a vacuity guard: the whole test means
nothing if the two constants ever become equal, so it says so rather than
passing silently. Break-tested by restoring the hardcoded constant.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A profile-instrumented build writes its counters when told to, but the
dataplane never exits on its own -- it runs until something kills it, and a
kill loses the counters. This gives us a way to ask for them without stopping
the gateway: signal init, and init passes SIGUSR1 along.

Forwarding is opt-in via DATAPLANE_DEV_PROFILE_DUMP, because the default
disposition of SIGUSR1 is to terminate, and an unwanted one would reach a
process that dies on it. The value names the target, defaulting to
"dataplane": FRR is supervised here too and reads SIGUSR1 as "rotate your
logs", so a broadcast would quietly do something else entirely.

The signal goes to the process, not its group. A group signal reaches
children that have no handler, where the default disposition kills them; the
test holds that line with a handler-less child of its own, and fails on the
group-signalling version.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Adds `instrumentation=pgo`, and a way to get a profile out of a build that
uses it. A `-Cprofile-generate` build writes its counters from an atexit
handler that the dataplane never reaches: it runs until something kills it,
and a kill loses them. SIGUSR1 now asks for them without stopping, so the
profile can come off a run that is still under load -- which is the only run
whose profile is worth having.

The signal already arrived here and was matched into an arm that did nothing.
It now calls the profiling runtime, and no longer shares an arm with SIGUSR2,
SIGHUP, SIGALRM and SIGPIPE, which should still do nothing.

Gating is on `profile_generate`, a cfg set by the same `instrumentation`
argument that adds the flag, rather than a cargo feature. The two cannot come
apart, so the symbol the call needs is present wherever the call compiles; an
uninstrumented build takes the other arm and says so, rather than leaving a
requested dump looking like it happened.

Where the counters land is defaulted, not forced, the same way perf's `-o` is,
and tied to the switch that turns forwarding on: a forward that arrives with
nowhere to write is not much use. The destination is a plain filename and not
one of LLVM's `%m` merge patterns -- each dump holds the counters accumulated
since process start, so merging one into the last would count everything
before the first dump twice.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…sion

A profile is only worth collecting from a real run under real traffic, so an
instrumented build that cannot be containerised cannot reach the test bench.
`_refuse-instrumented-artifact` blocked every instrumentation mode, which made
`pgo` useless from the moment it existed. It is now exempt; coverage and fuzz
still refuse, because those really are diagnostics that never want to ship.

The exemption is safe because the reason for the refusal is now addressed
rather than waived. Instrumentation was not part of the version, so an
instrumented image would take a clean tag and replace it -- the guard's own
stated objection. `version_instr` puts it in, next to the sanitizer and
feature components that are there for the same reason: an instrumented binary
performs nothing like a clean one, and a tag that does not say so is a trap
for whoever pulls it next. A pgo image is `-instr.pgo` and cannot occupy the
clean tag.

Also gives `-Cprofile-generate` the directory rustc requires; a bare one is
rejected with "must have a value". It is only a default, since
`LLVM_PROFILE_FILE` overrides it at runtime, and it names the same directory
init puts a perf profile in -- the one place in the container that outlives
the container.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…uld derive

`version_instr` only applies to a computed version: `version := env("VERSION",
...)` means an explicit VERSION= bypasses it entirely -- and that is the path
any deploy or bench script takes, so the protection was absent exactly where
it was most needed.

The guard now looks at the version it is about to tag with. An instrumented
image whose version does not name the instrumentation is refused, however that
version was arrived at.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`-Cprofile-use` consumes the Rust side only; instrumenting C as well collects
counters nothing then reads, and C PGO would need its own `-fprofile-use`.
This is a scope change, not a performance fix.

It is worth recording what it is *not*, because the first version of this
commit claimed otherwise. An instrumented build collapses under load, and
excluding DPDK does not rescue it. Measured on env-5, 64 B at 128 streams:

| workers | clean | C+Rust instr | Rust-only instr |
|---|---|---|---|
| 1  | 1.73  | 0.328 | 0.366 |
| 16 | 14.56 | 0.128 | 0.033 |

Excluding C buys nothing measurable at one worker, and at sixteen both are
gone. The mechanism is not instrumentation overhead, which is a tolerable 5x
per core: it is that LLVM's counters are process-global, so sixteen workers
running identical code do read-modify-write on the same cache lines and
ping-pong them. Throughput *falls* from 1 worker to 16 -- adding fifteen cores
makes it 2.6x slower -- which per-core overhead cannot produce.

The consequence for anyone collecting a profile: take it at one or two
workers. PGO wants relative block frequencies, and a single worker walking the
same pipeline gives those just as well, at a 5x slowdown rather than a 400x
one. DPDK's own `b_pgo=generate` would not have helped; meson emits the same
`-fprofile-generate`.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Collecting a profile was only half of it; nothing could consume one. Adds two
instrumentation modes and the recipes that join them up.

`pgo-use` feeds a merged profile back with `-Cprofile-use`. The path is fixed
at `.pgo/dataplane.profdata` rather than passed as an argument, so nothing has
to be threaded through the three `profiles.nix` call sites; nix copies the file
into the store, so editing it rebuilds what depends on it, and a missing one
fails evaluation naming the path. `-pgo-warn-missing-function` goes with it,
because rustc's default is to warn quietly and carry on, which reads exactly
like a successful PGO build when the profile no longer matches the code.

`bolt` asks the linker for the relocations it normally drops, which is all a
binary needs to become a BOLT input. It stays separate from `pgo-use` so the
two compose: a PGO build is a perfectly good BOLT input.

`just pgo-merge`, `just bolt-fdata` and `just bolt` drive the tools, and BOLT
joins the dev shell from the same `llvmPackages'` as clang, so it cannot drift
to a different LLVM than the one that built the binary.

The container guard now refuses by name. It had been written as "anything that
is not none", which would have blocked both new modes -- and those are the ones
that produce artifacts worth shipping: `pgo-use` is not instrumented at all,
and `bolt` only carries extra relocations. Only coverage and fuzz are diagnostic
builds that never want to ship.

Profiles are gitignored: megabytes of machine-specific counters with a short
shelf life, and a stale one is worse than none, since `-Cprofile-use` will
consume it happily and optimise for a workload nobody runs any more.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…acks properly

Two things the first BOLT run got wrong, both found by running it.

`instrument=bolt` did not build at all. `--emit-relocs` makes the linker emit
`.rela.debug_*` alongside the code relocations, and postInstall then removes
`.debug_loclists`, leaving a relocation pointing at a section that is gone:
objcopy stops with "symbol `.debug_loclists' required but not present" and the
cli derivation fails. A BOLT build now keeps its debug sections, which it wants
anyway to update them as it moves code. Other builds are untouched -- the
objcopy invocation is unchanged for them.

`bolt-fdata` decided whether to pass `-nl` by grepping the report text for
"branch", which reported "no branch stacks" for a recording made with
`perf record -j any,u` and would have silently dropped every future run into
degraded mode. It now reads the recording's own sample_type, which gains BRANCH
exactly when branch records are present, and says nothing when they are.

Verified end to end against a real build: perf2bolt consumed branch events and
wrote a profile, llvm-bolt rewrote the binary (taken branches -43.7%), and the
result runs with output identical to the original once argv[0] is normalised.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Daniel Noland <daniel@githedgehog.com>
Co-authored-by: Codex <codex@openai.com>
@daniel-noland
daniel-noland force-pushed the pr/daniel-noland/profiling branch from f1e0f51 to 2771be3 Compare October 17, 2026 06:17
@daniel-noland
daniel-noland force-pushed the pr/daniel-noland/pgo-bolt branch from 1031436 to 2149d34 Compare October 17, 2026 06:17
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

dont-merge Do not merge this Pull Request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant