ethers-io / ethers.js

Complete Ethereum library and wallet implementation in JavaScript.
https://ethers.org/
MIT License
8.01k stars 1.87k forks source link

Performance regression when asked for multiple tokens balances #4865

Open tommasini opened 1 month ago

tommasini commented 1 month ago

Ethers Version

6.13.4

Search Terms

performance

Describe the Problem

Currently trying version ^6 to see if a performance issue was solved on version ^5 (ethersproject/contracts).

Using this abi single-call-balance-checker-abi, asking for multiple (1000 each time) tokens balances against the account address, created a big drop of JavaScript FPS on Android. When tried to do it with version ^6 the time changed from 9821 ms to 19725 ms.

The way it was tested:

    const contract = new Contract(
      contractAddress,
      abiSingleCallBalancesContract,
      provider,
    );
    const start = Date.now();
    const result = await contract.balances([selectedAddress], tokensToDetect);
    const end = Date.now();
    console.log('ENTER time spent on contract.balances:', end - start);

tokensToDetect = ["0xe5d7c2a44ffddf6b295a15c148167daaaf5cf34f", "0x1e1f509963a6d33e169d9497b11c7dbfe73b7f13",...]

I was able to trace down on version 5 a recursive function that it was using a lot of computational needs, that it was really noticeable with Android (Hermes enabled), making the app freeze/slow down. I was not able to trace down what happened on version 6. Would love some thoughts from the maintainers! Also I'm here to help to trace down the issue.

If the maintainers have also an idea of how we could improve the performance on v^5 or create a fix for v^6 be more performative than v^5 I'm here to help as well!

Thanks for your time!

Code Snippet

const contract = new Contract(
      contractAddress,
      abiSingleCallBalancesContract,
      provider,
    );
    const start = Date.now();
    const result = await contract.balances([selectedAddress], tokensToDetect);
    const end = Date.now();
    console.log('ENTER time spent on contract.balances:', end - start);

### Contract ABI

```shell
module.exports = [
    {
     "payable": true,
     "stateMutability": "payable",
     "type": "fallback"
    },
    {
     "constant": true,
     "inputs": [
      {
       "name": "user",
       "type": "address"
      },
      {
       "name": "token",
       "type": "address"
      }
     ],
     "name": "tokenBalance",
     "outputs": [
      {
       "name": "",
       "type": "uint256"
      }
     ],
     "payable": false,
     "stateMutability": "view",
     "type": "function"
    },
    {
     "constant": true,
     "inputs": [
      {
       "name": "users",
       "type": "address[]"
      },
      {
       "name": "tokens",
       "type": "address[]"
      }
     ],
     "name": "balances",
     "outputs": [
      {
       "name": "",
       "type": "uint256[]"
      }
     ],
     "payable": false,
     "stateMutability": "view",
     "type": "function"
    }
   ]

### Errors

```shell
n/a

Environment

Ethereum (mainnet/ropsten/rinkeby/goerli), React Native/Expo/JavaScriptCore

Environment (Other)

Android with Hermes

ricmoo commented 15 hours ago

This should not have changed much between v5 and v6, but for your purposes, I believe you likely want to use the MulticallProvider, which will automatically batch all the calls into a single multi-call eth_call.

It should handle everything for you, as long as you preserve the async context, using something akin to:

const multicallProvider = new MulticallProvider(provider);
const balances = await Promise.all(tokens.map((tokenAddr) => {
    const contract = new Contract(tokenAddr, tokenAbi, multicallProvider);
    return contract.balanceOf(addr);
}));

Each Contract call will add to the list of calls, and the MulticallProvider will prepare a single call that can resolve many calls made locally from the JSON-RPC node.