Skip to content

Commit a1d32aa

Browse files
committed
perf(wotoken-yield,es-address-yield): prefetch day rates in one parallel batch
The per-day pass awaited getR (previewRedeem / accRewardPerShare) sequentially — one RPC round-trip per transfer block and per day-end, the cold-run bottleneck. Now every block that needs a rate (transfers + each day-end, via the new dayEndBlocks helper) is fetched up front with a single Promise.all, which the RPC client coalesces into batched eth_calls. Rates are stored in the existing in-memory rCache Map and read back from it, so this holds one RPC per block per batch with or without the server RPC cache — it does not rely on RPC caching to avoid duplicate calls. Output-preserving: the ES launch window reproduces the exact same checksum (f3e9b04d…) as the pre-prefetch runs, zero errors.
1 parent 640f022 commit a1d32aa

3 files changed

Lines changed: 49 additions & 5 deletions

File tree

src/templates/exponential-staking/es-address-yield.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import * as abi from '@abi/exponential-staking'
22
import { ESAddressYield } from '@model'
33
import { Block, Context, EvmBatchProcessor, Processor, logFilter } from '@originprotocol/squid-utils'
4-
import { DayBoundaryCarry, forEachBlockByDay } from '@utils/for-each-block-by-day'
4+
import { DayBoundaryCarry, dayEndBlocks, forEachBlockByDay } from '@utils/for-each-block-by-day'
55

66
// xOGN is a MasterChef-style accumulator: previewRewards(user) == balanceOf(user) *
77
// (accRewardPerShare - rewardDebtPerShare(user)) / 1e12. So accRewardPerShare is scaled by 1e12 and a
@@ -146,6 +146,19 @@ export const createESAddressYieldProcessor = ({
146146
changedYieldRows.set(row.id, row)
147147
}
148148

149+
// Pre-warm the accRewardPerShare cache in one parallel batch (the RPC client coalesces these
150+
// into batched eth_calls): every block that needs R — those with a stake/unstake, plus each
151+
// day-end — is fetched concurrently so the sequential per-day pass below never blocks on a
152+
// round-trip.
153+
const rBlocks = new Map<number, Block>()
154+
for (const block of ctx.blocks) {
155+
if (block.logs.some((log) => stakeFilter.matches(log) || unstakeFilter.matches(log))) {
156+
rBlocks.set(block.header.height, block)
157+
}
158+
}
159+
for (const block of dayEndBlocks(ctx, dayCarry)) rBlocks.set(block.header.height, block)
160+
await Promise.all([...rBlocks.values()].map((block) => getR(block).catch(() => undefined)))
161+
149162
// Deterministic per-day accrual: stake/unstake checkpoint at exact event time; each day's pure
150163
// reward accrual is closed out once, at that day's true last block (`onDayEnd`), regardless of
151164
// how blocks are batched. At the chain head the current day is also swept for freshness.

src/templates/wotoken-yield/wotoken-yield.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import * as wotokenAbi from '@abi/woeth'
22
import { WOTokenAddressYield } from '@model'
33
import { Block, Context, EvmBatchProcessor, Processor, logFilter } from '@originprotocol/squid-utils'
44
import { ADDRESS_ZERO } from '@utils/addresses'
5-
import { DayBoundaryCarry, forEachBlockByDay } from '@utils/for-each-block-by-day'
5+
import { DayBoundaryCarry, dayEndBlocks, forEachBlockByDay } from '@utils/for-each-block-by-day'
66

77
const ONE = 10n ** 18n
88

@@ -174,6 +174,18 @@ export const createWOTokenYieldProcessor = ({
174174
changedYieldRows.set(row.id, row)
175175
}
176176

177+
// Pre-warm the rate cache in one parallel batch (the RPC client coalesces these into batched
178+
// eth_calls): every block that needs R — those with a wrapped transfer, plus each day-end — is
179+
// fetched concurrently so the sequential per-day pass below never blocks on a round-trip.
180+
// Best-effort — a pre-init revert on the empty vault is ignored and simply skipped by onDayEnd's
181+
// active-holder guard.
182+
const rBlocks = new Map<number, Block>()
183+
for (const block of ctx.blocks) {
184+
if (block.logs.some((log) => transferFilter.matches(log))) rBlocks.set(block.header.height, block)
185+
}
186+
for (const block of dayEndBlocks(ctx, dayCarry)) rBlocks.set(block.header.height, block)
187+
await Promise.all([...rBlocks.values()].map((block) => getR(block).catch(() => undefined)))
188+
177189
// Deterministic per-day accrual: transfers checkpoint at exact event time; each day's pure
178190
// share-appreciation is closed out once, at that day's true last block (`onDayEnd`), regardless
179191
// of how blocks are batched. At the chain head the current day is also swept for freshness.

src/utils/for-each-block-by-day.ts

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,23 @@ export interface DayBoundaryCarry {
2020
*
2121
* `carry` must persist across batches: declare it once at processor scope.
2222
*/
23+
/**
24+
* The blocks `forEachBlockByDay` will treat as day-ends this batch — pure, and does not mutate
25+
* `carry`. Use it to pre-warm any per-block RPC (e.g. an on-chain rate read) for all day-ends in one
26+
* parallel batch before the sequential per-day pass, so `onDayEnd` never blocks on a round-trip.
27+
*/
28+
export const dayEndBlocks = (ctx: Context, carry: DayBoundaryCarry): Block[] => {
29+
const blocks = ctx.blocks
30+
const ends: Block[] = []
31+
for (let i = 0; i < blocks.length; i++) {
32+
const prev = i === 0 ? carry.lastBlock : blocks[i - 1]
33+
if (prev && utcDate(prev) !== utcDate(blocks[i])) ends.push(prev)
34+
}
35+
// At the chain head the current (incomplete) day is swept at the latest block for live freshness.
36+
if (ctx.isHead && blocks.length > 0) ends.push(blocks[blocks.length - 1])
37+
return ends
38+
}
39+
2340
export const forEachBlockByDay = async (
2441
ctx: Context,
2542
carry: DayBoundaryCarry,
@@ -28,20 +45,22 @@ export const forEachBlockByDay = async (
2845
onDayEnd: (dayEndBlock: Block) => Promise<void>
2946
},
3047
) => {
48+
// Compute day-ends up front (shared with `dayEndBlocks` so prefetch and this loop never drift).
49+
const endHeights = new Set(dayEndBlocks(ctx, carry).map((b) => b.header.height))
3150
const blocks = ctx.blocks
3251
for (let i = 0; i < blocks.length; i++) {
3352
const block = blocks[i]
3453
const prev = i === 0 ? carry.lastBlock : blocks[i - 1]
3554
// A new UTC day has started, so `prev` was the previous day's last block — finalize it there.
36-
if (prev && utcDate(prev) !== utcDate(block)) {
55+
if (prev && endHeights.has(prev.header.height) && utcDate(prev) !== utcDate(block)) {
3756
await handlers.onDayEnd(prev)
3857
}
3958
await handlers.onBlock(block)
4059
}
4160
if (blocks.length > 0) {
4261
const tail = blocks[blocks.length - 1]
62+
// The head-tail day-end (current day, live freshness) has no following block to trigger it above.
63+
if (endHeights.has(tail.header.height)) await handlers.onDayEnd(tail)
4364
carry.lastBlock = tail
44-
// At the chain head the current day is incomplete; sweep the latest block for live freshness.
45-
if (ctx.isHead) await handlers.onDayEnd(tail)
4665
}
4766
}

0 commit comments

Comments
 (0)