tapimo 0.8.0 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/App.d.ts +7410 -7613
- package/dist/App.d.ts.map +1 -1
- package/dist/App.js +0 -1
- package/dist/App.js.map +1 -1
- package/dist/Webhooks.d.ts +21 -20
- package/dist/Webhooks.d.ts.map +1 -1
- package/dist/Webhooks.js +66 -23
- package/dist/Webhooks.js.map +1 -1
- package/dist/admin/apps/verified-tokens.d.ts +74 -74
- package/dist/apps/Zones.d.ts +34 -22
- package/dist/apps/Zones.d.ts.map +1 -1
- package/dist/apps/Zones.js +9 -8
- package/dist/apps/Zones.js.map +1 -1
- package/dist/apps/data/App.d.ts +3120 -2782
- package/dist/apps/data/App.d.ts.map +1 -1
- package/dist/apps/data/FxOracle.d.ts +4 -5
- package/dist/apps/data/FxOracle.d.ts.map +1 -1
- package/dist/apps/data/FxOracle.js +19 -4
- package/dist/apps/data/FxOracle.js.map +1 -1
- package/dist/apps/data/routes/activities.d.ts.map +1 -1
- package/dist/apps/data/routes/activities.js +32 -27
- package/dist/apps/data/routes/activities.js.map +1 -1
- package/dist/apps/data/routes/addresses.d.ts +283 -52
- package/dist/apps/data/routes/addresses.d.ts.map +1 -1
- package/dist/apps/data/routes/balances.d.ts +291 -21
- package/dist/apps/data/routes/balances.d.ts.map +1 -1
- package/dist/apps/data/routes/balances.js +232 -66
- package/dist/apps/data/routes/balances.js.map +1 -1
- package/dist/apps/data/routes/blocks.d.ts +4 -4
- package/dist/apps/data/routes/coingecko.d.ts +54 -54
- package/dist/apps/data/routes/earn.d.ts +642 -126
- package/dist/apps/data/routes/earn.d.ts.map +1 -1
- package/dist/apps/data/routes/earn.js +627 -58
- package/dist/apps/data/routes/earn.js.map +1 -1
- package/dist/apps/data/routes/exchanges.d.ts +120 -120
- package/dist/apps/data/routes/exchanges.d.ts.map +1 -1
- package/dist/apps/data/routes/exchanges.js +114 -118
- package/dist/apps/data/routes/exchanges.js.map +1 -1
- package/dist/apps/data/routes/receipts.d.ts.map +1 -1
- package/dist/apps/data/routes/receipts.js +68 -47
- package/dist/apps/data/routes/receipts.js.map +1 -1
- package/dist/apps/data/routes/tokens.d.ts +2 -0
- package/dist/apps/data/routes/tokens.d.ts.map +1 -1
- package/dist/apps/data/routes/transactions.d.ts +59 -51
- package/dist/apps/data/routes/transactions.d.ts.map +1 -1
- package/dist/apps/data/routes/transactions.js +25 -23
- package/dist/apps/data/routes/transactions.js.map +1 -1
- package/dist/apps/data/routes/transfers.d.ts +40 -18
- package/dist/apps/data/routes/transfers.d.ts.map +1 -1
- package/dist/apps/data/routes/transfers.js +618 -105
- package/dist/apps/data/routes/transfers.js.map +1 -1
- package/dist/apps/data/routes/valuation.d.ts +21 -21
- package/dist/apps/data/routes/valuation.d.ts.map +1 -1
- package/dist/apps/data/routes/valuation.js +2 -2
- package/dist/apps/data/routes/valuation.js.map +1 -1
- package/dist/apps/data/routes/verified-tokens.d.ts +37 -37
- package/dist/apps/data/routes/webhooks.d.ts +102 -505
- package/dist/apps/data/routes/webhooks.d.ts.map +1 -1
- package/dist/apps/data/routes/webhooks.js +484 -342
- package/dist/apps/data/routes/webhooks.js.map +1 -1
- package/dist/apps/data/routes/zones.d.ts +12 -12
- package/dist/apps/data/routes/zones.d.ts.map +1 -1
- package/dist/apps/data/routes/zones.js +3 -1
- package/dist/apps/data/routes/zones.js.map +1 -1
- package/dist/apps/funding/App.d.ts +127 -386
- package/dist/apps/funding/App.d.ts.map +1 -1
- package/dist/apps/funding/App.js +20 -5
- package/dist/apps/funding/App.js.map +1 -1
- package/dist/apps/funding/routes/deposit-addresses.d.ts +141 -532
- package/dist/apps/funding/routes/deposit-addresses.d.ts.map +1 -1
- package/dist/apps/funding/routes/deposit-addresses.js +21 -103
- package/dist/apps/funding/routes/deposit-addresses.js.map +1 -1
- package/dist/apps/funding/routes/transfers.d.ts +4 -4
- package/dist/apps/funding/routes/transfers.js +5 -5
- package/dist/apps/funding/routes/transfers.js.map +1 -1
- package/dist/apps/management/App.d.ts +429 -727
- package/dist/apps/management/App.d.ts.map +1 -1
- package/dist/apps/management/routes/api-keys.d.ts +24 -24
- package/dist/apps/management/routes/billing.d.ts +72 -72
- package/dist/apps/management/routes/faucet.d.ts +6 -6
- package/dist/apps/management/routes/invite-links.d.ts +38 -38
- package/dist/apps/management/routes/me.d.ts +6 -6
- package/dist/apps/management/routes/members.d.ts +30 -30
- package/dist/apps/management/routes/projects.d.ts +20 -20
- package/dist/apps/management/routes/usage.d.ts +24 -24
- package/dist/apps/management/routes/webhooks.d.ts +158 -573
- package/dist/apps/management/routes/webhooks.d.ts.map +1 -1
- package/dist/apps/mpp/App.d.ts +12 -0
- package/dist/apps/mpp/App.d.ts.map +1 -1
- package/dist/apps/mpp/Mpp.d.ts +3 -0
- package/dist/apps/mpp/Mpp.d.ts.map +1 -1
- package/dist/apps/mpp/Mpp.js +3 -0
- package/dist/apps/mpp/Mpp.js.map +1 -1
- package/dist/apps/mpp/internal/Adapter.d.ts.map +1 -1
- package/dist/apps/mpp/internal/Adapter.js +9 -2
- package/dist/apps/mpp/internal/Adapter.js.map +1 -1
- package/dist/db/Db.d.ts +2 -2
- package/dist/db/Db.d.ts.map +1 -1
- package/dist/db/Db.js +25 -15
- package/dist/db/Db.js.map +1 -1
- package/dist/db/tables/fundingDeposits.d.ts +18 -1
- package/dist/db/tables/fundingDeposits.d.ts.map +1 -1
- package/dist/db/tables/fundingDeposits.js +46 -2
- package/dist/db/tables/fundingDeposits.js.map +1 -1
- package/dist/db/tables/sponsoredTransactions.d.ts.map +1 -1
- package/dist/db/tables/sponsoredTransactions.js +4 -1
- package/dist/db/tables/sponsoredTransactions.js.map +1 -1
- package/dist/db/tables/webhookSubscriptions.d.ts +56 -3
- package/dist/db/tables/webhookSubscriptions.d.ts.map +1 -1
- package/dist/db/tables/webhookSubscriptions.js +76 -3
- package/dist/db/tables/webhookSubscriptions.js.map +1 -1
- package/dist/internal/Auth.d.ts +4 -0
- package/dist/internal/Auth.d.ts.map +1 -1
- package/dist/internal/Auth.js +11 -2
- package/dist/internal/Auth.js.map +1 -1
- package/dist/internal/MetricSink.d.ts +112 -3
- package/dist/internal/MetricSink.d.ts.map +1 -1
- package/dist/internal/MetricSink.js +116 -3
- package/dist/internal/MetricSink.js.map +1 -1
- package/dist/internal/Schema.js +1 -1
- package/dist/internal/Schema.js.map +1 -1
- package/dist/internal/Tidx.d.ts +1 -3
- package/dist/internal/Tidx.d.ts.map +1 -1
- package/dist/internal/Tidx.js +1 -3
- package/dist/internal/Tidx.js.map +1 -1
- package/dist/internal/Viem.d.ts.map +1 -1
- package/dist/internal/Viem.js +2 -1
- package/dist/internal/Viem.js.map +1 -1
- package/dist/internal/WebhookDestination.d.ts +2 -0
- package/dist/internal/WebhookDestination.d.ts.map +1 -1
- package/dist/internal/WebhookDestination.js +11 -0
- package/dist/internal/WebhookDestination.js.map +1 -1
- package/dist/internal/WebhookTransfer.d.ts +55 -0
- package/dist/internal/WebhookTransfer.d.ts.map +1 -0
- package/dist/internal/WebhookTransfer.js +296 -0
- package/dist/internal/WebhookTransfer.js.map +1 -0
- package/dist/internal/Webhooks.d.ts +37 -0
- package/dist/internal/Webhooks.d.ts.map +1 -1
- package/dist/internal/Webhooks.js +87 -13
- package/dist/internal/Webhooks.js.map +1 -1
- package/dist/internal/funding/Provider.d.ts +10 -6
- package/dist/internal/funding/Provider.d.ts.map +1 -1
- package/dist/internal/funding/Provider.js +11 -7
- package/dist/internal/funding/Provider.js.map +1 -1
- package/dist/internal/funding/Reconciliation.d.ts.map +1 -1
- package/dist/internal/funding/Reconciliation.js +229 -16
- package/dist/internal/funding/Reconciliation.js.map +1 -1
- package/dist/internal/funding/providers/relay.d.ts +3 -0
- package/dist/internal/funding/providers/relay.d.ts.map +1 -1
- package/dist/internal/funding/providers/relay.js +18 -3
- package/dist/internal/funding/providers/relay.js.map +1 -1
- package/package.json +3 -3
- package/src/App.test.ts +32 -0
- package/src/App.ts +4 -1
- package/src/Client.test-d.ts +116 -38
- package/src/Webhooks.bench.ts +714 -0
- package/src/Webhooks.memory.bench.ts +250 -0
- package/src/Webhooks.test.ts +77 -6
- package/src/Webhooks.ts +83 -29
- package/src/apps/Zones.test.ts +5 -4
- package/src/apps/Zones.ts +9 -8
- package/src/apps/composition.test.ts +39 -10
- package/src/apps/data/FxOracle.test.ts +18 -0
- package/src/apps/data/FxOracle.ts +28 -5
- package/src/apps/data/routes/activities.test.ts +19 -0
- package/src/apps/data/routes/activities.ts +34 -30
- package/src/apps/data/routes/balances.test.ts +161 -10
- package/src/apps/data/routes/balances.ts +339 -147
- package/src/apps/data/routes/blocks.test.ts +12 -6
- package/src/apps/data/routes/earn.test.ts +286 -9
- package/src/apps/data/routes/earn.ts +841 -58
- package/src/apps/data/routes/exchanges.test.ts +7 -6
- package/src/apps/data/routes/exchanges.ts +124 -117
- package/src/apps/data/routes/receipts.test.ts +24 -7
- package/src/apps/data/routes/receipts.ts +67 -50
- package/src/apps/data/routes/tokens.test.ts +51 -0
- package/src/apps/data/routes/transactions.test.ts +42 -0
- package/src/apps/data/routes/transactions.ts +44 -39
- package/src/apps/data/routes/transfers.test.ts +402 -24
- package/src/apps/data/routes/transfers.ts +708 -101
- package/src/apps/data/routes/valuation.test.ts +26 -0
- package/src/apps/data/routes/valuation.ts +6 -2
- package/src/apps/data/routes/webhooks.test.ts +414 -130
- package/src/apps/data/routes/webhooks.ts +603 -419
- package/src/apps/data/routes/zones.ts +6 -1
- package/src/apps/funding/App.ts +20 -5
- package/src/apps/funding/routes/deposit-addresses.test.ts +43 -22
- package/src/apps/funding/routes/deposit-addresses.ts +34 -117
- package/src/apps/funding/routes/quotes.test.ts +1 -1
- package/src/apps/funding/routes/transfers.test.ts +6 -25
- package/src/apps/funding/routes/transfers.ts +5 -5
- package/src/apps/mpp/Mpp.test.ts +13 -1
- package/src/apps/mpp/Mpp.ts +8 -0
- package/src/apps/mpp/internal/Adapter.ts +10 -2
- package/src/db/Db.test.ts +40 -6
- package/src/db/Db.ts +42 -19
- package/src/db/tables/fundingDeposits.test.ts +101 -0
- package/src/db/tables/fundingDeposits.ts +71 -2
- package/src/db/tables/sponsoredTransactions.ts +9 -2
- package/src/db/tables/webhookSubscriptions.ts +116 -4
- package/src/internal/Auth.test.ts +28 -0
- package/src/internal/Auth.ts +15 -1
- package/src/internal/EarnVaults.test.ts +2 -2
- package/src/internal/MetricSink.test.ts +518 -0
- package/src/internal/MetricSink.ts +266 -9
- package/src/internal/Schema.ts +1 -1
- package/src/internal/Tidx.test.ts +4 -4
- package/src/internal/Tidx.ts +1 -3
- package/src/internal/Viem.test.ts +8 -0
- package/src/internal/Viem.ts +2 -1
- package/src/internal/WebhookDestination.test.ts +20 -0
- package/src/internal/WebhookDestination.ts +10 -0
- package/src/internal/WebhookTransfer.ts +426 -0
- package/src/internal/Webhooks.test.ts +137 -0
- package/src/internal/Webhooks.ts +131 -17
- package/src/internal/funding/Provider.test.ts +47 -7
- package/src/internal/funding/Provider.ts +16 -11
- package/src/internal/funding/Reconciliation.local.test.ts +3 -1
- package/src/internal/funding/Reconciliation.test.ts +1532 -54
- package/src/internal/funding/Reconciliation.ts +296 -53
- package/src/internal/funding/SourceObservation.test.ts +3 -2
- package/src/internal/funding/providers/relay.test.ts +1 -4
- package/src/internal/funding/providers/relay.ts +23 -3
|
@@ -0,0 +1,714 @@
|
|
|
1
|
+
/** @module-tag localnet */
|
|
2
|
+
/**
|
|
3
|
+
* Benchmarks the production poller against disposable PostgreSQL and Tempo services.
|
|
4
|
+
* Set `WEBHOOK_BENCH_COUNTS`, `WEBHOOK_BENCH_ITERATIONS`, and
|
|
5
|
+
* `WEBHOOK_BENCH_SCENARIOS` to focus a run.
|
|
6
|
+
*/
|
|
7
|
+
import * as http from 'node:http'
|
|
8
|
+
|
|
9
|
+
import { Webhooks } from 'tapimo'
|
|
10
|
+
import { bench } from 'vite-plus/test'
|
|
11
|
+
|
|
12
|
+
import * as Cursor from './internal/Cursor.js'
|
|
13
|
+
import * as Metrics from './Metrics.js'
|
|
14
|
+
import * as TestApp from '../test/App.js'
|
|
15
|
+
import * as Runtime from '../test/runtime.js'
|
|
16
|
+
import * as TestViem from '../test/Viem.js'
|
|
17
|
+
|
|
18
|
+
const concurrency = 8
|
|
19
|
+
const endOfBlockIndex = 2_147_483_647
|
|
20
|
+
const noMatchAddress = '0xffffffffffffffffffffffffffffffffffffffff'
|
|
21
|
+
const runtime = Runtime.get()
|
|
22
|
+
const scenarioNames = [
|
|
23
|
+
'caught-up',
|
|
24
|
+
'empty-log',
|
|
25
|
+
'empty-transaction',
|
|
26
|
+
'empty-transfer',
|
|
27
|
+
'matching-block',
|
|
28
|
+
] as const
|
|
29
|
+
const transactionIterations = 1
|
|
30
|
+
const warmupIterations = 3
|
|
31
|
+
|
|
32
|
+
type DispatchStats = {
|
|
33
|
+
calls: number
|
|
34
|
+
messages: number
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
type Fixture = {
|
|
38
|
+
count: number
|
|
39
|
+
scenarios: Partial<Record<ScenarioName, Scenario>>
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
type Measurement = {
|
|
43
|
+
cursorWriteMs: number
|
|
44
|
+
enrichMs: number
|
|
45
|
+
rpcCalls: number
|
|
46
|
+
rpcHeadMs: number
|
|
47
|
+
rpcMethods: Record<string, number>
|
|
48
|
+
rpcRequestBytes: number
|
|
49
|
+
rpcRequests: number
|
|
50
|
+
rpcResponseBytes: number
|
|
51
|
+
scanMs: number
|
|
52
|
+
subscriptionReadMs: number
|
|
53
|
+
tickMs: number
|
|
54
|
+
unattributedMs: number
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
type RpcStats = {
|
|
58
|
+
calls: number
|
|
59
|
+
methods: Record<string, number>
|
|
60
|
+
requestBytes: number
|
|
61
|
+
requests: number
|
|
62
|
+
responseBytes: number
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
type Scenario = {
|
|
66
|
+
db: ReturnType<typeof TestApp.database>
|
|
67
|
+
dispatchStats: DispatchStats
|
|
68
|
+
head: number
|
|
69
|
+
ids: readonly string[]
|
|
70
|
+
measurements: Measurement[]
|
|
71
|
+
metrics: ReturnType<typeof createMetricProbe>
|
|
72
|
+
nextHead: number
|
|
73
|
+
poller: Webhooks.createPoller.Poller
|
|
74
|
+
rpc: Awaited<ReturnType<typeof createRpcProbe>>
|
|
75
|
+
samples: number
|
|
76
|
+
startBlockNumber: number
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
type ScenarioName = (typeof scenarioNames)[number]
|
|
80
|
+
|
|
81
|
+
const counts = (() => {
|
|
82
|
+
const input = process.env.WEBHOOK_BENCH_COUNTS
|
|
83
|
+
if (!input) return [1, 100, 1_000]
|
|
84
|
+
const values = [...new Set(input.split(',').map(Number))].sort((a, b) => a - b)
|
|
85
|
+
if (values.length === 0 || values.some((value) => !Number.isSafeInteger(value) || value < 1))
|
|
86
|
+
throw new Error('WEBHOOK_BENCH_COUNTS must be a comma-separated list of positive integers.')
|
|
87
|
+
return values
|
|
88
|
+
})()
|
|
89
|
+
|
|
90
|
+
const iterations = (() => {
|
|
91
|
+
const input = process.env.WEBHOOK_BENCH_ITERATIONS
|
|
92
|
+
if (!input) return 10
|
|
93
|
+
const value = Number(input)
|
|
94
|
+
if (!Number.isSafeInteger(value) || value < 1)
|
|
95
|
+
throw new Error('WEBHOOK_BENCH_ITERATIONS must be a positive integer.')
|
|
96
|
+
return value
|
|
97
|
+
})()
|
|
98
|
+
|
|
99
|
+
const selectedScenarios = (() => {
|
|
100
|
+
const input = process.env.WEBHOOK_BENCH_SCENARIOS
|
|
101
|
+
if (!input) return new Set<ScenarioName>(scenarioNames)
|
|
102
|
+
const values = [...new Set(input.split(','))]
|
|
103
|
+
const unknown = values.filter((value) => !scenarioNames.includes(value as ScenarioName))
|
|
104
|
+
if (values.length === 0 || unknown.length > 0)
|
|
105
|
+
throw new Error(
|
|
106
|
+
`WEBHOOK_BENCH_SCENARIOS must contain: ${scenarioNames.join(', ')}. Unknown: ${unknown.join(', ')}.`,
|
|
107
|
+
)
|
|
108
|
+
return new Set(values as ScenarioName[])
|
|
109
|
+
})()
|
|
110
|
+
|
|
111
|
+
const fixtures = new Map<number, Fixture>()
|
|
112
|
+
let rpcProbe: Awaited<ReturnType<typeof createRpcProbe>> | undefined
|
|
113
|
+
|
|
114
|
+
beforeAll(async () => {
|
|
115
|
+
if (runtime.mode !== 'localnet')
|
|
116
|
+
throw new Error('Run webhook benchmarks with TEMPO_ENV=localnet.')
|
|
117
|
+
rpcProbe = await createRpcProbe()
|
|
118
|
+
for (const count of counts) fixtures.set(count, await createFixture(count, rpcProbe))
|
|
119
|
+
}, 180_000)
|
|
120
|
+
|
|
121
|
+
afterAll(async () => {
|
|
122
|
+
reportMeasurements()
|
|
123
|
+
for (const fixture of fixtures.values())
|
|
124
|
+
for (const scenario of Object.values(fixture.scenarios)) await scenario.db.close()
|
|
125
|
+
await rpcProbe?.close()
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
describe('webhook poller', () => {
|
|
129
|
+
for (const count of counts) {
|
|
130
|
+
if (selectedScenarios.has('caught-up'))
|
|
131
|
+
bench(
|
|
132
|
+
`caught-up tick / ${count} subscriptions`,
|
|
133
|
+
async () => {
|
|
134
|
+
const scenario = getScenario(count, 'caught-up')
|
|
135
|
+
resetDispatchStats(scenario)
|
|
136
|
+
resetMeasurement(scenario)
|
|
137
|
+
const startedAt = performance.now()
|
|
138
|
+
const result = await scenario.poller.tick({
|
|
139
|
+
confirmations: 0,
|
|
140
|
+
eventTypes: ['block:created'],
|
|
141
|
+
headBlockNumber: scenario.head,
|
|
142
|
+
maxBlocks: 1,
|
|
143
|
+
})
|
|
144
|
+
recordMeasurement(scenario, performance.now() - startedAt)
|
|
145
|
+
assertDispatch(scenario, { calls: 0, messages: 0 })
|
|
146
|
+
assertResult(result, { count, dispatched: 0, scanned: 0 })
|
|
147
|
+
},
|
|
148
|
+
{
|
|
149
|
+
iterations,
|
|
150
|
+
setup: async () => {
|
|
151
|
+
const scenario = getScenario(count, 'caught-up')
|
|
152
|
+
await resetCursors(scenario, scenario.head)
|
|
153
|
+
},
|
|
154
|
+
time: 0,
|
|
155
|
+
throws: true,
|
|
156
|
+
warmupIterations,
|
|
157
|
+
warmupTime: 0,
|
|
158
|
+
},
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
if (selectedScenarios.has('empty-transfer'))
|
|
162
|
+
bench(
|
|
163
|
+
`empty selective transfer block / ${count} subscriptions`,
|
|
164
|
+
async () => {
|
|
165
|
+
const scenario = getScenario(count, 'empty-transfer')
|
|
166
|
+
resetDispatchStats(scenario)
|
|
167
|
+
resetMeasurement(scenario)
|
|
168
|
+
const startedAt = performance.now()
|
|
169
|
+
const result = await scenario.poller.tick({
|
|
170
|
+
confirmations: 0,
|
|
171
|
+
eventTypes: ['token:transfer'],
|
|
172
|
+
headBlockNumber: nextHead(scenario),
|
|
173
|
+
maxBlocks: 1,
|
|
174
|
+
})
|
|
175
|
+
recordMeasurement(scenario, performance.now() - startedAt)
|
|
176
|
+
assertDispatch(scenario, { calls: 0, messages: 0 })
|
|
177
|
+
assertResult(result, { count, dispatched: 0, scanned: 0 })
|
|
178
|
+
},
|
|
179
|
+
{
|
|
180
|
+
iterations,
|
|
181
|
+
setup: async () => {
|
|
182
|
+
const scenario = getScenario(count, 'empty-transfer')
|
|
183
|
+
await prepareScan(scenario)
|
|
184
|
+
},
|
|
185
|
+
time: 0,
|
|
186
|
+
throws: true,
|
|
187
|
+
warmupIterations,
|
|
188
|
+
warmupTime: 0,
|
|
189
|
+
},
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
if (selectedScenarios.has('empty-log'))
|
|
193
|
+
bench(
|
|
194
|
+
`empty selective log block / ${count} subscriptions`,
|
|
195
|
+
async () => {
|
|
196
|
+
const scenario = getScenario(count, 'empty-log')
|
|
197
|
+
resetDispatchStats(scenario)
|
|
198
|
+
resetMeasurement(scenario)
|
|
199
|
+
const startedAt = performance.now()
|
|
200
|
+
const result = await scenario.poller.tick({
|
|
201
|
+
confirmations: 0,
|
|
202
|
+
eventTypes: ['log:emitted'],
|
|
203
|
+
headBlockNumber: nextHead(scenario),
|
|
204
|
+
maxBlocks: 1,
|
|
205
|
+
})
|
|
206
|
+
recordMeasurement(scenario, performance.now() - startedAt)
|
|
207
|
+
assertDispatch(scenario, { calls: 0, messages: 0 })
|
|
208
|
+
assertResult(result, { count, dispatched: 0, scanned: 0 })
|
|
209
|
+
},
|
|
210
|
+
{
|
|
211
|
+
iterations,
|
|
212
|
+
setup: async () => {
|
|
213
|
+
const scenario = getScenario(count, 'empty-log')
|
|
214
|
+
await prepareScan(scenario)
|
|
215
|
+
},
|
|
216
|
+
time: 0,
|
|
217
|
+
throws: true,
|
|
218
|
+
warmupIterations,
|
|
219
|
+
warmupTime: 0,
|
|
220
|
+
},
|
|
221
|
+
)
|
|
222
|
+
|
|
223
|
+
if (selectedScenarios.has('empty-transaction'))
|
|
224
|
+
bench(
|
|
225
|
+
`empty selective transaction block / ${count} subscriptions`,
|
|
226
|
+
async () => {
|
|
227
|
+
const scenario = getScenario(count, 'empty-transaction')
|
|
228
|
+
resetDispatchStats(scenario)
|
|
229
|
+
resetMeasurement(scenario)
|
|
230
|
+
const startedAt = performance.now()
|
|
231
|
+
const result = await scenario.poller.tick({
|
|
232
|
+
confirmations: 0,
|
|
233
|
+
eventTypes: ['transaction:included'],
|
|
234
|
+
headBlockNumber: nextHead(scenario),
|
|
235
|
+
maxBlocks: 1,
|
|
236
|
+
})
|
|
237
|
+
recordMeasurement(scenario, performance.now() - startedAt)
|
|
238
|
+
assertDispatch(scenario, { calls: 0, messages: 0 })
|
|
239
|
+
assertResult(result, { count, dispatched: 0, scanned: 0 })
|
|
240
|
+
},
|
|
241
|
+
{
|
|
242
|
+
iterations: transactionIterations,
|
|
243
|
+
setup: async () => {
|
|
244
|
+
const scenario = getScenario(count, 'empty-transaction')
|
|
245
|
+
await prepareScan(scenario)
|
|
246
|
+
},
|
|
247
|
+
time: 0,
|
|
248
|
+
throws: true,
|
|
249
|
+
warmupIterations: 0,
|
|
250
|
+
warmupTime: 0,
|
|
251
|
+
},
|
|
252
|
+
)
|
|
253
|
+
|
|
254
|
+
if (selectedScenarios.has('matching-block'))
|
|
255
|
+
bench(
|
|
256
|
+
`matching block fanout / ${count} subscriptions`,
|
|
257
|
+
async () => {
|
|
258
|
+
const scenario = getScenario(count, 'matching-block')
|
|
259
|
+
resetDispatchStats(scenario)
|
|
260
|
+
resetMeasurement(scenario)
|
|
261
|
+
const startedAt = performance.now()
|
|
262
|
+
const result = await scenario.poller.tick({
|
|
263
|
+
confirmations: 0,
|
|
264
|
+
eventTypes: ['block:created'],
|
|
265
|
+
headBlockNumber: nextHead(scenario),
|
|
266
|
+
maxBlocks: 1,
|
|
267
|
+
})
|
|
268
|
+
recordMeasurement(scenario, performance.now() - startedAt)
|
|
269
|
+
assertDispatch(scenario, { calls: count, messages: count })
|
|
270
|
+
assertResult(result, { count, dispatched: count, scanned: count })
|
|
271
|
+
},
|
|
272
|
+
{
|
|
273
|
+
iterations,
|
|
274
|
+
setup: async () => {
|
|
275
|
+
const scenario = getScenario(count, 'matching-block')
|
|
276
|
+
await prepareScan(scenario)
|
|
277
|
+
},
|
|
278
|
+
time: 0,
|
|
279
|
+
throws: true,
|
|
280
|
+
warmupIterations,
|
|
281
|
+
warmupTime: 0,
|
|
282
|
+
},
|
|
283
|
+
)
|
|
284
|
+
}
|
|
285
|
+
})
|
|
286
|
+
|
|
287
|
+
async function createFixture(
|
|
288
|
+
count: number,
|
|
289
|
+
rpc: Awaited<ReturnType<typeof createRpcProbe>>,
|
|
290
|
+
): Promise<Fixture> {
|
|
291
|
+
const client = TestViem.getClient()
|
|
292
|
+
const liveHead = Number(await client.getBlockNumber())
|
|
293
|
+
const head = Math.max(1, liveHead - 1)
|
|
294
|
+
const startBlockNumber = Math.max(0, head - Math.max(iterations, warmupIterations) - 1)
|
|
295
|
+
const scenarios: Partial<Record<ScenarioName, Scenario>> = {}
|
|
296
|
+
if (selectedScenarios.has('caught-up'))
|
|
297
|
+
scenarios['caught-up'] = await createScenario({
|
|
298
|
+
count,
|
|
299
|
+
cursorBlockNumber: head,
|
|
300
|
+
eventType: 'block:created',
|
|
301
|
+
filters: {},
|
|
302
|
+
head,
|
|
303
|
+
rpc,
|
|
304
|
+
})
|
|
305
|
+
if (selectedScenarios.has('empty-log'))
|
|
306
|
+
scenarios['empty-log'] = await createScenario({
|
|
307
|
+
count,
|
|
308
|
+
cursorBlockNumber: startBlockNumber,
|
|
309
|
+
eventType: 'log:emitted',
|
|
310
|
+
filters: { address: noMatchAddress },
|
|
311
|
+
head,
|
|
312
|
+
rpc,
|
|
313
|
+
})
|
|
314
|
+
if (selectedScenarios.has('empty-transaction'))
|
|
315
|
+
scenarios['empty-transaction'] = await createScenario({
|
|
316
|
+
count,
|
|
317
|
+
cursorBlockNumber: Math.max(0, head - transactionIterations),
|
|
318
|
+
eventType: 'transaction:included',
|
|
319
|
+
filters: { from: noMatchAddress },
|
|
320
|
+
head,
|
|
321
|
+
rpc,
|
|
322
|
+
samples: transactionIterations,
|
|
323
|
+
})
|
|
324
|
+
if (selectedScenarios.has('empty-transfer'))
|
|
325
|
+
scenarios['empty-transfer'] = await createScenario({
|
|
326
|
+
count,
|
|
327
|
+
cursorBlockNumber: startBlockNumber,
|
|
328
|
+
eventType: 'token:transfer',
|
|
329
|
+
filters: { sender: noMatchAddress },
|
|
330
|
+
head,
|
|
331
|
+
rpc,
|
|
332
|
+
})
|
|
333
|
+
if (selectedScenarios.has('matching-block'))
|
|
334
|
+
scenarios['matching-block'] = await createScenario({
|
|
335
|
+
count,
|
|
336
|
+
cursorBlockNumber: startBlockNumber,
|
|
337
|
+
eventType: 'block:created',
|
|
338
|
+
filters: {},
|
|
339
|
+
head,
|
|
340
|
+
rpc,
|
|
341
|
+
})
|
|
342
|
+
return {
|
|
343
|
+
count,
|
|
344
|
+
scenarios,
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
async function createScenario(options: createScenario.Options): Promise<Scenario> {
|
|
349
|
+
const source = TestApp.databaseFactory()
|
|
350
|
+
const db = source()
|
|
351
|
+
const dispatchStats = { calls: 0, messages: 0 }
|
|
352
|
+
const metrics = createMetricProbe()
|
|
353
|
+
const ids = await createSubscriptions({
|
|
354
|
+
count: options.count,
|
|
355
|
+
cursor: Cursor.encode([options.cursorBlockNumber, endOfBlockIndex]),
|
|
356
|
+
db,
|
|
357
|
+
eventType: options.eventType,
|
|
358
|
+
filters: options.filters,
|
|
359
|
+
})
|
|
360
|
+
return {
|
|
361
|
+
db,
|
|
362
|
+
dispatchStats,
|
|
363
|
+
head: options.head,
|
|
364
|
+
ids,
|
|
365
|
+
measurements: [],
|
|
366
|
+
metrics,
|
|
367
|
+
nextHead: options.cursorBlockNumber + 1,
|
|
368
|
+
poller: Webhooks.createPoller({
|
|
369
|
+
db: source,
|
|
370
|
+
defaultChainId: runtime.chainId,
|
|
371
|
+
dispatchBatch: async (dispatchables) => {
|
|
372
|
+
dispatchStats.calls++
|
|
373
|
+
dispatchStats.messages += dispatchables.length
|
|
374
|
+
return { ok: true }
|
|
375
|
+
},
|
|
376
|
+
metrics: metrics.sink,
|
|
377
|
+
rpc: { ...TestApp.rpc, url: options.rpc.url },
|
|
378
|
+
}),
|
|
379
|
+
rpc: options.rpc,
|
|
380
|
+
samples: options.samples ?? iterations,
|
|
381
|
+
startBlockNumber: options.cursorBlockNumber,
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
declare namespace createScenario {
|
|
386
|
+
type Options = {
|
|
387
|
+
count: number
|
|
388
|
+
cursorBlockNumber: number
|
|
389
|
+
eventType: Webhooks.EventType
|
|
390
|
+
filters: Record<string, unknown>
|
|
391
|
+
head: number
|
|
392
|
+
rpc: Awaited<ReturnType<typeof createRpcProbe>>
|
|
393
|
+
samples?: number | undefined
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
async function createSubscriptions(options: createSubscriptions.Options): Promise<string[]> {
|
|
398
|
+
const ids: string[] = []
|
|
399
|
+
for (let offset = 0; offset < options.count; offset += concurrency) {
|
|
400
|
+
const batch = Array.from(
|
|
401
|
+
{ length: Math.min(concurrency, options.count - offset) },
|
|
402
|
+
async (_, index) => {
|
|
403
|
+
const subscription = await Webhooks.createSubscription(options.db, {
|
|
404
|
+
chainId: runtime.chainId,
|
|
405
|
+
destination: {
|
|
406
|
+
type: 'url',
|
|
407
|
+
url: `https://hooks.example.com/bench/${options.eventType}/${offset + index}`,
|
|
408
|
+
},
|
|
409
|
+
eventType: options.eventType,
|
|
410
|
+
filters: options.filters,
|
|
411
|
+
owner: { orgId: 'org_benchmark', type: 'api_key' },
|
|
412
|
+
})
|
|
413
|
+
await Webhooks.setCursor(options.db, subscription.id, options.cursor)
|
|
414
|
+
return subscription.id
|
|
415
|
+
},
|
|
416
|
+
)
|
|
417
|
+
ids.push(...(await Promise.all(batch)))
|
|
418
|
+
}
|
|
419
|
+
return ids
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
declare namespace createSubscriptions {
|
|
423
|
+
type Options = {
|
|
424
|
+
count: number
|
|
425
|
+
cursor: string
|
|
426
|
+
db: ReturnType<typeof TestApp.database>
|
|
427
|
+
eventType: Webhooks.EventType
|
|
428
|
+
filters: Record<string, unknown>
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
const pollStages = ['cursor_write', 'enrich', 'rpc_head', 'scan', 'subscription_read'] as const
|
|
433
|
+
|
|
434
|
+
type PollStage = (typeof pollStages)[number]
|
|
435
|
+
|
|
436
|
+
function createMetricProbe() {
|
|
437
|
+
let stages = emptyStages()
|
|
438
|
+
return {
|
|
439
|
+
reset() {
|
|
440
|
+
stages = emptyStages()
|
|
441
|
+
},
|
|
442
|
+
sink: Metrics.from({
|
|
443
|
+
count() {},
|
|
444
|
+
flush() {},
|
|
445
|
+
gauge() {},
|
|
446
|
+
histogram(name, value, tags) {
|
|
447
|
+
const stage = tags?.['stage']
|
|
448
|
+
if (name !== 'webhook_poller_stage_duration_ms' || !isPollStage(stage)) return
|
|
449
|
+
stages[stage] += value
|
|
450
|
+
},
|
|
451
|
+
}),
|
|
452
|
+
snapshot() {
|
|
453
|
+
return { ...stages }
|
|
454
|
+
},
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
function emptyStages(): Record<PollStage, number> {
|
|
459
|
+
return {
|
|
460
|
+
cursor_write: 0,
|
|
461
|
+
enrich: 0,
|
|
462
|
+
rpc_head: 0,
|
|
463
|
+
scan: 0,
|
|
464
|
+
subscription_read: 0,
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
function isPollStage(value: unknown): value is PollStage {
|
|
469
|
+
return typeof value === 'string' && pollStages.includes(value as PollStage)
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
async function createRpcProbe() {
|
|
473
|
+
const target = runtime.rpcUrl
|
|
474
|
+
if (!target) throw new Error('Localnet runtime is missing its RPC URL.')
|
|
475
|
+
|
|
476
|
+
let stats = emptyRpcStats()
|
|
477
|
+
const server = http.createServer((request, response) => {
|
|
478
|
+
void forwardRpc({ request, response, stats, target }).catch((error) => {
|
|
479
|
+
response.writeHead(502, { 'content-type': 'text/plain' })
|
|
480
|
+
response.end(error instanceof Error ? error.message : 'RPC probe failed.')
|
|
481
|
+
})
|
|
482
|
+
})
|
|
483
|
+
await new Promise<void>((resolve, reject) => {
|
|
484
|
+
const onError = (error: Error) => reject(error)
|
|
485
|
+
server.once('error', onError)
|
|
486
|
+
server.listen(0, '127.0.0.1', () => {
|
|
487
|
+
server.off('error', onError)
|
|
488
|
+
resolve()
|
|
489
|
+
})
|
|
490
|
+
})
|
|
491
|
+
const address = server.address()
|
|
492
|
+
if (!address || typeof address === 'string') throw new Error('RPC probe did not bind TCP.')
|
|
493
|
+
|
|
494
|
+
return {
|
|
495
|
+
close: () =>
|
|
496
|
+
new Promise<void>((resolve, reject) => {
|
|
497
|
+
server.close((error) => (error ? reject(error) : resolve()))
|
|
498
|
+
}),
|
|
499
|
+
reset() {
|
|
500
|
+
stats = emptyRpcStats()
|
|
501
|
+
},
|
|
502
|
+
snapshot(): RpcStats {
|
|
503
|
+
return { ...stats, methods: { ...stats.methods } }
|
|
504
|
+
},
|
|
505
|
+
url: `http://127.0.0.1:${address.port}`,
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
async function forwardRpc(options: {
|
|
510
|
+
request: http.IncomingMessage
|
|
511
|
+
response: http.ServerResponse
|
|
512
|
+
stats: RpcStats
|
|
513
|
+
target: string
|
|
514
|
+
}): Promise<void> {
|
|
515
|
+
const { request, response, stats, target } = options
|
|
516
|
+
const chunks: Uint8Array[] = []
|
|
517
|
+
for await (const chunk of request)
|
|
518
|
+
chunks.push(typeof chunk === 'string' ? Buffer.from(chunk) : chunk)
|
|
519
|
+
const body = Buffer.concat(chunks)
|
|
520
|
+
stats.requestBytes += body.byteLength
|
|
521
|
+
stats.requests++
|
|
522
|
+
countRpcCalls(stats, body)
|
|
523
|
+
|
|
524
|
+
const headers = new Headers()
|
|
525
|
+
for (const name of ['authorization', 'content-type']) {
|
|
526
|
+
const value = request.headers[name]
|
|
527
|
+
if (typeof value === 'string') headers.set(name, value)
|
|
528
|
+
}
|
|
529
|
+
const upstream = await fetch(target, {
|
|
530
|
+
body,
|
|
531
|
+
headers,
|
|
532
|
+
method: request.method ?? 'POST',
|
|
533
|
+
})
|
|
534
|
+
const result = new Uint8Array(await upstream.arrayBuffer())
|
|
535
|
+
stats.responseBytes += result.byteLength
|
|
536
|
+
response.writeHead(upstream.status, {
|
|
537
|
+
'content-type': upstream.headers.get('content-type') ?? 'application/json',
|
|
538
|
+
})
|
|
539
|
+
response.end(result)
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
function emptyRpcStats(): RpcStats {
|
|
543
|
+
return { calls: 0, methods: {}, requestBytes: 0, requests: 0, responseBytes: 0 }
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
function countRpcCalls(stats: RpcStats, body: Uint8Array): void {
|
|
547
|
+
try {
|
|
548
|
+
const parsed = JSON.parse(Buffer.from(body).toString('utf8')) as unknown
|
|
549
|
+
for (const value of Array.isArray(parsed) ? parsed : [parsed]) {
|
|
550
|
+
if (!value || typeof value !== 'object') continue
|
|
551
|
+
const method = (value as Record<string, unknown>)['method']
|
|
552
|
+
if (typeof method !== 'string') continue
|
|
553
|
+
stats.calls++
|
|
554
|
+
stats.methods[method] = (stats.methods[method] ?? 0) + 1
|
|
555
|
+
}
|
|
556
|
+
} catch {}
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
function resetMeasurement(scenario: Scenario): void {
|
|
560
|
+
scenario.metrics.reset()
|
|
561
|
+
scenario.rpc.reset()
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
function recordMeasurement(scenario: Scenario, tickMs: number): void {
|
|
565
|
+
const rpc = scenario.rpc.snapshot()
|
|
566
|
+
const stages = scenario.metrics.snapshot()
|
|
567
|
+
const attributedMs = Object.values(stages).reduce((sum, value) => sum + value, 0)
|
|
568
|
+
scenario.measurements.push({
|
|
569
|
+
cursorWriteMs: stages.cursor_write,
|
|
570
|
+
enrichMs: stages.enrich,
|
|
571
|
+
rpcCalls: rpc.calls,
|
|
572
|
+
rpcHeadMs: stages.rpc_head,
|
|
573
|
+
rpcMethods: rpc.methods,
|
|
574
|
+
rpcRequestBytes: rpc.requestBytes,
|
|
575
|
+
rpcRequests: rpc.requests,
|
|
576
|
+
rpcResponseBytes: rpc.responseBytes,
|
|
577
|
+
scanMs: stages.scan,
|
|
578
|
+
subscriptionReadMs: stages.subscription_read,
|
|
579
|
+
tickMs,
|
|
580
|
+
unattributedMs: Math.max(0, tickMs - attributedMs),
|
|
581
|
+
})
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
function reportMeasurements(): void {
|
|
585
|
+
const summaries: Record<string, unknown>[] = []
|
|
586
|
+
for (const fixture of fixtures.values()) {
|
|
587
|
+
const scenarios = [
|
|
588
|
+
{ label: 'caught-up tick', scenario: fixture.scenarios['caught-up'] },
|
|
589
|
+
{ label: 'empty selective log block', scenario: fixture.scenarios['empty-log'] },
|
|
590
|
+
{
|
|
591
|
+
label: 'empty selective transaction block',
|
|
592
|
+
scenario: fixture.scenarios['empty-transaction'],
|
|
593
|
+
},
|
|
594
|
+
{ label: 'empty selective transfer block', scenario: fixture.scenarios['empty-transfer'] },
|
|
595
|
+
{ label: 'matching block fanout', scenario: fixture.scenarios['matching-block'] },
|
|
596
|
+
]
|
|
597
|
+
for (const { label, scenario } of scenarios) {
|
|
598
|
+
if (!scenario) continue
|
|
599
|
+
const samples = scenario.measurements.slice(-scenario.samples)
|
|
600
|
+
summaries.push({
|
|
601
|
+
count: fixture.count,
|
|
602
|
+
cursorWriteMs: distribution(samples.map((sample) => sample.cursorWriteMs)),
|
|
603
|
+
enrichMs: distribution(samples.map((sample) => sample.enrichMs)),
|
|
604
|
+
label,
|
|
605
|
+
rpcCalls: distribution(samples.map((sample) => sample.rpcCalls)),
|
|
606
|
+
rpcHeadMs: distribution(samples.map((sample) => sample.rpcHeadMs)),
|
|
607
|
+
rpcMethods: summarizeMethods(samples),
|
|
608
|
+
rpcRequestBytes: distribution(samples.map((sample) => sample.rpcRequestBytes)),
|
|
609
|
+
rpcRequests: distribution(samples.map((sample) => sample.rpcRequests)),
|
|
610
|
+
rpcResponseBytes: distribution(samples.map((sample) => sample.rpcResponseBytes)),
|
|
611
|
+
scanMs: distribution(samples.map((sample) => sample.scanMs)),
|
|
612
|
+
subscriptionReadMs: distribution(samples.map((sample) => sample.subscriptionReadMs)),
|
|
613
|
+
tickMs: distribution(samples.map((sample) => sample.tickMs)),
|
|
614
|
+
unattributedMs: distribution(samples.map((sample) => sample.unattributedMs)),
|
|
615
|
+
})
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
console.log(`webhook benchmark instrumentation\n${JSON.stringify(summaries, null, 2)}`)
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
function summarizeMethods(samples: readonly Measurement[]): Record<string, number> {
|
|
622
|
+
const methods: Record<string, number> = {}
|
|
623
|
+
for (const sample of samples)
|
|
624
|
+
for (const [method, count] of Object.entries(sample.rpcMethods))
|
|
625
|
+
methods[method] = (methods[method] ?? 0) + count
|
|
626
|
+
for (const method of Object.keys(methods))
|
|
627
|
+
methods[method] = round(methods[method]! / samples.length)
|
|
628
|
+
return methods
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
function distribution(values: readonly number[]) {
|
|
632
|
+
const sorted = [...values].sort((a, b) => a - b)
|
|
633
|
+
const mean = sorted.reduce((sum, value) => sum + value, 0) / sorted.length
|
|
634
|
+
const percentile = (value: number) =>
|
|
635
|
+
sorted[Math.max(0, Math.ceil(sorted.length * value) - 1)] ?? 0
|
|
636
|
+
return {
|
|
637
|
+
mean: round(mean),
|
|
638
|
+
p50: round(percentile(0.5)),
|
|
639
|
+
p95: round(percentile(0.95)),
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
function round(value: number): number {
|
|
644
|
+
return Math.round(value * 100) / 100
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
async function resetCursors(scenario: Scenario, blockNumber: number): Promise<void> {
|
|
648
|
+
const cursor = Cursor.encode([blockNumber, endOfBlockIndex])
|
|
649
|
+
for (let offset = 0; offset < scenario.ids.length; offset += concurrency)
|
|
650
|
+
await Promise.all(
|
|
651
|
+
scenario.ids
|
|
652
|
+
.slice(offset, offset + concurrency)
|
|
653
|
+
.map((subscriptionId) => Webhooks.setCursor(scenario.db, subscriptionId, cursor)),
|
|
654
|
+
)
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
async function prepareScan(scenario: Scenario): Promise<void> {
|
|
658
|
+
scenario.nextHead = scenario.startBlockNumber + 1
|
|
659
|
+
await resetCursors(scenario, scenario.startBlockNumber)
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
function nextHead(scenario: Scenario): number {
|
|
663
|
+
if (scenario.nextHead > scenario.head)
|
|
664
|
+
throw new Error(`Benchmark exhausted its block window at ${scenario.head}.`)
|
|
665
|
+
return scenario.nextHead++
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
function getFixture(count: number): Fixture {
|
|
669
|
+
const fixture = fixtures.get(count)
|
|
670
|
+
if (!fixture) throw new Error(`Missing benchmark fixture for ${count} subscriptions.`)
|
|
671
|
+
return fixture
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
function getScenario(count: number, name: ScenarioName): Scenario {
|
|
675
|
+
const scenario = getFixture(count).scenarios[name]
|
|
676
|
+
if (!scenario) throw new Error(`Missing ${name} benchmark fixture for ${count} subscriptions.`)
|
|
677
|
+
return scenario
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
function assertDispatch(scenario: Scenario, expected: DispatchStats): void {
|
|
681
|
+
if (
|
|
682
|
+
scenario.dispatchStats.calls !== expected.calls ||
|
|
683
|
+
scenario.dispatchStats.messages !== expected.messages
|
|
684
|
+
)
|
|
685
|
+
throw new Error(
|
|
686
|
+
`Unexpected dispatches: ${JSON.stringify({
|
|
687
|
+
actual: scenario.dispatchStats,
|
|
688
|
+
expected,
|
|
689
|
+
})}`,
|
|
690
|
+
)
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
function assertResult(
|
|
694
|
+
result: Webhooks.createPoller.Result,
|
|
695
|
+
expected: { count: number; dispatched: number; scanned: number },
|
|
696
|
+
): void {
|
|
697
|
+
if (
|
|
698
|
+
result.dispatched !== expected.dispatched ||
|
|
699
|
+
result.failed !== 0 ||
|
|
700
|
+
result.scanned !== expected.scanned ||
|
|
701
|
+
result.subscriptions !== expected.count
|
|
702
|
+
)
|
|
703
|
+
throw new Error(
|
|
704
|
+
`Unexpected poll result: ${JSON.stringify({
|
|
705
|
+
expected,
|
|
706
|
+
result,
|
|
707
|
+
})}`,
|
|
708
|
+
)
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
function resetDispatchStats(scenario: Scenario): void {
|
|
712
|
+
scenario.dispatchStats.calls = 0
|
|
713
|
+
scenario.dispatchStats.messages = 0
|
|
714
|
+
}
|