diff --git a/common/changes/@subsquid/evm-typegen/feat-improve-efficiency-array-multicall_2026-06-17-21-37.json b/common/changes/@subsquid/evm-typegen/feat-improve-efficiency-array-multicall_2026-06-17-21-37.json new file mode 100644 index 000000000..b5c3d1187 --- /dev/null +++ b/common/changes/@subsquid/evm-typegen/feat-improve-efficiency-array-multicall_2026-06-17-21-37.json @@ -0,0 +1,10 @@ +{ + "changes": [ + { + "packageName": "@subsquid/evm-typegen", + "comment": "improve efficiency of array manipulation in multicall eth_calls promises", + "type": "patch" + } + ], + "packageName": "@subsquid/evm-typegen" +} \ No newline at end of file diff --git a/evm/evm-typegen/bench/multicall-loop.js b/evm/evm-typegen/bench/multicall-loop.js new file mode 100644 index 000000000..d4df377eb --- /dev/null +++ b/evm/evm-typegen/bench/multicall-loop.js @@ -0,0 +1,101 @@ +const { performance } = require("node:perf_hooks"); + +function splitSlice(maxSize, beg, end = Number.MAX_SAFE_INTEGER) { + maxSize = Math.max(1, maxSize); + const slices = []; + while (beg < end) { + const left = end - beg; + const splits = Math.ceil(left / maxSize); + const step = Math.round(left / splits); + slices.push([beg, beg + step]); + beg += step; + } + return slices; +} + +function splitArray(maxSize, arr) { + if (arr.length <= maxSize) { + return [arr]; + } + + const pages = []; + for (const [beg, end] of splitSlice(maxSize, 0, arr.length)) { + pages.push(arr.slice(beg, end)); + } + return pages; +} + +async function oldPattern(calls, pageSize) { + const pages = splitArray(pageSize, calls); + const results = await Promise.all( + pages.map(async (page) => page.map((x) => x + 1)), + ); + return results.flat(); +} + +async function newPattern(calls, pageSize) { + const promises = []; + for (const page of splitArray(pageSize, calls)) { + promises.push(Promise.resolve(page.map((x) => x + 1))); + } + + const result = []; + for (const group of await Promise.all(promises)) { + result.push(...group); + } + return result; +} + +async function bench(name, fn, calls, pageSize, rounds) { + for (let i = 0; i < 20; i++) { + await fn(calls, pageSize); + } + + const start = performance.now(); + let checksum = 0; + for (let i = 0; i < rounds; i++) { + const result = await fn(calls, pageSize); + checksum += result.length + result[0] + result[result.length - 1]; + } + const ms = performance.now() - start; + + return { name, ms, checksum }; +} + +async function run(size, pageSize, rounds) { + const calls = Array.from({ length: size }, (_, i) => i); + const oldResult = await bench("old", oldPattern, calls, pageSize, rounds); + const newResult = await bench("new", newPattern, calls, pageSize, rounds); + + return { + size, + pageSize, + rounds, + oldMs: oldResult.ms, + newMs: newResult.ms, + speedup: oldResult.ms / newResult.ms, + same: oldResult.checksum === newResult.checksum, + }; +} + +async function main() { + const cases = [ + { size: 1000, pageSize: 50, rounds: 5000 }, + { size: 10000, pageSize: 100, rounds: 1000 }, + ]; + + for (const testCase of cases) { + console.log( + JSON.stringify( + await run(testCase.size, testCase.pageSize, testCase.rounds), + null, + 2, + ), + ); + } +} + +main().catch((err) => { + console.error(err); + process.exit(1); +}); diff --git a/evm/evm-typegen/src/multicall.ts b/evm/evm-typegen/src/multicall.ts index 17783e9ea..dd60668ef 100644 --- a/evm/evm-typegen/src/multicall.ts +++ b/evm/evm-typegen/src/multicall.ts @@ -67,15 +67,20 @@ export class Multicall extends ContractBase { let [calls, pageSize] = this.makeCalls(args) if (calls.length === 0) return [] - const pages = Array.from(splitArray(pageSize, calls)) - const results = await Promise.all( - pages.map(async (page) => { - const {returnData} = await this.eth_call(aggregate, {calls: page}) - return returnData.map((data, i) => page[i].func.decodeResult(data)) - }), - ) + const promises: Promise[]>[] = [] + for (let page of splitArray(pageSize, calls)) { + promises.push( + this.eth_call(aggregate, {calls: page}).then(({returnData}) => { + return returnData.map((data, i) => page[i].func.decodeResult(data)) + }), + ) + } - return results.flat() + const result: FunctionReturn[] = [] + for (let group of await Promise.all(promises)) { + result.push(...group) + } + return result } tryAggregate( @@ -97,31 +102,36 @@ export class Multicall extends ContractBase { let [calls, pageSize] = this.makeCalls(args) if (calls.length === 0) return [] - const pages = Array.from(splitArray(pageSize, calls)) - const results = await Promise.all( - pages.map(async (page) => { - const response = await this.eth_call(tryAggregate, { + const promises: Promise[]>[] = [] + for (let page of splitArray(pageSize, calls)) { + promises.push( + this.eth_call(tryAggregate, { requireSuccess: false, calls: page, - }) - return response.map((res, i) => { - if (res.success) { - try { - return { - success: true, - value: page[i].func.decodeResult(res.returnData), + }).then((response) => { + return response.map((res, i) => { + if (res.success) { + try { + return { + success: true, + value: page[i].func.decodeResult(res.returnData), + } + } catch (err: any) { + return {success: false, returnData: res.returnData} } - } catch (err: any) { - return {success: false, returnData: res.returnData} + } else { + return {success: false} } - } else { - return {success: false} - } - }) - }), - ) + }) + }), + ) + } - return results.flat() + const result: MulticallResult[] = [] + for (let group of await Promise.all(promises)) { + result.push(...group) + } + return result } private makeCalls(args: any[]): [calls: Call[], page: number] { diff --git a/test/erc20-transfers/src/abi/multicall.ts b/test/erc20-transfers/src/abi/multicall.ts index 17783e9ea..dd60668ef 100644 --- a/test/erc20-transfers/src/abi/multicall.ts +++ b/test/erc20-transfers/src/abi/multicall.ts @@ -67,15 +67,20 @@ export class Multicall extends ContractBase { let [calls, pageSize] = this.makeCalls(args) if (calls.length === 0) return [] - const pages = Array.from(splitArray(pageSize, calls)) - const results = await Promise.all( - pages.map(async (page) => { - const {returnData} = await this.eth_call(aggregate, {calls: page}) - return returnData.map((data, i) => page[i].func.decodeResult(data)) - }), - ) + const promises: Promise[]>[] = [] + for (let page of splitArray(pageSize, calls)) { + promises.push( + this.eth_call(aggregate, {calls: page}).then(({returnData}) => { + return returnData.map((data, i) => page[i].func.decodeResult(data)) + }), + ) + } - return results.flat() + const result: FunctionReturn[] = [] + for (let group of await Promise.all(promises)) { + result.push(...group) + } + return result } tryAggregate( @@ -97,31 +102,36 @@ export class Multicall extends ContractBase { let [calls, pageSize] = this.makeCalls(args) if (calls.length === 0) return [] - const pages = Array.from(splitArray(pageSize, calls)) - const results = await Promise.all( - pages.map(async (page) => { - const response = await this.eth_call(tryAggregate, { + const promises: Promise[]>[] = [] + for (let page of splitArray(pageSize, calls)) { + promises.push( + this.eth_call(tryAggregate, { requireSuccess: false, calls: page, - }) - return response.map((res, i) => { - if (res.success) { - try { - return { - success: true, - value: page[i].func.decodeResult(res.returnData), + }).then((response) => { + return response.map((res, i) => { + if (res.success) { + try { + return { + success: true, + value: page[i].func.decodeResult(res.returnData), + } + } catch (err: any) { + return {success: false, returnData: res.returnData} } - } catch (err: any) { - return {success: false, returnData: res.returnData} + } else { + return {success: false} } - } else { - return {success: false} - } - }) - }), - ) + }) + }), + ) + } - return results.flat() + const result: MulticallResult[] = [] + for (let group of await Promise.all(promises)) { + result.push(...group) + } + return result } private makeCalls(args: any[]): [calls: Call[], page: number] {