perf(contracts): optimize contract gas usage and establish benchmarks#31
Open
mftee wants to merge 1 commit into
Open
perf(contracts): optimize contract gas usage and establish benchmarks#31mftee wants to merge 1 commit into
mftee wants to merge 1 commit into
Conversation
- counter: move COUNTER from persistent to temporary storage (cheaper reads/writes, safe here since the counter carries no financial value) - betting: remove MatchStats.bettors, an unbounded Vec<Address> that was appended to and rewritten in full on every place_bet call despite never being read anywhere — this made place_bet's storage cost grow linearly with the number of bettors on a match - betting: get_bet/Bet intentionally kept on persistent storage and documented why (holds real funds; temporary storage risks fund loss on TTL expiry) - add gas_bench test modules to counter and betting measuring cpu_instruction_cost/memory_bytes_cost per function via env.budget(), asserting against configurable thresholds - wire a "Gas budget benchmarks" step into contracts.yml CI so a regression fails the build - fix counter/betting/rewards/player-nft Cargo.toml: none declared soroban-sdk's `testutils` feature in [dev-dependencies] (oracle and vault already did), so none of their #[cfg(test)] modules had ever actually compiled — cargo test was never run in CI, only cargo check - docs/gas-benchmarks.md: target/actual table and rationale for the above storage decisions Closes Exquisitech#23
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
counter:COUNTERmoved from persistent to temporary storage. Temporary entries are cheaper to read/write (no long-term rent accounting). Safe here because the counter carries no financial value — worst case on TTL lapse is a silent reset to 0.betting: removedMatchStats.bettors: Vec<Address>— it was appended to and the entireMatchStatsrecord rewritten on every singleplace_betcall, but the field was never read anywhere (not even byclaim_payout). This madeplace_bet's storage cost grow linearly with the number of bettors already on a match. Removing it restores constant per-call cost.betting:get_bet/Betintentionally kept on persistent storage, with the rationale documented in the module doc comment —Betholds real transferred funds, aclaimedflag, and apayout; moving it to temporary storage risks unrecoverable fund loss if the TTL lapses before a bettor claims/refunds. The issue asked to moveget_count/get_betto temporary storage "where safe" — forget_betspecifically, it isn't.gas_benchtest modules tocounterandbettingthat measurecpu_instruction_cost/memory_bytes_costper function viaenv.budget()and assert against configurable thresholds (seedocs/gas-benchmarks.md).Gas budget benchmarksstep to.github/workflows/contracts.ymlrunning these twogas_benchsuites so a regression fails CI.A blocking pre-existing issue I found and partially fixed
None of
counter,betting,rewards, orplayer-nft'sCargo.tomldeclaredsoroban-sdk'stestutilsfeature in[dev-dependencies](oracleandvaultalready had the correct pattern). That meantEnv::mock_all_auths,Address::generate,env.register_contract, etc. were unavailable, and none of these four crates'#[cfg(test)]modules had ever actually compiled — invisible until now becausecontracts.ymlonly rancargo check, nevercargo test. I fixed theCargo.tomlgap in all four (copying the working pattern fromoracle/vault), since my newgas_benchtests needcargo testto actually work.This surfaced further pre-existing, unrelated bugs in
betting's existingmod testthat were also never caught for the same reason (several call sites treat an already-auto-unwrapped client method return value as aResultand call.unwrap()on it again; one helper function is missing a lifetime annotation). I fixed two of these opportunistically while getting my own code to compile (theupgrade/try_upgradecall intest_upgrade_requires_two_governance_approvals_and_preserves_state, and the missing lifetime on theinitializetest helper), but did not do a full audit of the rest ofmod test— that's a pre-existing problem spanning the whole file and out of scope for this gas-optimization issue.Practical consequence: I could not get a clean local
cargo test --releaserun to completion — beyond the remaining pre-existing test bugs, this Windows dev environment also hit unrelated toolchain issues (missingdlltool.exefor the-gnutarget, incomplete MSVC "C++ build tools" for the-msvctarget) that block linking locally regardless. TheGas budget benchmarksCI job on this PR is the first environment where this can actually be verified end-to-end, and it may currently fail on the remaining pre-existing bugs inbetting'smod testrather than on anything in this diff. I'd rather flag that clearly than claim a green run I couldn't produce. Happy to keep iterating from the actual CI output, or to split the pre-existing-bug cleanup into its own PR if that's preferred.docs/gas-benchmarks.md
Records the budget thresholds enforced by
gas_benchand the rationale for each storage decision above. The "Actual" measured values are marked pending the first real CI run for the reason above, rather than backfilled with numbers I couldn't actually produce locally.Closes #23