Skip to content

perf(kernel): read every candidate's object state in one statement and each digest's facts once - #208

Merged
ahrav merged 5 commits into
mainfrom
perf/kr-11-batch-egress-candidates
Sep 4, 2026
Merged

perf(kernel): read every candidate's object state in one statement and each digest's facts once#208
ahrav merged 5 commits into
mainfrom
perf/kr-11-batch-egress-candidates

Conversation

@ahrav

@ahrav ahrav commented Sep 4, 2026

Copy link
Copy Markdown
Owner

egress_candidates ran load_object_state per candidate and egress_facts_tx per candidate even when candidates shared a digest, so a 1024-candidate batch executed over two thousand statements after the served-rows query. load_object_states reads the whole id list with one json_each query (duplicate candidate ids get the same state, as before), and egress facts are memoized per distinct digest within the batch.

Measured (marginal): eligibility cold 1 77 → 69 µs, 64 695 → 640 µs, 1024 10.9 → 10.2 ms; warm unchanged. Cumulative cold 4.6×/3.5×/3.3×; 6 improved, 0 regressed.

Stack (11/13): base perf/kr-10-page-single-hop → head perf/kr-11-batch-egress-candidates.

Method. Numbers come from crates/mc-module/benches/kernel_routes.rs (added in the first PR of this stack): a baseline binary and a candidate binary run as separate processes in 10 ABBA blocks pinned to one core; each cell is median_baseline / median_candidate with a paired bootstrap 95% CI over the blocks. Baseline is the suite commit on stack/kernel-routes-03-routes before any optimization; ratios are therefore cumulative through this PR, and the marginal effect of this PR alone is called out. A/A control on the same host: all CIs include 1.0, half-widths about 1%. Host: AMD EPYC 9R14, SQLite 3.51.3 (bundled).

Guard. cargo test -p mc-module --features test-support --test kernel_routes (45), cargo test -p mc-kernel --features test-support (all suites), cargo clippy -p mc-module -p mc-kernel --all-targets -- -D warnings. No file under tests/ changed; the wire contract, receipts, and serving semantics are unchanged.

…ment and each digest's egress facts once

egress_candidates ran load_object_state per candidate and egress_facts_tx per
candidate even when candidates shared a digest, so a 1024-candidate batch
executed over two thousand statements after the served-rows query.
@coderabbitai

coderabbitai Bot commented Sep 4, 2026

Copy link
Copy Markdown

Warning

Review limit reached

Next included review available in 8 minutes.

Check out review usage here.

View limit details

Limit details: You’ve used the included review currently available. Your 108 included PR review attempts over the past 7 days set your current allowance at 1 review per hour.

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

Learn how review limits work.

Review configuration:

⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Essentials

Run ID: a24fbd6a-8355-48af-bb07-0970b1cacb9d

📥 Commits

Reviewing files that changed from the base of the PR and between 989848f and 14ba722.

📒 Files selected for processing (3)
  • crates/mc-kernel/src/admission.rs
  • crates/mc-kernel/src/envelope.rs
  • crates/mc-kernel/tests/kernel_cas.rs

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

@claude

claude Bot commented Sep 4, 2026

Copy link
Copy Markdown

Reviewed the diff. This is a clean, well-scoped perf change — batching load_object_state into load_object_states (one IN (json_each(...)) query for the whole candidate list) and memoizing egress_facts_tx per distinct digest within egress_candidates.

Checked closely and didn't find correctness issues:

  • Column layout in the new load_object_states query is copy-identical to load_object_state (OBJECT_ROW_COLUMNS + 3 extra columns → indices 10/11/12 line up with object_row_from + the two extra row.get calls).
  • ObjectState derives Clone and ArtifactEgressFacts derives Copy, so the new .cloned() / *facts reuse is sound — no unnecessary allocation surprises.
  • Duplicate object ids in the IN (json_each(...)) subquery don't produce duplicate rows (membership test, not a join), so the HashMap<String, ObjectState> keyed by object_id is correctly deduped; ids absent from the registry are correctly absent from the map, which the existing never-written case in eligibility_verdicts_cover_every_class_and_cache_per_incarnation_and_tip already exercises.
  • The digest memoization (HashMap<&str, ArtifactEgressFacts>) is a pure function of (digest, destination) per call, so reusing a cached value across candidates sharing a digest can't diverge from calling egress_facts_tx per-candidate.
  • The one incidental change (query_rowquery_row_cached for the tip lookup) matches the file's existing convention (used everywhere else in admission.rs).

Minor/non-blocking note: the benchmark added in the stack's first PR exercises many candidates sharing one digest (the scenario this PR optimizes for), but the correctness test suite doesn't have a case with several candidates sharing the same artifact_digest in one kernel.eligibility.batch call — worth a small test if you want explicit coverage of the memoization path, though the logic is safe by construction since the cached value doesn't depend on which candidate triggered the fetch.

I wasn't able to run cargo test/clippy in this sandbox (Bash tool required approval that wasn't available), so please confirm the guard commands from the PR description pass locally.

@ahrav
ahrav marked this pull request as ready for review September 4, 2026 20:34
…e and batch readers

load_object_state and load_object_states carried the same SELECT list, joins,
and column positions; one helper now builds the SQL and one mapper reads the
row, so the token-check and egress paths cannot drift apart.
Comment on lines +2246 to +2249
// One registry read for the batch, and one egress-facts read per
// distinct digest, instead of one of each per candidate.
let states = load_object_states(tx, &ids)?;
let mut artifacts: HashMap<&str, ArtifactEgressFacts> = HashMap::new();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The correctness of this batch depends on two dedup assumptions holding for every caller: (1) load_object_states collapsing duplicate object_ids to a single state is safe because o.object_id is unique in object_registry, and (2) memoizing ArtifactEgressFacts by digest is safe because destination is fixed for the whole call. Both look right from reading the schema/signature, but I don't see a test at this level (or in kernel_routes) that exercises a batch with a repeated object_id or repeated digest and asserts the per-candidate results still line up correctly with their index. Given this is exactly the case the PR is optimizing for (candidates sharing a digest), it'd be worth a small regression test here so a future change to either dedup path fails loudly instead of only showing up as a perf regression.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 1ddb997. Confirmed the gap: the only batch test (eligibility_verdicts_cover_every_class_and_cache_per_incarnation_and_tip) uses 11 distinct ids and a single digest, so neither dedup path was exercised.

Added egress_candidates_line_up_by_index_when_ids_and_digests_repeat in crates/mc-kernel/tests/kernel_cas.rs. It builds a 7-candidate batch with a repeated object id (naming two different digests), a repeated digest (named by a live object and by a never-written id), a repeated unknown digest, and a None digest, then for both destinations asserts each batch entry equals a single-candidate egress_candidates read at the same index, plus that repeated ids/digests share one answer and distinct digests do not.

Sanity-checked the oracle by mutating the memo to return any cached facts regardless of digest; the test fails on the batch[1].artifact == batch[2].artifact / assert_ne!(batch[0].artifact, batch[1].artifact) pair.

@claude

claude Bot commented Sep 4, 2026

Copy link
Copy Markdown

Reviewed the diff (admission.rs + envelope.rs).

Summary: Solid, well-scoped perf change. load_object_states reads all candidate states with a single IN (SELECT value FROM json_each(?1)) query instead of N load_object_state calls, and egress_facts_tx results are memoized per distinct digest within the batch. I traced through the correctness-sensitive parts:

  • No SQL injection risk: the where_clause passed into object_state_sql is one of two hardcoded literals (o.object_id=?1 or the json_each IN clause) — never built from candidate data. The actual ids are bound as a parameter (?1), not interpolated.
  • Dedup is safe for load_object_states: object_id is the registry's primary key, so IN (...) with duplicate ids in the JSON array can't produce duplicate/cartesian rows; the HashMap<String, ObjectState> construction naturally collapses repeats to the same state, matching what N individual load_object_state calls against the same snapshot would have returned.
  • Digest memoization is safe: destination is fixed for the whole egress_candidates call, so keying the artifacts cache by digest alone (not (digest, destination)) is correct. ArtifactEgressFacts is Copy, so *facts on a cache hit is cheap and doesn't fight the borrow checker.
  • Validation order preserved: the is_artifact_digest guard still runs per-candidate before the memo lookup, so an invalid digest on any candidate still fails that candidate the same way it did before.
  • Both load_object_state and load_object_states now share object_state_sql/object_state_from, removing the duplicated column list — nice cleanup alongside the perf win.

Minor, non-blocking: left one inline note — I didn't find a test that exercises a batch with a repeated object_id or repeated digest and asserts per-candidate results still line up by index. The logic looks correct by inspection (dedup shouldn't change results, only avoid redundant reads), but that's exactly the scenario this PR optimizes for, so a small regression test would guard against a future change silently breaking it.

No security or behavioral issues found; changes are internal to mc-kernel and don't touch the wire contract.

Base automatically changed from perf/kr-10-page-single-hop to main September 4, 2026 21:59
Resolve the conflict in load_object_state: main hoisted its query text into a
LazyLock, while this branch factored the text into object_state_sql shared with
load_object_states. Keep the shared helper and build each variant once behind
a LazyLock so both readers avoid the per-call format! allocation.
@claude

claude Bot commented Sep 4, 2026

Copy link
Copy Markdown

Review summary

This is a clean, well-scoped perf change. I traced the logic in egress_candidates (crates/mc-kernel/src/admission.rs:2222) and the new load_object_states/object_state_sql/object_state_from helpers (crates/mc-kernel/src/envelope.rs:1369-1434) carefully and didn't find correctness bugs:

  • Column indices preserved: object_state_from reads columns 10/11/12 after the shared 10-column OBJECT_ROW_COLUMNS prefix, matching the original single-object query exactly.
  • Batched IN (SELECT value FROM json_each(?1)) lookup is safe from SQL injection (bound as a single JSON parameter, not interpolated) and correctly dedupes: duplicate candidate ids just filter to the same row, so the resulting HashMap<String, ObjectState> has one entry per distinct id — matches the doc comment's claim.
  • Digest memoization (artifacts: HashMap<&str, ArtifactEgressFacts>) is keyed only by digest, which is safe here because destination is a single fixed value for the whole batch call — ArtifactEgressFacts is Copy, so *facts is cheap and correct.
  • The invalid-digest guard (!is_artifact_digest) still short-circuits before the memoized fetch, so error semantics for malformed digests are unchanged.
  • No wire/serving semantics changed; this is a pure internal batching refactor.

One minor nitpick left inline (non-blocking) about avoiding an extra Vec allocation in load_object_states, since the PR is specifically about cutting overhead in this hot path.

Nice, thorough benchmarking methodology in the description (paired bootstrap CI, A/A control). No security concerns — no new untrusted-input paths, and the batched query still uses parameter binding throughout.

Comment thread crates/mc-kernel/src/envelope.rs Outdated
Comment on lines +1425 to +1433
let states = statement
.query_map([ids], object_state_from)
.map_err(map_sqlite)?
.collect::<rusqlite::Result<Vec<_>>>()
.map_err(map_sqlite)?;
Ok(states
.into_iter()
.map(|state| (state.object.object_id.clone(), state))
.collect())

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor: this collects into an intermediate Vec<ObjectState> and then rebuilds a HashMap from it, which allocates twice for a batch that can be 1024 items. Since the goal of this PR is cutting per-batch overhead, collecting directly into the map avoids the extra pass:

Suggested change
let states = statement
.query_map([ids], object_state_from)
.map_err(map_sqlite)?
.collect::<rusqlite::Result<Vec<_>>>()
.map_err(map_sqlite)?;
Ok(states
.into_iter()
.map(|state| (state.object.object_id.clone(), state))
.collect())
statement
.query_map([ids], object_state_from)
.map_err(map_sqlite)?
.map(|row| row.map(|state| (state.object.object_id.clone(), state)))
.collect::<rusqlite::Result<HashMap<_, _>>>()
.map_err(map_sqlite)

Non-blocking — the savings are small relative to the statement-count win this PR already delivers.

@ahrav ahrav Sep 4, 2026

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 14ba722. One note on the suggestion as written: returning the chain directly as the tail expression fails to compile (statement does not live long enough — the Rows temporary outlives the statement), so the result is bound to a local first and then returned. Same single-pass collect into HashMap, no intermediate Vec.

…d digests repeat

egress_candidates now reads every object's state in one statement and each
digest's egress facts once. Nothing exercised a batch that repeats an object
id or a digest, so a change to either dedup path would only show up as a
perf regression. Compare each batch entry with a single-candidate read at the
same index, and check that repeated ids and digests share one answer while
distinct digests keep distinct facts.
@ahrav

ahrav commented Sep 4, 2026

Copy link
Copy Markdown
Owner Author

Follow-up on the two review passes:

  • Test coverage for repeated ids / digests (raised in both passes and inline): added in 1ddb997, egress_candidates_line_up_by_index_when_ids_and_digests_repeat in crates/mc-kernel/tests/kernel_cas.rs. Details in the inline reply.
  • Guard commands: run locally on the merged head. cargo fmt --all -- --check, cargo clippy -p mc-kernel --all-targets -D warnings, cargo test -p mc-kernel, and cargo nextest run -p mc-module --test kernel_routes (53/53) all pass.
  • Merge conflict with main: resolved in 479c8f6. Main had hoisted load_object_state's query text into a LazyLock<String> (#40a58783e); this branch had factored the text into object_state_sql/object_state_from shared with load_object_states. Kept the shared helper and put each where_clause variant behind its own LazyLock so both readers keep main's no-allocation-per-call property.

load_object_states collected every row into a Vec and then rebuilt a HashMap
from it, two allocations for a batch that can hold 1024 states. Collect into
the map in one pass.
@claude

claude Bot commented Sep 4, 2026

Copy link
Copy Markdown

Reviewed the diff (admission.rs, envelope.rs, kernel_cas.rs). No correctness, security, or performance issues found.

Notes from the review:

  • Correctness: load_object_states uses object_id IN (SELECT value FROM json_each(?1)), which is a membership filter, so duplicate ids in the batch don't produce duplicate rows — the HashMap<String, ObjectState> keying is safe. The per-digest artifacts memoization is sound because egress_facts_tx is a pure function of (tx snapshot, digest, destination), so caching by digest within one transaction can't diverge from a fresh read.
  • Test coverage: egress_candidates_line_up_by_index_when_ids_and_digests_repeat is a good regression guard — it directly compares batched output against one-candidate-at-a-time reads at each index, for both dedup paths (repeated object id, repeated digest) and the not-found cases (unknown object, unknown digest, no digest). This is exactly the kind of test that would have caught a dedup bug the described "measured" perf work could otherwise hide.
  • Security: the ids JSON array is built via serde_json::to_string and passed as a bound parameter (?1), not string-interpolated into SQL, so there's no injection risk from object ids containing quotes/special characters.
  • Style/refactor: factoring object_state_sql/object_state_from out of load_object_state so load_object_states shares the exact same projection is a good move — keeps the token-check and batch-egress paths from silently drifting apart on column list or join semantics.

One very minor, non-blocking observation: ids (built once in egress_candidates) can contain duplicate object ids when candidates repeat ids, which are then round-tripped through json_each on every call. Harmless correctness-wise (confirmed by the new test) and dwarfed by the win from batching in the first place — not worth acting on unless a future workload has very high duplication in a single batch.

@ahrav

ahrav commented Sep 4, 2026

Copy link
Copy Markdown
Owner Author

Re the duplicate-ids observation: agreed it's harmless, and leaving it as-is on purpose. Deduping before serializing would add a set allocation and a hash pass on every call to shave a few bytes off the bound JSON parameter, and the new test pins the semantics either way. Not changing it unless a workload shows high intra-batch duplication.

@ahrav
ahrav merged commit d0adec8 into main Sep 4, 2026
17 of 20 checks passed
@ahrav
ahrav deleted the perf/kr-11-batch-egress-candidates branch September 4, 2026 22:24
@kilo-code-bot

kilo-code-bot Bot commented Sep 4, 2026

Copy link
Copy Markdown

Code Review Summary

Status: No Issues Found | Recommendation: Merge

Files Reviewed (3 files)
  • crates/mc-kernel/src/admission.rs
  • crates/mc-kernel/src/envelope.rs
  • crates/mc-kernel/tests/kernel_cas.rs

Reviewed by gemini-3.8-flash · Input: 185.8K · Output: 24.3K · Cached: 1.2M

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