Skip to content

Commit

Permalink
metrics for scoreboard (#361)
Browse files Browse the repository at this point in the history
* metrics for scoreboard

* gitignore

* better control of ethers batching

* better code

* linting

* html viewer

* better scoreboard

* better scoreboard support

* restore files

* scoreboard script

* display metrics for commands

* lint

* wrong type in snapshot
  • Loading branch information
bergarces authored Oct 29, 2024
1 parent 9f23881 commit e0c2589
Show file tree
Hide file tree
Showing 10 changed files with 1,763 additions and 20 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
"build-types": "npm run adapters-cli build-types --",
"new-adapter": "npm run adapters-cli new-adapter --",
"build-snapshots": "npm run adapters-cli build-snapshots --",
"build-scoreboard": "npm run adapters-cli build-scoreboard --",
"positions": "npm run adapters-cli positions --",
"profits": "npm run adapters-cli profits --",
"unwrap": "npm run adapters-cli unwrap --",
Expand Down
9 changes: 6 additions & 3 deletions packages/adapters-library/src/adapters/integration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,10 @@ import { logger } from '../core/utils/logger'
import { DefiProvider } from '../defiProvider'
import { getInvalidAddresses } from '../scripts/addressValidation'
import { protocolFilter } from '../scripts/commandFilters'
import { RpcInterceptedResponse, startRpcMock } from '../scripts/rpcInterceptor'
import {
RpcInterceptedResponses,
startRpcMock,
} from '../scripts/rpcInterceptor'
import { TestCase } from '../types/testCase'
import { testCases as aaveV2ATokenTestCases } from './aave-v2/products/a-token/tests/testCases'
import { testCases as aaveV2StableDebtTokenTestCases } from './aave-v2/products/stable-debt-token/tests/testCases'
Expand Down Expand Up @@ -366,7 +369,7 @@ function runProductTests(
.reduce((acc, x) => {
acc[x.key] = x.responses
return acc
}, {} as RpcInterceptedResponse)
}, {} as RpcInterceptedResponses)

const chainUrls = Object.values(defiProvider.chainProvider.providers).map(
(rpcProvider) => rpcProvider._getConnection().url,
Expand Down Expand Up @@ -797,7 +800,7 @@ async function loadJsonFile(
) as {
snapshot: unknown
blockNumber?: number
rpcResponses?: RpcInterceptedResponse
rpcResponses?: RpcInterceptedResponses
}

return {
Expand Down
1 change: 1 addition & 0 deletions packages/adapters-library/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,7 @@ const ConfigSchema = z
})
.default(maxBatchSize),
useDatabase: z.boolean().default(true),
disableEthersBatching: z.boolean().default(false),
})
.strict()
.default({})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ export class ChainProvider {
customOptions,
jsonRpcProviderOptions: {
staticNetwork: Network.from(chainId),
batchMaxCount: this.config.disableEthersBatching ? 1 : undefined,
},
hasUnlimitedGetLogsRange,
})
Expand Down
254 changes: 254 additions & 0 deletions packages/adapters-library/src/scripts/buildScoreboard.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,254 @@
import { readFile } from 'node:fs/promises'
import path from 'node:path'
import { Command } from 'commander'
import { Protocol } from '../adapters/protocols'
import { Chain, ChainIdToChainNameMap } from '../core/constants/chains'
import { filterMapSync } from '../core/utils/filters'
import { writeAndLintFile } from '../core/utils/writeAndLintFile'
import { DefiProvider } from '../defiProvider'
import type { TestCase } from '../types/testCase'
import { multiProtocolFilter } from './commandFilters'
import { RpcInterceptedResponses, startRpcSnapshot } from './rpcInterceptor'

type ScoreboardEntry = {
key: string | undefined
protocolId: string
productId: string
chain: string
latency: number
totalPools: number
} & RpcMetrics

type RpcMetrics = {
totalCalls: number
relativeMaxStartTime: number | undefined
relativeMaxEndTime: number | undefined
maxRpcRequestLatency: number
totalGas: string
}

export function buildScoreboard(program: Command, defiProvider: DefiProvider) {
program
.command('build-scoreboard')
.option(
'-p, --protocols <protocols>',
'comma-separated protocols filter (e.g. stargate,aave-v2)',
)
.option(
'-pd, --products <products>',
'comma-separated products filter (e.g. stargate,aave-v2)',
)
.showHelpAfterError()
.action(async ({ protocols, products }) => {
const filterProtocolIds = multiProtocolFilter(protocols)
const filterProductIds = (products as string | undefined)?.split(',')

const allProtocols = await defiProvider.getSupport({ filterProtocolIds })
const allProducts = Object.values(allProtocols).flatMap(
(protocolAdapters) =>
filterMapSync(protocolAdapters, (adapter) => {
if (
filterProductIds &&
!filterProductIds.includes(adapter.protocolDetails.productId)
) {
return undefined
}

return {
protocolId: adapter.protocolDetails.protocolId,
productId: adapter.protocolDetails.productId,
}
}),
)

const scoreboard: ScoreboardEntry[] = []

for (const { protocolId, productId } of allProducts) {
const testCases: TestCase[] = (
await import(
path.resolve(
__dirname,
`../adapters/${protocolId}/products/${productId}/tests/testCases`,
)
)
).testCases

for (const testCase of testCases) {
if (testCase.method !== 'positions') {
continue
}

console.log(
`Running test case # Chain: ${
testCase.chainId
} - Protocol: ${protocolId} - Product: ${productId}${
testCase.key ? ` - Key:${testCase.key}` : ''
}`,
)

// Recreate the provider for each test case to avoid cached data
const defiProvider = new DefiProvider({
useMulticallInterceptor: false,
disableEthersBatching: true,
})

const msw = startRpcSnapshot(
Object.values(defiProvider.chainProvider.providers).map(
(provider) => provider._getConnection().url,
),
)

const chainId = testCase.chainId

const blockNumber = testCase.blockNumber

const startTime = Date.now()

const result = await defiProvider.getPositions({
...testCase.input,
filterChainIds: [chainId],
filterProtocolIds: [protocolId],
filterProductIds: [productId],
blockNumbers: {
[chainId]: blockNumber,
},
})

if (result.length === 0) {
console.error('Snapshot with no results', {
protocolId,
productId,
chainId,
})
}

if (result.some((x) => !x.success)) {
console.error('Snapshot failed', { protocolId, productId, chainId })
}

if (result.length > 1) {
console.error('Snapshot with multiple results', {
protocolId,
productId,
chainId,
})
}

const endTime = Date.now()

scoreboard.push(
aggregateMetrics({
interceptedResponses: msw.interceptedResponses,
key: testCase.key,
protocolId,
productId,
chainId,
latency: endTime - startTime,
totalPools: result.reduce(
(acc, x) => acc + (!x.success ? 0 : x.tokens.length),
0,
),
}),
)

msw.stop()
}
}

const scoreboardHtml = await readFile('./scoreboard.html', 'utf-8')
const updatedHtml = scoreboardHtml.replace(
/\/\/ ### BEGIN DATA INSERT ###.*\/\/ ### END DATA INSERT ###/s,
`// ### BEGIN DATA INSERT ###\nconst data = ${JSON.stringify(
scoreboard,
null,
2,
)};\n// ### END DATA INSERT ###`,
)
await writeAndLintFile('./scoreboard.html', updatedHtml)

process.exit()
})
}

function aggregateMetrics({
interceptedResponses,
key,
protocolId,
productId,
chainId,
latency,
totalPools,
}: {
interceptedResponses: RpcInterceptedResponses
key: string | undefined
protocolId: Protocol
productId: string
chainId: Chain
latency: number
totalPools: number
}): ScoreboardEntry {
const rpcMetrics = extractRpcMetrics(interceptedResponses)

return {
key: key,
protocolId: protocolId,
productId: productId,
chain: ChainIdToChainNameMap[chainId],
latency: latency / 1_000,
totalPools,
...rpcMetrics,
}
}

export function extractRpcMetrics(
interceptedResponses: RpcInterceptedResponses,
): RpcMetrics {
if (Object.values(interceptedResponses).length === 0) {
return {
relativeMaxStartTime: undefined,
relativeMaxEndTime: undefined,
totalCalls: 0,
maxRpcRequestLatency: 0,
totalGas: '0',
}
}

let minStartTime: number | undefined
let maxStartTime: number | undefined
let minEndTime: number | undefined
let maxEndTime: number | undefined
let totalCalls = 0
let maxTakenTime = 0
let totalGas = 0n
for (const rpcInterceptedResponse of Object.values(interceptedResponses)) {
const metrics = rpcInterceptedResponse.metrics!
if (minStartTime === undefined || metrics.startTime < minStartTime) {
minStartTime = metrics.startTime
}
if (maxStartTime === undefined || metrics.startTime > maxStartTime) {
maxStartTime = metrics.startTime
}
if (minEndTime === undefined || metrics.endTime < minEndTime) {
minEndTime = metrics.endTime
}
if (maxEndTime === undefined || metrics.endTime > maxEndTime) {
maxEndTime = metrics.endTime
}

if (metrics.timeTaken > maxTakenTime) {
maxTakenTime = metrics.timeTaken
}

totalGas += BigInt(metrics.estimatedGas ?? 0)

totalCalls++
}

return {
relativeMaxStartTime: (maxStartTime! - minStartTime!) / 1_000,
relativeMaxEndTime: (maxEndTime! - minStartTime!) / 1_000,
totalCalls,
maxRpcRequestLatency: maxTakenTime / 1_000,
totalGas: totalGas.toString(),
}
}
24 changes: 22 additions & 2 deletions packages/adapters-library/src/scripts/buildSnapshots.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import { DefiProvider } from '../defiProvider'
import { DefiPositionResponse, DefiProfitsResponse } from '../types/response'
import type { TestCase } from '../types/testCase'
import { multiProtocolFilter } from './commandFilters'
import { startRpcSnapshot } from './rpcInterceptor'
import { RpcInterceptedResponses, startRpcSnapshot } from './rpcInterceptor'
import n = types.namedTypes
import b = types.builders
import { getPreviousLatency } from '../core/utils/readFile'
Expand Down Expand Up @@ -110,6 +110,7 @@ export function buildSnapshots(program: Command, defiProvider: DefiProvider) {
// Recreate the provider for each test case to avoid cached data
const defiProvider = new DefiProvider({
useMulticallInterceptor: false,
disableEthersBatching: true,
})

const msw = startRpcSnapshot(
Expand Down Expand Up @@ -440,12 +441,31 @@ export function buildSnapshots(program: Command, defiProvider: DefiProvider) {
}
})()

const rpcResponses = Object.entries(msw.interceptedResponses).reduce(
(acc, [key, response]) => {
acc[key] = {
result: response.result,
error: response.error,
}

if (
process.env.DEFI_ADAPTERS_SAVE_INTERCEPTED_REQUESTS === 'true'
) {
acc[key]!.request = response.request
acc[key]!.metrics = response.metrics
}

return acc
},
{} as RpcInterceptedResponses,
)

await writeAndLintFile(
filePath,
bigintJsonStringify(
{
...snapshotFileContent,
rpcResponses: msw.interceptedRequests,
rpcResponses,
},
2,
),
Expand Down
Loading

0 comments on commit e0c2589

Please sign in to comment.