Skip to content
This repository was archived by the owner on Aug 11, 2026. It is now read-only.

perf: project only the KOTH detail keys the retest path reads - #675

Closed
Peyton-Spencer wants to merge 2 commits into
perf/allocator-floor-skip-detailsfrom
perf/koth-narrow-details-projection
Closed

perf: project only the KOTH detail keys the retest path reads#675
Peyton-Spencer wants to merge 2 commits into
perf/allocator-floor-skip-detailsfrom
perf/koth-narrow-details-projection

Conversation

@Peyton-Spencer

Copy link
Copy Markdown
Contributor

Summary

Third in the live hotfix stack (on top of #674, which is on #673). This is the one the profiler actually pointed at.

_current_koth_entries selected every eligible agent's entire score telemetry blob and read exactly three fields out of it: composite_stderr (via _ledger_stderr) plus the legacy confirmation_seeds/confirmation_composites pair. That blob averages ~22KB a row on bench v8, across every eligible agent.

Only the retest path pays for it. _current_koth_entries is reached solely from _current_emission_set and _current_retest_cohort; the canonical /validator/job lane never calls it. That is precisely the asymmetry seen in production — new submissions were being scored normally while the continual-retest lane crawled.

Measured on the live prod worker

py-spy record, 2383 samples over 30s, --nonblocking:

  • 41% of CPU inside asyncpg's JSONB decoderraw_decode the top leaf frame by a wide margin
  • /validator/top5-confirmation-job slowest request: 151,884ms — 152 seconds. Validators are timing out before a ticket returns, which is why the lane looks dead rather than slow.
  • /validator/job slowest over the same period: 20-40s
  • the decode is charged to asyncpg's event-loop callback, not the awaiting coroutine — which is why no request-level metric ever attributed it, and why an earlier guess at the wrong caller (perf: stop hydrating score telemetry in the allocator floor read #674) produced no measurable change

The change

list_eligible_ledger(details_keys=(...)) has Postgres build a details object from only the named keys, so the remainder never detoasts, never crosses the wire, and never reaches the JSON decoder. Absent keys arrive as None, which all three readers already treat as absent.

Same class of fix as the existing include_details=False, but for a consumer that needs part of the blob rather than none of it. _KOTH_DETAIL_KEYS sits directly above the function that reads them, with a comment tying the tuple to its readers.

Validation

  • uv run pytest ditto/tests/api_server/endpoints/ -q1150 passed
  • uv run pytest ditto/tests/db/queries/ ditto/tests/api_server/endpoints/test_validator.py -q850 passed, 4 skipped
  • ruff check / ruff format --check / mypy → clean

The validator endpoint suite covers the KOTH and retest paths, so stderr and legacy-confirmation behaviour are exercised against the narrowed projection. New tests pin that only requested keys are returned and that an absent key arrives as None.

Honest scope

This attacks per-request cost on the retest path. It does not reduce the number of allocator recomputations — subnet#388's batched claim contract is still unimplemented and is still the structural fix. Whether 152s becomes acceptable or merely better is an empirical question; I'll post before/after from the same profiler after deploy.

_current_koth_entries selected every eligible agent's whole score telemetry
blob and read three fields out of it: composite_stderr (via _ledger_stderr)
and the legacy confirmation_seeds/confirmation_composites pair. The blob
averages ~22KB a row on bench v8.

Only the retest path pays this. _current_koth_entries is reached solely
from _current_emission_set and _current_retest_cohort, so the canonical
/validator/job lane never touches it -- which is exactly the asymmetry
observed in production: new submissions were being scored normally while
the continual-retest lane crawled.

Measured on the live prod worker with py-spy (2383 samples, 30s):

  - 41% of CPU inside asyncpg's JSONB decoder (raw_decode the top leaf)
  - /top5-confirmation-job slowest request 151,884ms -- 152 seconds
  - /validator/job slowest 20-40s over the same period
  - the decode is charged to the event-loop callback, not the awaiting
    coroutine, which is why no request-level metric ever showed it

Add `details_keys=` to list_eligible_ledger: Postgres builds a details
object from only the named keys, so the rest never detoasts, never crosses
the wire, and never reaches the JSON decoder. Absent keys arrive as None,
which every reader of these three already treats as absent.

This is the same class of fix as the include_details=False pair, but for a
consumer that genuinely needs part of the blob rather than none of it.

Refs ditto-assistant/ditto-subnet#388.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
CI typechecks the tree; LedgerRow.details is dict | None, so the absent-key
test needed the same isinstance narrowing its sibling already had.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@Peyton-Spencer

Copy link
Copy Markdown
Contributor Author

Closing unmerged: superseded by the monorepo cutover.

Production is now served from ditto-assistant/ditto-subnet (/opt/ditto-subnet/apps/platform), so this repository no longer deploys anywhere. This PR's content already landed in the monorepo and is live:

Merging here would land the fix on a branch nothing ships, which is worse than closing: it makes this repo look maintained and invites a deploy from it. See ditto-subnet#566 for the cutover follow-ups.

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant