feat(pxe): speculatively sync predicted contract calls - #25126
Conversation
| utilityExecutor, | ||
| scope, | ||
| ), | ||
| async ensureContractSynced({ |
There was a problem hiding this comment.
Decided to change the params with an obj param, so that it was easier to understand what each param was
| * Waits until every sync the job started has settled, so all its staged writes land before the job's stores | ||
| * commit or discard. Never rejects: sync failures are surfaced by the requests that await them, not here. | ||
| */ | ||
| async settle(jobId: JobId): Promise<void> { |
There was a problem hiding this comment.
Had to add this to the store's interface, so that we could wait for all syncs to be settled (either worked or failed). More about it on the JobCoordinator
nventuro
left a comment
There was a problem hiding this comment.
(these are human comments, I've not yet gone over the thing with ai)
| contractClassService, | ||
| noteStore, | ||
| createLogger('test:contract-sync'), | ||
| true, // concurrentContractSyncEnabled |
There was a problem hiding this comment.
This is full of these comments. What if we just make this an object? Raw booleans are unparseable
There was a problem hiding this comment.
Ok, replaced the bool with ContractSyncConfig so that it's easier to read. Could replace all the args with an object if you prefer
Why we are doing this
The last few weeks of PXE work have been about cutting RPC calls and round trips (#25033, #25074, #25076, #25089, #25116). Measuring what is left on the
transfersbenchmark, contract sync is now the single biggest source: 67% of the calls and 80% of the round trips.It is also the hardest part to run in parallel, because a job's contract dependencies are only discovered as execution reaches them. A transfer that touches an account, a token and an FPC syncs them one after another, so each contract's round trips wait on every earlier contract's.
Our fix
Predict the contracts a job is about to need from its past runs, so their syncs can all start at once.
Best case, all the syncs collapse into a single round trip and every one of them was needed. Worst case, we spend extra calls on contracts the job never uses, but they ride the same round trip, so they cost no extra latency. A speculative sync that fails is dropped from the cache, so it cannot fail a job that did not need it, and the next request retries it from scratch.
Configuration
This is experimental, so it is off by default (
PXE_CONCURRENT_CONTRACT_SYNC_ENABLED). The prediction is only as good as the workload is repetitive: a general purpose wallet running many different flows may see little from it, while an embedded wallet doing the same operation over and over should benefit.Metrics
BENCHMARK_CONFIG=key_flowson the transfers bench. Each of the four flows runs three times so the predictor reaches steady state. Both sides are the same build toggled by the env var, so the delta is the feature and nothing else (the disabled path returns before any bookkeeping).Round trips, disabled → enabled, flows in execution order:
sponsored_fpc, 0 recursionssponsored_fpc, 1 recursionprivate_fpc, 0 recursionsprivate_fpc, 1 recursionOnce the dependencies are learned the flows cost 17% fewer round trips. Only same run columns are comparable, because the disabled side improves across runs too: a contract whose notes are not in the store yet skips its nullifier lookup entirely, so its class id read travels alone, and once an earlier run has populated the store the two reads share a round trip.
Calls are learned per function, not per flow, and all four flows enter through the same account entrypoint and share the token. Only the very first flow's first run is genuinely cold, and it is reported unchanged because nothing is predicted there; by the second run the threshold is already met, since the change sending transaction between runs is another job making the same calls.
Call counts are identical on both sides in every run (144 on the first, 142 afterwards), so every prediction that fired was a hit and this workload wasted nothing.