From 25f3592186884fd46533189b6bf5a454b018d713 Mon Sep 17 00:00:00 2001 From: aspnxdd Date: Wed, 17 Jun 2026 23:30:39 +0200 Subject: [PATCH 1/4] feat: improve efficiency array in multicall --- evm/evm-typegen/src/multicall.ts | 66 +++++++++++++---------- test/erc20-transfers/src/abi/multicall.ts | 66 +++++++++++++---------- 2 files changed, 76 insertions(+), 56 deletions(-) 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] { From b8bb3e7c80104653d0872cb90c76f24ae50fc033 Mon Sep 17 00:00:00 2001 From: aspnxdd Date: Wed, 17 Jun 2026 23:30:46 +0200 Subject: [PATCH 2/4] feat: add bench --- evm/evm-typegen/bench/multicall-loop.js | 97 +++++++++++++++++++++++++ 1 file changed, 97 insertions(+) create mode 100644 evm/evm-typegen/bench/multicall-loop.js diff --git a/evm/evm-typegen/bench/multicall-loop.js b/evm/evm-typegen/bench/multicall-loop.js new file mode 100644 index 000000000..1a5394c91 --- /dev/null +++ b/evm/evm-typegen/bench/multicall-loop.js @@ -0,0 +1,97 @@ +const { performance } = require("node:perf_hooks"); + +function* splitSlice(maxSize, beg, end = Number.MAX_SAFE_INTEGER) { + maxSize = Math.max(1, maxSize); + while (beg < end) { + const left = end - beg; + const splits = Math.ceil(left / maxSize); + const step = Math.round(left / splits); + yield [beg, beg + step]; + beg += step; + } +} + +function* splitArray(maxSize, arr) { + if (arr.length <= maxSize) { + yield arr; + } else { + for (const [beg, end] of splitSlice(maxSize, 0, arr.length)) { + yield arr.slice(beg, end); + } + } +} + +async function oldPattern(calls, pageSize) { + const pages = Array.from(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); +}); From 3ada8e8735bf7b0aa04d311f902da2128f44b05c Mon Sep 17 00:00:00 2001 From: aspnxdd Date: Wed, 17 Jun 2026 23:37:16 +0200 Subject: [PATCH 3/4] feat: add rush change file --- ...ve-efficiency-array-multicall_2026-06-17-21-37.json | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 common/changes/@subsquid/evm-typegen/feat-improve-efficiency-array-multicall_2026-06-17-21-37.json 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 From 0de69523f3591b9b6bc901f58e66694867c70600 Mon Sep 17 00:00:00 2001 From: aspnxdd Date: Mon, 29 Jun 2026 11:35:02 +0200 Subject: [PATCH 4/4] feat: simplify bench js file --- evm/evm-typegen/bench/multicall-loop.js | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/evm/evm-typegen/bench/multicall-loop.js b/evm/evm-typegen/bench/multicall-loop.js index 1a5394c91..d4df377eb 100644 --- a/evm/evm-typegen/bench/multicall-loop.js +++ b/evm/evm-typegen/bench/multicall-loop.js @@ -1,28 +1,32 @@ const { performance } = require("node:perf_hooks"); -function* splitSlice(maxSize, beg, end = Number.MAX_SAFE_INTEGER) { +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); - yield [beg, beg + step]; + slices.push([beg, beg + step]); beg += step; } + return slices; } -function* splitArray(maxSize, arr) { +function splitArray(maxSize, arr) { if (arr.length <= maxSize) { - yield arr; - } else { - for (const [beg, end] of splitSlice(maxSize, 0, arr.length)) { - yield arr.slice(beg, end); - } + 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 = Array.from(splitArray(pageSize, calls)); + const pages = splitArray(pageSize, calls); const results = await Promise.all( pages.map(async (page) => page.map((x) => x + 1)), );