Skip to content

Commit 5e486fc

Browse files
committed
perf(multicall): improve fetching code to allow for fetching immutable data like token symbols/names/decimals
1 parent 87d24c4 commit 5e486fc

4 files changed

Lines changed: 153 additions & 36 deletions

File tree

src/state/multicall/actions.ts

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,25 @@ export function parseCallKey(callKey: string): Call {
3030
}
3131
}
3232

33-
export const addMulticallListeners = createAction<{ chainId: number; calls: Call[] }>('addMulticallListeners')
34-
export const removeMulticallListeners = createAction<{ chainId: number; calls: Call[] }>('removeMulticallListeners')
33+
interface ListenerOptions {
34+
// how often this data should be fetched, by default 1
35+
blocksPerFetch?: number
36+
}
37+
38+
export const addMulticallListeners = createAction<{ chainId: number; calls: Call[]; options?: ListenerOptions }>(
39+
'addMulticallListeners'
40+
)
41+
export const removeMulticallListeners = createAction<{ chainId: number; calls: Call[]; options?: ListenerOptions }>(
42+
'removeMulticallListeners'
43+
)
44+
export const fetchingMulticallResults = createAction<{ chainId: number; calls: Call[]; fetchingBlockNumber: number }>(
45+
'fetchingMulticallResults'
46+
)
47+
export const errorFetchingMulticallResults = createAction<{
48+
chainId: number
49+
calls: Call[]
50+
fetchingBlockNumber: number
51+
}>('errorFetchingMulticallResults')
3552
export const updateMulticallResults = createAction<{
3653
chainId: number
3754
blockNumber: number

src/state/multicall/hooks.ts

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -53,11 +53,9 @@ function useCallsData(calls: (Call | undefined)[]): CallResult[] {
5353
[calls]
5454
)
5555

56-
const debouncedSerializedCallKeys = useDebounce(serializedCallKeys, 20)
57-
5856
// update listeners when there is an actual change that persists for at least 100ms
5957
useEffect(() => {
60-
const callKeys: string[] = JSON.parse(debouncedSerializedCallKeys)
58+
const callKeys: string[] = JSON.parse(serializedCallKeys)
6159
if (!chainId || callKeys.length === 0) return
6260
const calls = callKeys.map(key => parseCallKey(key))
6361
dispatch(
@@ -75,7 +73,7 @@ function useCallsData(calls: (Call | undefined)[]): CallResult[] {
7573
})
7674
)
7775
}
78-
}, [chainId, dispatch, debouncedSerializedCallKeys])
76+
}, [chainId, dispatch, serializedCallKeys])
7977

8078
return useMemo(
8179
() =>

src/state/multicall/reducer.ts

Lines changed: 60 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,22 @@
11
import { createReducer } from '@reduxjs/toolkit'
2-
import { addMulticallListeners, removeMulticallListeners, toCallKey, updateMulticallResults } from './actions'
2+
import {
3+
addMulticallListeners,
4+
errorFetchingMulticallResults,
5+
fetchingMulticallResults,
6+
removeMulticallListeners,
7+
toCallKey,
8+
updateMulticallResults
9+
} from './actions'
310

411
interface MulticallState {
5-
callListeners: {
12+
callListeners?: {
13+
// on a per-chain basis
614
[chainId: number]: {
7-
[callKey: string]: number
15+
// stores for each call key the listeners' preferences
16+
[callKey: string]: {
17+
// stores how many listeners there are per each blocks per fetch preference
18+
[blocksPerFetch: number]: number
19+
}
820
}
921
}
1022

@@ -13,36 +25,71 @@ interface MulticallState {
1325
[callKey: string]: {
1426
data: string | null
1527
blockNumber: number
28+
fetchingBlockNumber?: number
1629
}
1730
}
1831
}
1932
}
2033

2134
const initialState: MulticallState = {
22-
callListeners: {},
2335
callResults: {}
2436
}
2537

2638
export default createReducer(initialState, builder =>
2739
builder
28-
.addCase(addMulticallListeners, (state, { payload: { calls, chainId } }) => {
29-
state.callListeners[chainId] = state.callListeners[chainId] ?? {}
40+
.addCase(addMulticallListeners, (state, { payload: { calls, chainId, options: { blocksPerFetch = 1 } = {} } }) => {
41+
const listeners: MulticallState['callListeners'] = state.callListeners
42+
? state.callListeners
43+
: (state.callListeners = {})
44+
listeners[chainId] = listeners[chainId] ?? {}
3045
calls.forEach(call => {
3146
const callKey = toCallKey(call)
32-
state.callListeners[chainId][callKey] = (state.callListeners[chainId][callKey] ?? 0) + 1
47+
listeners[chainId][callKey] = listeners[chainId][callKey] ?? {}
48+
listeners[chainId][callKey][blocksPerFetch] = (listeners[chainId][callKey][blocksPerFetch] ?? 0) + 1
3349
})
3450
})
35-
.addCase(removeMulticallListeners, (state, { payload: { chainId, calls } }) => {
36-
if (!state.callListeners[chainId]) return
51+
.addCase(
52+
removeMulticallListeners,
53+
(state, { payload: { chainId, calls, options: { blocksPerFetch = 1 } = {} } }) => {
54+
const listeners: MulticallState['callListeners'] = state.callListeners
55+
? state.callListeners
56+
: (state.callListeners = {})
57+
58+
if (!listeners[chainId]) return
59+
calls.forEach(call => {
60+
const callKey = toCallKey(call)
61+
if (!listeners[chainId][callKey]) return
62+
if (!listeners[chainId][callKey][blocksPerFetch]) return
63+
64+
if (listeners[chainId][callKey][blocksPerFetch] === 1) {
65+
delete listeners[chainId][callKey][blocksPerFetch]
66+
} else {
67+
listeners[chainId][callKey][blocksPerFetch]--
68+
}
69+
})
70+
}
71+
)
72+
.addCase(fetchingMulticallResults, (state, { payload: { chainId, fetchingBlockNumber, calls } }) => {
73+
state.callResults[chainId] = state.callResults[chainId] ?? {}
3774
calls.forEach(call => {
3875
const callKey = toCallKey(call)
39-
if (state.callListeners[chainId][callKey] === 1) {
40-
delete state.callListeners[chainId][callKey]
41-
} else {
42-
state.callListeners[chainId][callKey]--
76+
const current = state.callResults[chainId][callKey]
77+
if (current && current.blockNumber > fetchingBlockNumber) return
78+
state.callResults[chainId][callKey] = {
79+
...state.callResults[chainId][callKey],
80+
fetchingBlockNumber
4381
}
4482
})
4583
})
84+
.addCase(errorFetchingMulticallResults, (state, { payload: { fetchingBlockNumber, chainId, calls } }) => {
85+
state.callResults[chainId] = state.callResults[chainId] ?? {}
86+
calls.forEach(call => {
87+
const callKey = toCallKey(call)
88+
const current = state.callResults[chainId][callKey]
89+
if (current && current.fetchingBlockNumber !== fetchingBlockNumber) return
90+
delete current.fetchingBlockNumber
91+
})
92+
})
4693
.addCase(updateMulticallResults, (state, { payload: { chainId, results, blockNumber } }) => {
4794
state.callResults[chainId] = state.callResults[chainId] ?? {}
4895
Object.keys(results).forEach(callKey => {

src/state/multicall/updater.tsx

Lines changed: 72 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -7,48 +7,96 @@ import useDebounce from '../../hooks/useDebounce'
77
import chunkArray from '../../utils/chunkArray'
88
import { useBlockNumber } from '../application/hooks'
99
import { AppDispatch, AppState } from '../index'
10-
import { parseCallKey, updateMulticallResults } from './actions'
10+
import {
11+
errorFetchingMulticallResults,
12+
fetchingMulticallResults,
13+
parseCallKey,
14+
updateMulticallResults
15+
} from './actions'
1116

1217
// chunk calls so we do not exceed the gas limit
1318
const CALL_CHUNK_SIZE = 250
1419

20+
/**
21+
* From the current all listeners state, return each call key mapped to the
22+
* minimum number of blocks per fetch. This is how often each key must be fetched.
23+
* @param allListeners the all listeners state
24+
* @param chainId the current chain id
25+
*/
26+
function activeListeningKeys(
27+
allListeners: AppState['multicall']['callListeners'],
28+
chainId?: number
29+
): { [callKey: string]: number } {
30+
if (!allListeners || !chainId) return {}
31+
const listeners = allListeners[chainId]
32+
if (!listeners) return {}
33+
34+
return Object.keys(listeners).reduce<{ [callKey: string]: number }>((memo, callKey) => {
35+
const keyListeners = listeners[callKey]
36+
37+
memo[callKey] = Object.keys(keyListeners)
38+
.filter(key => keyListeners[parseInt(key)] > 0)
39+
.reduce((previousMin, current) => {
40+
return Math.min(previousMin, parseInt(current))
41+
}, Infinity)
42+
return memo
43+
}, {})
44+
}
45+
1546
export default function Updater() {
1647
const dispatch = useDispatch<AppDispatch>()
1748
const state = useSelector<AppState, AppState['multicall']>(state => state.multicall)
49+
// wait for listeners to settle before triggering updates
50+
const debouncedListeners = useDebounce(state.callListeners, 100)
1851
const latestBlockNumber = useBlockNumber()
1952
const { chainId } = useActiveWeb3React()
2053
const multicallContract = useMulticallContract()
2154

22-
const listeningKeys = useMemo(() => {
23-
if (!chainId || !state.callListeners[chainId]) return []
24-
return Object.keys(state.callListeners[chainId]).filter(callKey => state.callListeners[chainId][callKey] > 0)
25-
}, [state.callListeners, chainId])
26-
27-
const debouncedResults = useDebounce(state.callResults, 20)
28-
const debouncedListeningKeys = useDebounce(listeningKeys, 20)
55+
const listeningKeys: { [callKey: string]: number } = useMemo(() => {
56+
return activeListeningKeys(debouncedListeners, chainId)
57+
}, [debouncedListeners, chainId])
2958

3059
const unserializedOutdatedCallKeys = useMemo(() => {
31-
if (!chainId || !debouncedResults[chainId]) return debouncedListeningKeys
32-
if (!latestBlockNumber) return []
60+
// wait for these before fetching any data
61+
if (!chainId || !latestBlockNumber) return []
62+
// no results at all, load everything
63+
if (!state.callResults[chainId]) return Object.keys(listeningKeys)
64+
65+
return Object.keys(listeningKeys).filter(callKey => {
66+
const blocksPerFetch = listeningKeys[callKey]
67+
68+
const data = state.callResults[chainId][callKey]
69+
// no data, must fetch
70+
if (!data) return true
3371

34-
return debouncedListeningKeys.filter(key => {
35-
const data = debouncedResults[chainId][key]
36-
return !data || data.blockNumber < latestBlockNumber
72+
// already fetching it
73+
if (data.fetchingBlockNumber && data.fetchingBlockNumber >= latestBlockNumber + blocksPerFetch) return false
74+
75+
// data block number is older than blocksPerFetch blocks
76+
return data.blockNumber <= latestBlockNumber - blocksPerFetch
3777
})
38-
}, [chainId, debouncedResults, debouncedListeningKeys, latestBlockNumber])
78+
}, [chainId, state.callResults, listeningKeys, latestBlockNumber])
3979

4080
const serializedOutdatedCallKeys = useMemo(() => JSON.stringify(unserializedOutdatedCallKeys.sort()), [
4181
unserializedOutdatedCallKeys
4282
])
4383

4484
useEffect(() => {
85+
if (!latestBlockNumber || !chainId || !multicallContract) return
86+
4587
const outdatedCallKeys: string[] = JSON.parse(serializedOutdatedCallKeys)
46-
if (!multicallContract || !chainId || outdatedCallKeys.length === 0) return
88+
if (outdatedCallKeys.length === 0) return
4789
const calls = outdatedCallKeys.map(key => parseCallKey(key))
4890

4991
const chunkedCalls = chunkArray(calls, CALL_CHUNK_SIZE)
5092

51-
console.debug('Firing off chunked calls', chunkedCalls)
93+
dispatch(
94+
fetchingMulticallResults({
95+
calls,
96+
chainId,
97+
fetchingBlockNumber: latestBlockNumber
98+
})
99+
)
52100

53101
chunkedCalls.forEach((chunk, index) =>
54102
multicallContract
@@ -73,9 +121,16 @@ export default function Updater() {
73121
})
74122
.catch((error: any) => {
75123
console.error('Failed to fetch multicall chunk', chunk, chainId, error)
124+
dispatch(
125+
errorFetchingMulticallResults({
126+
calls: chunk,
127+
chainId,
128+
fetchingBlockNumber: latestBlockNumber
129+
})
130+
)
76131
})
77132
)
78-
}, [chainId, multicallContract, dispatch, serializedOutdatedCallKeys])
133+
}, [chainId, multicallContract, dispatch, serializedOutdatedCallKeys, latestBlockNumber])
79134

80135
return null
81136
}

0 commit comments

Comments
 (0)