perf: hot-path round — event amplification, fan-out, and per-request costs#2
Merged
Conversation
A pool status write during replenishment re-enqueued every claim referencing the pool; bound claims are driven by their own Sandbox events, so the map now skips them.
writeStatus re-entered the global loop through the For watch; under claim churn the 5s cadence degraded into a continuous back-to-back fleet PUT fan-out. Generation filtering keeps spec edits immediate.
Owns(Sandbox) passed every member update; one claim or Ready flip cost a full O(members) rescan, and the cache deep-copied all members per List. Updates are now filtered to transitions the reconcile reads, the List is copy-free with deep-copy-on-write at the two mutation sites, the template-ref hash is computed once per pass, and the stuck-member grace period gets its own requeue timer instead of riding on pool churn.
Both walked the fleet sequentially, one full inventory decode per node; Get now cancels on first hit and both reuse List's bounded fan-out.
Every get/delete/pause/connect/fork materialized and sorted the whole fleet to match one id. The store now resolves a claim id per node with first-hit cancel, materializing only the match; stores without the fast path keep the List fallback.
The state annotation embedded a full serialized copy of the object — near-doubling stored and wire size — to restore two replica fields. It now carries exactly those fields; legacy full-object annotations still decode.
fanOutNodes dedupes the scatter-gather skeleton; matchesID compares the DNS-safe rendering in place (the id sweep allocated per scanned entry); the member predicate composes LabelChangedPredicate and adds the generation bump so orphan spec edits re-vet; shared indexer and fake-client fixtures replace copy-pasted closures.
…redicate The reconcile reads only the controller ref; DeepEqual over OwnerReferences cost ~780ns + 12 allocs per member event vs ~4ns for the UID compare. Also drop the full-count capacity hint in mapWarmPoolToClaims now that bound claims are filtered.
detailFor hardcoded StateRunning, so a paused sandbox kept reporting running to the SDK. Derive state from the phase label the node publishes — the same signal isPaused falls back to.
Dead seams (QuotaChecker, WithConcurrency, PublishPeriodically, write-only Recorder, single-impl queue interface), unreachable guards, always-nil conversion error plumbing, duplicated bench/loadgen harness helpers (new test/benchutil), and stdlib forms (ContainsFunc, Concat, new(expr), errgroup.SetLimit). Verified no workspace-external callers before each cut.
CMGS
force-pushed
the
perf/hot-path-round
branch
from
July 27, 2026 10:30
d58de93 to
407a94c
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.
Six optimizations from a static performance analysis of the claim path and its neighbors. Each fix is paired with a benchmark committed immediately before it, so every claim below is an A/B on the same harness; commits alternate bench → fix.
Methodology. All numbers from a Linux container (golang:1.26) on a bare-metal bench host (AMD Ryzen 7 9700X, 16 threads),
-count=10(lookup: 6), compared with benchstat, p < 0.01 unless noted. Micro-benchmarks use the fake client /StaticInventorySource, which bounds what they can show (noted per item). Cluster-level claim-latency validation (poolbench p50/p95 at concurrency 20) is the follow-up acceptance run.1. Pool events no longer re-enqueue bound claims (1463bbd)
mapWarmPoolToClaimsre-enqueued every claim referencing a pool on every pool write; replenishment churn amplified into O(claims) no-op reconciles. Bound claims are now skipped — they are driven by their ownOwns(Sandbox)watch.2. Warm-pool driver ignores its own status writes (e6eda85)
The global driver loop's
writeStatusre-triggered its ownFor(SandboxWarmPool)watch; under claim churn the 5 s cadence degraded into a continuous back-to-back fleet PUT fan-out.GenerationChangedPredicatefilters status-only events; spec edits, create/delete, NodeInventory events, and the 5 s requeue keep full coverage.BenchmarkReconcileOnce(26 nodes, 4 pools): 0.10 ms + 81 KB per wake-up — the cost each filtered spurious wake no longer pays, plus the per-node PUT fan-out it no longer sends.3. Warm pool: member-event filtering + copy-free member List (eb9ac04)
Owns(Sandbox)had no predicate: every member update — pod IP changes, unrelated conditions, annotations — triggered a full pool reconcile (one O(N) cache List that deep-copied all members, plus three O(N) passes). Now:LabelChangedPredicate), ownership, deletion, Ready flips, and generation bumps (keeps orphan blueprint re-vetting on spec edits);UnsafeDisableDeepCopywith deep-copy-on-write at the two mutation sites (launch-type backfill, orphan adoption) — same pattern the metrics collector already uses;SandboxTemplateRefHashhoisted out of the per-member loop;RequeueAftertimer instead of riding on pool churn (also closes a pre-existing gap where a quiet pool never re-checked the deadline).BenchmarkPoolMemberDeepCopy(2500 minimal members): 0.69 ms + 2.3 MB + 15k allocs per event — the informer-cache copy each pool event no longer pays (production members carry full pod specs, so the real saving is larger). The steady-state fake-harness pass is flat (−1.5%): the fake client copies regardless of the option, so the copy elimination doesn't show there by construction.4. Store Get / warm-candidate sweeps fan out per node (8c272d4)
GetandwarmCandidates(the node pick on every aggregated claim) walked the fleet sequentially with one full inventory decode per node;Listalready fanned out 16-way. Both now shareList's bounded fan-out (extracted asfanOutNodes), andGetcancels on first hit.Honest caveat: at the deployed 26-node scale the goroutine overhead costs ~15 µs per call — noise next to the ms-scale claim path, and the test double (typed DeepCopy) understates the win on the production path (reflection
FromUnstructuredper node).5. e2b single-id verbs stop paying a fleet List (5d640f7, c3daa08)
Every get/delete/pause/connect/fork/snapshot/metrics request materialized and sorted the entire fleet to match one id. The store now resolves a claim id per node with first-hit cancel and materializes only the match (
GetByClaimID, optional interface with the List fallback for other stores), andmatchesIDcompares the DNS-safe rendering in place instead of allocating a rendered string per scanned entry.6. v1alpha1 round-trip annotation slimmed (688ffd8)
The conversion webhook embedded a full serialized copy of the object in an annotation — near-doubling stored and wire size for every v1alpha1-touched Sandbox — to restore two replica fields. The annotation now carries exactly those fields; legacy full-object annotations still decode (pinned by test).
Gates
make lint(darwin + linux): 0 issues;asl ./...both GOOS: cleango test -race -count=1 ./...: passFollow-up: cluster acceptance on the testbed (poolbench warm-claim p50/p95 at concurrency 1/5/20 against a 2500 pool) to attribute the event-amplification fixes (#1, #3) on the measured 316 ms @ c=20 curve.