perf(multicall): improve fetching code to allow for fetching immutable data like token symbols/names/decimals

This commit is contained in:
Moody Salem 2020-05-29 13:23:49 -04:00
parent 87d24c404b
commit 5e486fca7f
No known key found for this signature in database
GPG Key ID: 8CB5CD10385138DB
4 changed files with 153 additions and 36 deletions

@ -30,8 +30,25 @@ export function parseCallKey(callKey: string): Call {
}
}
export const addMulticallListeners = createAction<{ chainId: number; calls: Call[] }>('addMulticallListeners')
export const removeMulticallListeners = createAction<{ chainId: number; calls: Call[] }>('removeMulticallListeners')
interface ListenerOptions {
// how often this data should be fetched, by default 1
blocksPerFetch?: number
}
export const addMulticallListeners = createAction<{ chainId: number; calls: Call[]; options?: ListenerOptions }>(
'addMulticallListeners'
)
export const removeMulticallListeners = createAction<{ chainId: number; calls: Call[]; options?: ListenerOptions }>(
'removeMulticallListeners'
)
export const fetchingMulticallResults = createAction<{ chainId: number; calls: Call[]; fetchingBlockNumber: number }>(
'fetchingMulticallResults'
)
export const errorFetchingMulticallResults = createAction<{
chainId: number
calls: Call[]
fetchingBlockNumber: number
}>('errorFetchingMulticallResults')
export const updateMulticallResults = createAction<{
chainId: number
blockNumber: number

@ -53,11 +53,9 @@ function useCallsData(calls: (Call | undefined)[]): CallResult[] {
[calls]
)
const debouncedSerializedCallKeys = useDebounce(serializedCallKeys, 20)
// update listeners when there is an actual change that persists for at least 100ms
useEffect(() => {
const callKeys: string[] = JSON.parse(debouncedSerializedCallKeys)
const callKeys: string[] = JSON.parse(serializedCallKeys)
if (!chainId || callKeys.length === 0) return
const calls = callKeys.map(key => parseCallKey(key))
dispatch(
@ -75,7 +73,7 @@ function useCallsData(calls: (Call | undefined)[]): CallResult[] {
})
)
}
}, [chainId, dispatch, debouncedSerializedCallKeys])
}, [chainId, dispatch, serializedCallKeys])
return useMemo(
() =>

@ -1,10 +1,22 @@
import { createReducer } from '@reduxjs/toolkit'
import { addMulticallListeners, removeMulticallListeners, toCallKey, updateMulticallResults } from './actions'
import {
addMulticallListeners,
errorFetchingMulticallResults,
fetchingMulticallResults,
removeMulticallListeners,
toCallKey,
updateMulticallResults
} from './actions'
interface MulticallState {
callListeners: {
callListeners?: {
// on a per-chain basis
[chainId: number]: {
[callKey: string]: number
// stores for each call key the listeners' preferences
[callKey: string]: {
// stores how many listeners there are per each blocks per fetch preference
[blocksPerFetch: number]: number
}
}
}
@ -13,35 +25,70 @@ interface MulticallState {
[callKey: string]: {
data: string | null
blockNumber: number
fetchingBlockNumber?: number
}
}
}
}
const initialState: MulticallState = {
callListeners: {},
callResults: {}
}
export default createReducer(initialState, builder =>
builder
.addCase(addMulticallListeners, (state, { payload: { calls, chainId } }) => {
state.callListeners[chainId] = state.callListeners[chainId] ?? {}
.addCase(addMulticallListeners, (state, { payload: { calls, chainId, options: { blocksPerFetch = 1 } = {} } }) => {
const listeners: MulticallState['callListeners'] = state.callListeners
? state.callListeners
: (state.callListeners = {})
listeners[chainId] = listeners[chainId] ?? {}
calls.forEach(call => {
const callKey = toCallKey(call)
state.callListeners[chainId][callKey] = (state.callListeners[chainId][callKey] ?? 0) + 1
listeners[chainId][callKey] = listeners[chainId][callKey] ?? {}
listeners[chainId][callKey][blocksPerFetch] = (listeners[chainId][callKey][blocksPerFetch] ?? 0) + 1
})
})
.addCase(removeMulticallListeners, (state, { payload: { chainId, calls } }) => {
if (!state.callListeners[chainId]) return
.addCase(
removeMulticallListeners,
(state, { payload: { chainId, calls, options: { blocksPerFetch = 1 } = {} } }) => {
const listeners: MulticallState['callListeners'] = state.callListeners
? state.callListeners
: (state.callListeners = {})
if (!listeners[chainId]) return
calls.forEach(call => {
const callKey = toCallKey(call)
if (state.callListeners[chainId][callKey] === 1) {
delete state.callListeners[chainId][callKey]
if (!listeners[chainId][callKey]) return
if (!listeners[chainId][callKey][blocksPerFetch]) return
if (listeners[chainId][callKey][blocksPerFetch] === 1) {
delete listeners[chainId][callKey][blocksPerFetch]
} else {
state.callListeners[chainId][callKey]--
listeners[chainId][callKey][blocksPerFetch]--
}
})
}
)
.addCase(fetchingMulticallResults, (state, { payload: { chainId, fetchingBlockNumber, calls } }) => {
state.callResults[chainId] = state.callResults[chainId] ?? {}
calls.forEach(call => {
const callKey = toCallKey(call)
const current = state.callResults[chainId][callKey]
if (current && current.blockNumber > fetchingBlockNumber) return
state.callResults[chainId][callKey] = {
...state.callResults[chainId][callKey],
fetchingBlockNumber
}
})
})
.addCase(errorFetchingMulticallResults, (state, { payload: { fetchingBlockNumber, chainId, calls } }) => {
state.callResults[chainId] = state.callResults[chainId] ?? {}
calls.forEach(call => {
const callKey = toCallKey(call)
const current = state.callResults[chainId][callKey]
if (current && current.fetchingBlockNumber !== fetchingBlockNumber) return
delete current.fetchingBlockNumber
})
})
.addCase(updateMulticallResults, (state, { payload: { chainId, results, blockNumber } }) => {
state.callResults[chainId] = state.callResults[chainId] ?? {}

@ -7,48 +7,96 @@ import useDebounce from '../../hooks/useDebounce'
import chunkArray from '../../utils/chunkArray'
import { useBlockNumber } from '../application/hooks'
import { AppDispatch, AppState } from '../index'
import { parseCallKey, updateMulticallResults } from './actions'
import {
errorFetchingMulticallResults,
fetchingMulticallResults,
parseCallKey,
updateMulticallResults
} from './actions'
// chunk calls so we do not exceed the gas limit
const CALL_CHUNK_SIZE = 250
/**
* From the current all listeners state, return each call key mapped to the
* minimum number of blocks per fetch. This is how often each key must be fetched.
* @param allListeners the all listeners state
* @param chainId the current chain id
*/
function activeListeningKeys(
allListeners: AppState['multicall']['callListeners'],
chainId?: number
): { [callKey: string]: number } {
if (!allListeners || !chainId) return {}
const listeners = allListeners[chainId]
if (!listeners) return {}
return Object.keys(listeners).reduce<{ [callKey: string]: number }>((memo, callKey) => {
const keyListeners = listeners[callKey]
memo[callKey] = Object.keys(keyListeners)
.filter(key => keyListeners[parseInt(key)] > 0)
.reduce((previousMin, current) => {
return Math.min(previousMin, parseInt(current))
}, Infinity)
return memo
}, {})
}
export default function Updater() {
const dispatch = useDispatch<AppDispatch>()
const state = useSelector<AppState, AppState['multicall']>(state => state.multicall)
// wait for listeners to settle before triggering updates
const debouncedListeners = useDebounce(state.callListeners, 100)
const latestBlockNumber = useBlockNumber()
const { chainId } = useActiveWeb3React()
const multicallContract = useMulticallContract()
const listeningKeys = useMemo(() => {
if (!chainId || !state.callListeners[chainId]) return []
return Object.keys(state.callListeners[chainId]).filter(callKey => state.callListeners[chainId][callKey] > 0)
}, [state.callListeners, chainId])
const debouncedResults = useDebounce(state.callResults, 20)
const debouncedListeningKeys = useDebounce(listeningKeys, 20)
const listeningKeys: { [callKey: string]: number } = useMemo(() => {
return activeListeningKeys(debouncedListeners, chainId)
}, [debouncedListeners, chainId])
const unserializedOutdatedCallKeys = useMemo(() => {
if (!chainId || !debouncedResults[chainId]) return debouncedListeningKeys
if (!latestBlockNumber) return []
// wait for these before fetching any data
if (!chainId || !latestBlockNumber) return []
// no results at all, load everything
if (!state.callResults[chainId]) return Object.keys(listeningKeys)
return debouncedListeningKeys.filter(key => {
const data = debouncedResults[chainId][key]
return !data || data.blockNumber < latestBlockNumber
return Object.keys(listeningKeys).filter(callKey => {
const blocksPerFetch = listeningKeys[callKey]
const data = state.callResults[chainId][callKey]
// no data, must fetch
if (!data) return true
// already fetching it
if (data.fetchingBlockNumber && data.fetchingBlockNumber >= latestBlockNumber + blocksPerFetch) return false
// data block number is older than blocksPerFetch blocks
return data.blockNumber <= latestBlockNumber - blocksPerFetch
})
}, [chainId, debouncedResults, debouncedListeningKeys, latestBlockNumber])
}, [chainId, state.callResults, listeningKeys, latestBlockNumber])
const serializedOutdatedCallKeys = useMemo(() => JSON.stringify(unserializedOutdatedCallKeys.sort()), [
unserializedOutdatedCallKeys
])
useEffect(() => {
if (!latestBlockNumber || !chainId || !multicallContract) return
const outdatedCallKeys: string[] = JSON.parse(serializedOutdatedCallKeys)
if (!multicallContract || !chainId || outdatedCallKeys.length === 0) return
if (outdatedCallKeys.length === 0) return
const calls = outdatedCallKeys.map(key => parseCallKey(key))
const chunkedCalls = chunkArray(calls, CALL_CHUNK_SIZE)
console.debug('Firing off chunked calls', chunkedCalls)
dispatch(
fetchingMulticallResults({
calls,
chainId,
fetchingBlockNumber: latestBlockNumber
})
)
chunkedCalls.forEach((chunk, index) =>
multicallContract
@ -73,9 +121,16 @@ export default function Updater() {
})
.catch((error: any) => {
console.error('Failed to fetch multicall chunk', chunk, chainId, error)
dispatch(
errorFetchingMulticallResults({
calls: chunk,
chainId,
fetchingBlockNumber: latestBlockNumber
})
)
}, [chainId, multicallContract, dispatch, serializedOutdatedCallKeys])
})
)
}, [chainId, multicallContract, dispatch, serializedOutdatedCallKeys, latestBlockNumber])
return null
}