Skip to content

perf: reduce backend precompute engine CPU - #316

Merged
zzylol merged 1 commit into
mainfrom
perf/precompute-cpu
May 24, 2026
Merged

zzylol merged 1 commit into
mainfrom
perf/precompute-cpu

Conversation

@zzylol

@zzylol zzylol commented May 24, 2026

Copy link
Copy Markdown
Contributor

Path located

The asapquery-backend precompute/ingest path that consumes the agents' warm-tier OTLP output (delta Sums + sketch envelopes) and merges into the sketch store:

  • data_plane/src/drivers/ingest/otel.rs — OTLP gRPC/HTTP receiver; route_otlp_to_precompute + route_modified_otlp_sketches_to_precompute run per ingest batch.
  • data_plane/src/storage_engines/sketch_db/lifecycle/reconcile.rsreconcile_from_streaming_config, called from both ingest routines on every batch.
  • data_plane/src/storage_engines/sketch_db/index/mod.rsSketchStore + SketchInstanceMetadata (the cloned type).

Profiling method

Live perf on node2's running asap-backend (PID 429550, ~116% CPU under sustained agent load). perf was absent on node2; installed linux-tools-5.15.0-168-generic, then perf record -F 199 -g -p <pid> -- sleep 30 (6906 samples). The release binary symbolized cleanly.

Top hotspots (before)

All under reconcile_from_streaming_config -> SketchStore::snapshot_instances(), which deep-clones every SketchInstanceMetadata (String + BTreeSet + AggKind strings) on each ingest batch:

  • malloc 17.9% + cfree 5.7% (~23% allocation churn)
  • BTreeMap::clone::clone_subtree 19.6% (children)
  • SketchInstanceMetadata::clone 6.9% (children)
  • String::clone 5.0%
  • drop_in_place::<SketchInstanceMetadata> 2.1%
  • reconcile_from_streaming_config itself 2.0% self / drives the above

What changed

  1. Eliminate the per-reconcile catalog clone. New SketchStore::for_each_instance (lock-held visitor); reconcile now derives each sid's signature under the read lock into a reused scratch buffer, collects only u64 orphan sids, and retires them after the lock drops. No metadata clone, no per-sid signature Vec alloc.
  2. Skip reconcile when config is unchanged. The streaming config is an Arc<ArcSwap<StreamingConfig>> whose Arc identity only changes on a (rare) control-plane swap. New reconcile_if_config_changed gates on the Arc data pointer via an AtomicUsize on SketchStore, collapsing the steady-state per-batch reconcile to one relaxed atomic load.

Measured after

Criterion reconcile_per_batch (full un-gated scan, isolating change #1):

sids before after speedup
100 35.2 us 12.6 us 2.8x
1000 388 us 134 us 2.9x
10000 3.99 ms 1.45 ms 2.75x

Change #2 takes the steady-state per-batch cost to ~one atomic load (reconcile runs only on config swaps), removing this hotspot from the per-batch ingest path almost entirely.

Correctness

Reconcile only ever transitions Active->Retired on signature mismatch vs. the config (no time-based expiry — that stays in the eviction service), so an unchanged config yields identical results regardless of wall clock; a newly-minted sid matches a live signature by construction so it is never wrongly orphaned. Query answers are unaffected. All 9 reconcile lib tests pass (incl. 2 new gate tests + the HTTP config-swap path test); 228 sketch_db lib tests + edge-runtime adapter tests green.

Test plan

  • cargo test -p data_plane --lib reconcile
  • cargo test -p data_plane --lib sketch_db
  • cargo bench -p data_plane --bench sketch_db -- reconcile_per_batch
  • Re-profile live asap-backend to confirm the reconcile/clone frames are gone from the per-batch path.

Generated with Claude Code

The OTLP warm-tier ingest path ran `reconcile_from_streaming_config`
on *every* ingest batch, and that function deep-cloned every
`SketchInstanceMetadata` in the catalog via `snapshot_instances()`
(each carries a `String` + `BTreeSet<String>` + `AggKind` strings).
Live `perf` on node2's `asap-backend` showed this as the dominant
backend CPU cost: `BTreeMap::clone::clone_subtree` + `String::clone`
+ `SketchInstanceMetadata::clone` + drops, plus the malloc/free churn
they drive (~23% in malloc/cfree alone), all under
`reconcile_from_streaming_config`.

Two focused changes, both correctness-preserving:

1. Eliminate the per-reconcile catalog clone. Add
   `SketchStore::for_each_instance` (lock-held visitor) and rewrite
   reconcile to derive each sid's signature under the read lock into a
   reused scratch buffer, collecting only the `u64` orphan sids, then
   retiring them after the lock drops. No `SketchInstanceMetadata`
   clone, no per-sid signature `Vec` allocation.

2. Skip reconcile entirely when the config is unchanged. The streaming
   config is an `Arc<ArcSwap<StreamingConfig>>` whose `Arc` identity
   only changes on a (rare) control-plane swap. New
   `reconcile_if_config_changed` gates on the `Arc` data pointer via
   an `AtomicUsize` on `SketchStore`, collapsing the steady-state
   per-batch reconcile to one relaxed atomic load.

Measured (criterion `reconcile_per_batch`, full un-gated scan):
  100 sids:   35.2 us -> 12.6 us (2.8x)
  1000 sids:  388 us  -> 134 us  (2.9x)
  10000 sids: 3.99 ms -> 1.45 ms (2.75x)
The pointer gate additionally takes the steady-state per-batch cost
to ~one atomic load (reconcile runs only on config swaps).

Reconcile semantics are unchanged: it only ever transitions
Active->Retired on signature mismatch against the config (no
time-based expiry — that stays in the eviction service), so an
unchanged config produces identical results regardless of wall clock.
All 9 reconcile lib tests pass (incl. 2 new gate tests + the HTTP
config-swap path test); 228 sketch_db lib tests + edge-runtime
adapter tests green.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@zzylol
zzylol merged commit f365bfc into main May 24, 2026
@zzylol
zzylol deleted the perf/precompute-cpu branch July 17, 2026 20:06
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant