Repository navigation
chore: stabilize fastConfirmation perf bench #9494
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -1,6 +1,5 @@ | ||||||||||||||
| import {bench, describe} from "@chainsafe/benchmark"; | ||||||||||||||
| import {SLOTS_PER_EPOCH} from "@lodestar/params"; | ||||||||||||||
| import {computeEpochAtSlot} from "@lodestar/state-transition"; | ||||||||||||||
| import {Slot} from "@lodestar/types"; | ||||||||||||||
| import { | ||||||||||||||
| buildFastConfirmationSnapshot, | ||||||||||||||
|
|
@@ -41,10 +40,8 @@ function runFCRRulesBenchmark(opts: Opts): void { | |||||||||||||
| const forkChoice = initializeForkChoice({...opts, fastConfirmation: true}); | ||||||||||||||
|
|
||||||||||||||
| const head = forkChoice.updateHead(); | ||||||||||||||
| const prevBlock = forkChoice.getBlockHexDefaultStatus(head.parentRoot); | ||||||||||||||
| if (!prevBlock) throw Error("no prevBlock"); | ||||||||||||||
|
|
||||||||||||||
| // Vote everyone for head | ||||||||||||||
| // Vote everyone for head so the FCR vote-map paths see a populated voteNextIndices. | ||||||||||||||
| everyoneVotes(head, forkChoice); | ||||||||||||||
|
|
||||||||||||||
| // Advance to second slot of epoch 2 (not epoch boundary) | ||||||||||||||
|
|
@@ -60,25 +57,17 @@ function runFCRRulesBenchmark(opts: Opts): void { | |||||||||||||
| const store = forkChoice["fcStore"] as unknown as IFastConfirmationStore; | ||||||||||||||
| if (!ctx) throw Error("fastConfirmationContext not initialized"); | ||||||||||||||
|
|
||||||||||||||
| return {forkChoice, ctx, store, head, prevBlock, currentVoteIs1: true, currentSlot}; | ||||||||||||||
| }, | ||||||||||||||
| beforeEach: (data) => { | ||||||||||||||
| // Flip votes to force fresh vote map computation each iteration | ||||||||||||||
| const target = data.currentVoteIs1 ? data.prevBlock : data.head; | ||||||||||||||
| everyoneVotes(target, data.forkChoice); | ||||||||||||||
| data.currentVoteIs1 = !data.currentVoteIs1; | ||||||||||||||
| data.currentSlot = (data.currentSlot + 1) as Slot; | ||||||||||||||
|
|
||||||||||||||
| // Advance the slot (this calls updateHead + processAttestationQueue but NOT the FCR) | ||||||||||||||
| // We need to advance the slot so getCurrentSlot() returns the right value | ||||||||||||||
| data.forkChoice["fcStore"].currentSlot = data.currentSlot; | ||||||||||||||
| // Apply votes to proto array so getHead/isDescendant work correctly | ||||||||||||||
| data.forkChoice.updateHead(); | ||||||||||||||
|
|
||||||||||||||
| return data; | ||||||||||||||
| return {ctx, store}; | ||||||||||||||
| }, | ||||||||||||||
| // Pass-through: the bench harness invokes fn(beforeEach(inputAll)), so without a | ||||||||||||||
| // beforeEach fn would receive undefined. store is not mutated by the rule runner so | ||||||||||||||
| // no per-iteration reset is required. | ||||||||||||||
| beforeEach: (data) => data, | ||||||||||||||
|
Comment on lines
61
to
+65
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The
Suggested change
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Pushing back on this one — checked const input = beforeEach ? await beforeEach(inputAll, runIdx) : undefined;When The comment is there precisely because the pass-through looks redundant at a glance but isn't. Suggestion to remove it would break the benchmark. cc @gemini-code-assist. @nazarhussain — leaving this one as-is, comment is accurate.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thank you for the clarification, @lodekeeper. You are absolutely correct; my previous assessment regarding the redundancy of the
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thanks for confirming.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Agree with @lodekeeper — keeping the pass-through. Confirmed in
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 👍 Thanks for confirming. Agreed — the |
||||||||||||||
| fn: ({ctx, store}) => { | ||||||||||||||
| // Measure ONLY the FCR rules — build cache + snapshot + run rules | ||||||||||||||
| // Measure ONLY the FCR rules — build cache + snapshot + run rules. | ||||||||||||||
| // store is not mutated by runFastConfirmationRules (only the rule runner's caller | ||||||||||||||
| // would assign store.confirmedRoot), so each iteration replays the same work and | ||||||||||||||
| // no beforeEach reset is needed. | ||||||||||||||
| const cache = createFastConfirmationCache(); | ||||||||||||||
| const snapshot = buildFastConfirmationSnapshot(ctx, store, cache); | ||||||||||||||
| runFastConfirmationRules(snapshot, ctx, store, cache); | ||||||||||||||
|
|
@@ -87,9 +76,11 @@ function runFCRRulesBenchmark(opts: Opts): void { | |||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| function everyoneVotes(vote: ProtoBlock, forkChoice: ForkChoice): void { | ||||||||||||||
| const nextEpoch = computeEpochAtSlot(vote.slot); | ||||||||||||||
| const nextRoot = vote.blockRoot; | ||||||||||||||
| for (let i = 0; i < forkChoice["balances"].length; i++) { | ||||||||||||||
| forkChoice["addLatestMessage"](i, nextEpoch, nextRoot, PayloadStatus.FULL); | ||||||||||||||
| // addLatestMessage's second arg is `nextSlot: Slot`, not an epoch — pass vote.slot directly so | ||||||||||||||
| // the internal computeEpochAtSlot(nextSlot) lands on the correct vote epoch instead of | ||||||||||||||
| // computeEpochAtSlot(epoch)=0 making every vote look 3 epochs stale. | ||||||||||||||
| forkChoice["addLatestMessage"](i, vote.slot, nextRoot, PayloadStatus.FULL); | ||||||||||||||
| } | ||||||||||||||
| } | ||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There is a correctness bug in the helper function
everyoneVotes(defined at the bottom of this file) that is called here.\n\nIneveryoneVotes,nextEpochis passed as the second argument toforkChoice["addLatestMessage"]:\ntypescript\nfunction everyoneVotes(vote: ProtoBlock, forkChoice: ForkChoice): void {\n const nextEpoch = computeEpochAtSlot(vote.slot);\n const nextRoot = vote.blockRoot;\n for (let i = 0; i < forkChoice["balances"].length; i++) {\n forkChoice["addLatestMessage"](i, nextEpoch, nextRoot, PayloadStatus.FULL);\n }\n}\n\n\nHowever,addLatestMessageexpects aSlotas its second parameter, not anEpoch:\ntypescript\n private addLatestMessage(\n validatorIndex: ValidatorIndex,\n nextSlot: Slot,\n nextRoot: RootHex,\n nextPayloadStatus: PayloadStatus\n ): void\n\n\nBecause an epoch number (e.g.,2) is passed instead of a slot number (e.g.,64),addLatestMessageinternally computes the epoch of the vote ascomputeEpochAtSlot(2), which evaluates to0.\n\nThis causes all validator votes to be registered as belonging to epoch 0 (instead of epoch 2). Since the benchmark advances the slot to epoch 3, these votes are treated as expired/inactive by the Fast Confirmation Rules (FCR), meaning the benchmark is not actually executing the realistic code paths where active votes are processed and counted.\n\n### Suggested Fix\nUpdateeveryoneVotesto passvote.slotinstead ofnextEpoch:\ntypescript\nfunction everyoneVotes(vote: ProtoBlock, forkChoice: ForkChoice): void {\n const nextSlot = vote.slot;\n const nextRoot = vote.blockRoot;\n for (let i = 0; i < forkChoice["balances"].length; i++) {\n forkChoice["addLatestMessage"](i, nextSlot, nextRoot, PayloadStatus.FULL);\n }\n}\nThere was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Confirming Gemini's analysis:
addLatestMessagesignature is(validatorIndex, nextSlot, ...)and internally callscomputeEpochAtSlot(nextSlot)to determine vote epoch. Withvc:600000 bc:96, the head'svote.slot=96→nextEpoch=3is passed asnextSlot=3, sovoteNextSlots[i]=3and the internal epoch resolves tocomputeEpochAtSlot(3)=0. AfterupdateTime(97)(epoch 3), all votes look like they belong to epoch 0 — i.e. stale by 3 epochs. The realistic path through the FCR vote-map paths is not being exercised.Fix is one line as Gemini suggested:
cc @gemini-code-assist — good catch. @nazarhussain — the GC-pressure removal still delivers the flakiness fix on its own, but this means the benchmarked code path isn't faithfully representing real validator state. Worth folding into this PR before merge so the bench numbers actually reflect FCR-active votes.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good catch — fixed in 99872b5.
vote.slotis now passed directly; the now-unusedcomputeEpochAtSlotimport was dropped. Verified via local bench run (still stable, ~80K–135K samples/config).Note: this is independent of the GC-pressure flakiness fix in the first commit —
fn()at currentSlot=97 short-circuits at Rule 4 (confirmedEpoch+1 < currentEpochafter Rule 2 resets to genesis) so it never reaches the vote-map path either way. But fixing the bug now means the future state-backed bench (planned follow-up) will start from a correct setup.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍 Confirmed —
99872b50camatches what Gemini and I flagged:vote.slotis passed straight toaddLatestMessageand the now-unusedcomputeEpochAtSlotimport is gone. Thanks for closing it out.