Repository navigation
daemon: much less CPU under send bursts - #510
Merged
Merged
Conversation
Profiling two local daemons under 32- and 256-way bursts of `pilotctl send-message` showed the sender spending about 85% of its CPU in the Go scheduler, and 73% of its allocations on `info` replies. - Dials no longer poll their connection every 10 ms. SYN-ACK already signalled DialCh; RST and shutdown now do too, and the ticker is a 250 ms backstop. - Concurrent info requests share one reply for 200 ms. Every send asks for info, and the reply serialises every connection, so N concurrent sends cost N builds over a table of N rows. Requests arriving during a build wait for it instead of starting their own (this also stops a burst from issuing one registry lookup per request when the network-membership cache expires). - The daemon caps GOMAXPROCS at 4 unless the variable is set. Sender CPU per message: 4.5 -> 1.4 ms at 32 concurrent, 24 -> 2.6 ms at 256. Most of it comes from the first two changes (3.5 ms at 256 with 16 threads); the thread cap takes the rest. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The cap of 4 OS threads applied to every install unless GOMAXPROCS was set, including large hosts and the hosted variant. Keep the dial wake-up and the shared info reply; the changelog quotes this PR's measurements at 16 threads, which is what ships. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
TeoSlayer
force-pushed
the
perf/daemon-burst-cpu
branch
from
October 8, 2026 09:23
83e2ada to
08cc04d
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Profiling two local daemons under bursts of
pilotctl send-message(32- and 256-way, loopback, no network loss) showed the sending daemon spending ~85% of its CPU in the Go scheduler (waking and parking threads) and 73% of everything it allocated oninforeplies. Three changes:conn.DialCh; RST and shutdown now do too, so the ticker is only a 250 ms backstop. With hundreds of dials in flight the 10 ms tickers alone kept the scheduler busy.inforequests share one reply for 200 ms. Everypilotctl send-messageasks forinfo(to see whether a session with the peer exists), and the reply serialises every peer and connection row — so N concurrent sends cost N builds over a table that itself grows with N. Requests that arrive during a build wait for it rather than starting their own; this also stops a burst from issuing one registry lookup per request when the network-membership cache has just expired. The TTL is a package var so tests can turn reuse off.GOMAXPROCSis set. One thread per core (16 on the test machine, 64 on the service-agent VM) is what made the scheduler churn expensive.Measured (sender daemon, CPU per message)
Memory right after the burst: 48 → 35 MB (32-way), 65 → 56 MB (256-way). The receiving daemon is unchanged (~1.2 ms per message).
Not changed here, found on the way: at 256-way, 40–46% of sends fail on both builds. The receiver drops SYNs past its per-source limit (10/s sustained, burst 64), and each refused dial retries for seconds. Connection-per-message is the underlying cost.
Functionality
TestInfoReplyIsSharedWithinTTLThenRebuilt,TestInfoReplyReuseCanBeTurnedOff,TestResetWakesAWaitingDial(the last fails onmain).go test ./pkg/... ./cmd/... ./internal/... -short: pass. Fullgo test -parallel 4 -count=1 ./tests/: pass (291 s).mainpass on this branch. The image was built from a checkout of this branch with tests: make the Docker integration suite build and boot again #506 merged and the tests ran from it, so the NAT tests'compose up --buildrebuilt the same code. (An earlier run reported here had built from the harness checkout, i.e.main, after the first NAT test; corrected.)pilotctl infocalls within 200 ms show the same numbers.GOMAXPROCSstill overrides the thread cap.🤖 Generated with Claude Code