diff --git a/packages/state-transition/test/perf/epoch/processPendingDeposits.test.ts b/packages/state-transition/test/perf/epoch/processPendingDeposits.test.ts new file mode 100644 index 000000000000..a911cba0dd6a --- /dev/null +++ b/packages/state-transition/test/perf/epoch/processPendingDeposits.test.ts @@ -0,0 +1,83 @@ +import {bench, describe} from "@chainsafe/benchmark"; +import {ContainerNodeStructType, ContainerType, ListCompositeType} from "@chainsafe/ssz"; +import {PENDING_DEPOSITS_LIMIT} from "@lodestar/params"; +import {ssz} from "@lodestar/types"; + +// PERF: Cost is O(pendingDeposits.length). In the worst case (large deposit queue after a big network event), +// this can be 50_000 items. Each item requires reading all 5 fields. +// +// Benchmarks ContainerType (current) vs ContainerNodeStructType (proposed) for field access performance. +// ContainerNodeStructType stores items as plain JS objects, avoiding tree traversal on every field read. + +const NUM_DEPOSITS = 50_000; +const CHUNK = 100; + +// Reuse the same field types as the existing PendingDeposit SSZ type +const fields = ssz.electra.PendingDeposit.fields; + +const PendingDepositContainer = new ContainerType(fields, {typeName: "PendingDeposit", jsonCase: "eth2"}); +const PendingDepositNodeStruct = new ContainerNodeStructType(fields, {typeName: "PendingDeposit", jsonCase: "eth2"}); + +const ListContainer = new ListCompositeType(PendingDepositContainer, PENDING_DEPOSITS_LIMIT); +const ListNodeStruct = new ListCompositeType(PendingDepositNodeStruct, PENDING_DEPOSITS_LIMIT); + +function buildList(listType: typeof ListContainer): ReturnType; +function buildList(listType: typeof ListNodeStruct): ReturnType; +function buildList(listType: typeof ListContainer | typeof ListNodeStruct) { + const view = listType.defaultViewDU(); + const defaultDeposit = ssz.electra.PendingDeposit.defaultValue(); + for (let i = 0; i < NUM_DEPOSITS; i++) { + if (listType === ListContainer) { + view.push(PendingDepositContainer.toViewDU(defaultDeposit)); + } else { + view.push(PendingDepositNodeStruct.toViewDU(defaultDeposit)); + } + } + view.commit(); + return view; +} + +describe.skip(`processPendingDeposits - iterate ${NUM_DEPOSITS} deposits, access all fields`, () => { + const containerListView = buildList(ListContainer); + const nodeStructListView = buildList(ListNodeStruct); + + bench({ + id: `ContainerType - getReadonlyByRange chunk=${CHUNK}`, + yieldEventLoopAfterEach: true, + fn: () => { + let sum = 0; + for (let i = 0; i < NUM_DEPOSITS; i += CHUNK) { + const deposits = containerListView.getReadonlyByRange(i, CHUNK); + for (const deposit of deposits) { + sum += deposit.amount + deposit.slot; + void deposit.pubkey; + void deposit.withdrawalCredentials; + void deposit.signature; + } + } + if (sum === Number.MIN_SAFE_INTEGER) { + throw new Error("unreachable"); + } + }, + }); + + bench({ + id: `ContainerNodeStructType - getReadonlyByRange chunk=${CHUNK}`, + yieldEventLoopAfterEach: true, + fn: () => { + let sum = 0; + for (let i = 0; i < NUM_DEPOSITS; i += CHUNK) { + const deposits = nodeStructListView.getReadonlyByRange(i, CHUNK); + for (const deposit of deposits) { + sum += deposit.amount + deposit.slot; + void deposit.pubkey; + void deposit.withdrawalCredentials; + void deposit.signature; + } + } + if (sum === Number.MIN_SAFE_INTEGER) { + throw new Error("unreachable"); + } + }, + }); +}); diff --git a/packages/types/src/electra/sszTypes.ts b/packages/types/src/electra/sszTypes.ts index 9176fb7c76e4..d98435171eb8 100644 --- a/packages/types/src/electra/sszTypes.ts +++ b/packages/types/src/electra/sszTypes.ts @@ -1,6 +1,7 @@ import { BitListType, BitVectorType, + ContainerNodeStructType, ContainerType, ListBasicType, ListCompositeType, @@ -270,7 +271,7 @@ export const SignedBuilderBid = new ContainerType( {typeName: "SignedBuilderBid", jsonCase: "eth2"} ); -export const PendingDeposit = new ContainerType( +export const PendingDeposit = new ContainerNodeStructType( { pubkey: BLSPubkey, withdrawalCredentials: Bytes32, @@ -285,7 +286,7 @@ export const PendingDeposit = new ContainerType( export const PendingDeposits = new ListCompositeType(PendingDeposit, PENDING_DEPOSITS_LIMIT); -export const PendingPartialWithdrawal = new ContainerType( +export const PendingPartialWithdrawal = new ContainerNodeStructType( { validatorIndex: ValidatorIndex, amount: Gwei, @@ -299,7 +300,7 @@ export const PendingPartialWithdrawals = new ListCompositeType( PENDING_PARTIAL_WITHDRAWALS_LIMIT ); -export const PendingConsolidation = new ContainerType( +export const PendingConsolidation = new ContainerNodeStructType( { sourceIndex: ValidatorIndex, targetIndex: ValidatorIndex,