Skip to content

Latest commit

 

History

History
62 lines (41 loc) · 3.37 KB

File metadata and controls

62 lines (41 loc) · 3.37 KB

Gas Tuning Guide

This guide explains how to interpret the gas snapshots generated by our CI and which knobs to turn when you need to optimize CPU or memory usage in RemitWise contracts. It is intended for contributors writing code, reviewers verifying intent, and the support team answering performance questions.

How to Interpret Gas Snapshots

When gas benchmarks run, results are captured in gas_results.json. Each entry represents a specific contract operation and its resource cost.

Here is a concrete example from our remittance_split benchmarks:

{
  "contract": "remittance_split",
  "method": "distribute_usdc",
  "scenario": "4_recipients_all_nonzero",
  "cpu": 787445,
  "mem": 115986
}

Understanding the Fields

  • contract / method: The exact smart contract and entrypoint being benchmarked (e.g., the distribute_usdc function in the remittance_split crate).
  • scenario: The specific state or scale being tested. Benchmarks intentionally target worst-case scenarios (e.g., 50_schedules_worst_case) alongside happy paths to ensure limits hold under pressure.
  • cpu: Soroban CPU instructions consumed. If this spikes past threshold percentages (defined in benchmarks/thresholds.json), CI will block the PR.
  • mem: Peak memory bytes allocated during execution.

Which Knobs to Turn

When a benchmark fails or a contract feels expensive, consider these specific tuning strategies:

1. Storage and Data Access (Highest Impact)

The most expensive operations in Soroban are state reads and writes.

  • Cache aggregates for heavy reads: If cpu is too high on read-heavy paths, introduce caches. For example, bill_payments maintains a cached UNPD_TOT (unpaid total) during create_bill and pay_bill. This prevents get_total_unpaid from iterating over all bills.
  • Hybrid lookup paths: For savings_goals::get_all_goals, we use a full-scan fast path when the owner owns all goals, and an index lookup path (OWN_GOAL) when they only own a subset.
  • Batch your writes: Write to persistent or temporary storage only once at the end of your logic rather than updating iteratively in a loop.

2. Computation and Hot Paths

If a method consumes too much CPU without hitting storage:

  • Bypass Event Emission Internally: If a function is called as an internal helper (e.g., calculate_split within distribute_usdc), use a non-event emitting variant. Generating and allocating vectors for events you don't need burns CPU and memory.
  • Avoid redundant clones: Do not clone Address or Env objects inside loops.
  • Reuse calculated lengths: In family_wallet::configure_multisig, we reuse the result of signers.len() rather than recounting or cloning the vector to validate thresholds.

3. Pagination and Limits

If scale-testing scenarios (like 50_schedules_worst_case) fail:

  • Enforce Batch Limits: Clamp operations using shared constants like MAX_BATCH_SIZE.
  • Use Cursor-based Pagination: Always implement cursor-based pagination for large datasets (e.g., get_schedules_paginated) to predictably bound the maximum CPU and memory per call.

Verifying Locally

To verify your tuning on a specific contract without waiting for CI, run:

RUST_TEST_THREADS=1 cargo test -p <crate_name> --test gas_bench -- --nocapture

(For example: cargo test -p remittance_split --test gas_bench -- --nocapture)