Skip to content

perf: cache signature hashes by normalized signature - #570

Merged
gakonst merged 1 commit into
mainfrom
perf/signature-hash-cache
Nov 3, 2026
Merged

gakonst merged 1 commit into
mainfrom
perf/signature-hash-cache

Conversation

@gakonst

@gakonst gakonst commented Nov 3, 2026

Copy link
Copy Markdown
Collaborator

AbiItem.getSignatureHash keccak-hashes the signature on every call for ABI items that have no precomputed hash (raw JSON ABIs, or items created with prepare: false). Selector lookups call it for every item they compare against:

  • AbiItem.fromAbi(abi, selector)
  • AbiFunction.decodeData(abi, data)
  • AbiEvent.decodeLog and AbiEvent.extractLogs, once per log

That is one keccak for each ABI item that comes before the match. This is the same cost viem removed in wevm/viem#5392.

Change

  • getSignatureHash now caches the hash in a bounded map with 8192 entries, keyed by the normalized signature string (for example ownerOf(uint256)). A hit skips Hex.fromString and keccak256. The key is the signature string, not the ABI item object, so a mutated or re-created item can't read a stale hash, and equal signatures share one entry.
  • The map is in a new src/core/internal/caches.ts, next to the existing checksum cache. Caches.clear() resets both. Engine.set, Engine.reset and Engine.with already call Caches.clear(), so a swapped keccak engine never sees hashes computed by a different engine.
  • Only Caches.checksum remains public, so the exports of Caches are unchanged and there are no public API or type changes.
  • Tests: cache keyed by normalized signature (string and object forms share one entry); hashes recomputed under Engine.with; Caches.clear() empties the signature cache.
  • Added a patch changeset.

Measurements

Node 22, CPU time, min of 5 runs, main vs this branch. These are steady-state numbers, where the same ABI is used repeatedly:

main this PR
AbiItem.fromAbi(abi200, selector) (last of 180 functions) 4.7 ms 0.22 ms
AbiItem.getSelector (unprepared item) 26 µs 1.5 µs
AbiFunction.decodeData(erc20Abi, data) 113 µs 11–13 µs
AbiEvent.extractLogs(erc20Abi, 100 logs) 8.2 ms 0.74 ms

The remaining cost of a lookup is formatAbiItem and normalizeSignature, which still run on every call because the key comes from them. A cache miss costs one extra Map lookup and insert.

Verification

  • pnpm check:types (tsc -b) passed.
  • vp check (format and type-aware lint) passed on the changed files.
  • SKIP_GLOBAL_SETUP=1 pnpm test --project core passed for AbiItem, Caches, Engine, AbiFunction, Abi, Address, TypedData and index.
  • Without Anvil, the network-backed cases in AbiEvent, AbiConstructor and AbiError failed to connect (ECONNREFUSED 127.0.0.1:8545). CI runs them with Anvil.
  • I committed with the hook skipped, because the full-repo pre-commit vp check --fix doesn't finish on this loaded machine. I ran the same check on the changed files.

Follow-up to #569.

`AbiItem.getSignatureHash` keccak-hashed the signature on every call
for ABI items without a precomputed `hash`. Selector lookups call it
for every item they compare: `AbiItem.fromAbi` with a selector,
`AbiFunction.decodeData` with an ABI, and `AbiEvent.decodeLog` and
`AbiEvent.extractLogs` for each log.

Memoize the hash in a bounded (8192-entry) map keyed by the normalized
signature string. The key is the signature itself rather than the ABI
item object, so a mutated or re-created item can never read a stale
hash. The map sits next to the checksum cache in a new
`internal/caches.ts`, so `Caches.clear()` resets it, and so does
every `Engine.set`/`reset`/`with` call (they already call
`Caches.clear`). Only `Caches.checksum` stays public.
@vercel

vercel Bot commented Nov 3, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
ox Error Error Nov 3, 2026 2:14am UTC

Request Review

@github-actions

github-actions Bot commented Nov 3, 2026

Copy link
Copy Markdown
Contributor

Published ox@0.0.0-pr-570-c351af5 under the pr-570 dist-tag, which follows this pull request until it closes.

pnpm add ox@pr-570
pnpm add ox@0.0.0-pr-570-c351af5

@gakonst
gakonst merged commit 8a9f850 into main Nov 3, 2026
15 of 16 checks passed
@gakonst
gakonst deleted the perf/signature-hash-cache branch November 3, 2026 02:15
@github-actions github-actions Bot mentioned this pull request Nov 3, 2026
gakonst added a commit that referenced this pull request Nov 3, 2026
Backports #569 and #570 from main.

#569: Regex literals inside a function body allocate a fresh RegExp on
every evaluation. Move the ones on hot validation paths to module scope:
`Hex.assert` (strict), `Value.format`/`Value.from`, `TypedData` type
dependency and field validation, `Siwe.isUri`, frame nonce RPC
decoding, and the integer and fixed-bytes checks in ABI overload
resolution. All of them are non-global and non-sticky, so sharing them
holds no `lastIndex` state. `isArgOfType` also reads the integer bit
size from the integer type regex capture instead of running a second
`replace` regex. 0.x has no `ox/zod` entrypoint, and keeps its own
`Value.from` decimal pattern, so behavior is unchanged.

#570: `AbiItem.getSignatureHash` keccak-hashed the signature on every
call for ABI items without a precomputed `hash`. Memoize the hash in a
bounded (8192-entry) LRU map keyed by the normalized signature string,
next to the checksum cache in a new `internal/caches.ts`, so
`Caches.clear()` resets it. Only `Caches.checksum` stays public. 0.x
has no `Engine`, so keccak256 cannot be swapped and the engine test is
not ported.

This branch had an error being deployed

1 failed deployment
Preview — c351af5b Deployed Nov 3, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant