Skip to content

VxAdmin: Remove per-write-in query from write-in tabulation - #9312

Open
adghayes wants to merge 1 commit into
admin/tabulation-perffrom
admin/write-in-tally-perf
Open

adghayes wants to merge 1 commit into
admin/tabulation-perffrom
admin/write-in-tally-perf

Conversation

@adghayes

@adghayes adghayes commented Sep 7, 2026

Copy link
Copy Markdown
Collaborator

Overview

Stacked on #9311.

addWriteInToElectionWriteInSummary called store.getCastVoteRecords({ cvrId })
for every valid write-in, just to learn one contest's vote count and
whether the ballot had a crossover vote. Each call rebuilt the filter SQL
(which re-read and zod-parsed system settings), re-prepared two statements,
and parsed both JSON blobs. Because the CVR tabulation pass is memoized but
the write-in pass is not, this N+1 was most of what a repeat tally-report
request actually paid for.

getWriteInsForTallies now always selects votes/adjudicated_votes (the
combined-primary branch already did) and carries the CVR's effective votes on
WriteInForTally, so the summary builder needs no store access. The
now-unused cvrId point-lookup parameter on getCastVoteRecords is removed.
Output is identical.

Related to the scale testing in #9063.

Benchmark

Same harness as #9311: real Store (in-memory SQLite) +
tabulateElectionResults, NH test-ballot fixture, 50k HMPB CVRs, 13.3k
write-ins, grouped by precinct × voting method,
includeWriteInAdjudicationResults. Dev VM (arm64, 3 vCPU). "Before" is
#9311's tip; results hash identical before/after.

Path Before (#9311) After
tabulateWriteInTallies (grouped) ~830 ms ~205 ms
tabulateElectionResults, cache miss 1284 ms 665 ms
tabulateElectionResults, cache hit ~830 ms ~200 ms

So we're getting 1.5x speed improvement with no write-ins, >2x speed improvement with heavy write-ins.

Demo Video or Screenshot

N/A (no UI change).

Testing Plan

  • Existing tests all pass unchanged
  • Benchmarked

Checklist

  • I have prefixed my PR title with "VxDesign: ", "VxPollBook: ", or "HWTA: " if my change is specific to one of those products.
  • I have added logging where appropriate for any new user actions.
  • I have added the "user-facing-change" label to this PR, if relevant, to automate an announcement in #machine-product-updates.

🤖 Generated with Claude Code

`addWriteInToElectionWriteInSummary` called `store.getCastVoteRecords({ cvrId })`
for every non-invalid write-in to learn one contest's vote count. Each call
rebuilt the filter SQL (re-reading and zod-parsing system settings),
re-prepared two statements, and parsed both JSON blobs.
`getWriteInsForTallies` now always selects `votes`/`adjudicated_votes` (the
combined-primary branch already did) and carries the CVR's effective votes
on `WriteInForTally`. The now-unused `cvrId` lookup on `getCastVoteRecords`
is removed.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
}): Tabulation.ElectionWriteInSummary {
const {
contestId,
cvrId,

@adghayes adghayes Sep 7, 2026

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

We avoid hitting the store here by having the votes already on the write-in object.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

It may seem like object sprawl to have the votes on the write-in objects, but I think we've learned through these various tabulation cases (overvotes for write-ins, crossover, combined ballot) that we need that votes context to make sense of the write-in. In the DB query we're already doing the join.

electionId,
election,
filter,
cvrId,

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Optional argument made unnecessary by removing only consumer.

Comment on lines -3017 to -3021
if (isCombinedBallotPrimary(election)) {
selectParts.push('cvrs.votes as votes');
selectParts.push('cvrs.adjudicated_votes as adjudicatedVotes');
} else {
selectParts.push('ballot_styles.party_id as partyId');

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Previously we'd only pull the votes here if it was a combined ballot primary, because we needed them to infer the party of the ballot (rather than, in the else branch, it just being evident from the ballot style). Since downstream we're fetching the votes anyway, it is cleaner to just always fetch the votes and always parse them here (see below).

@adghayes
adghayes marked this pull request as ready for review September 7, 2026 23:53
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