Near Learn

Measure gas before you optimise

Measure NEAR gas before optimising: read env::used_gas() and env::prepaid_gas(), check gas burnt per receipt, and print gas in near-workspaces sandbox tests.

Intermediate6 min read3-question check

Gas surprises people. The expensive part of a method is often not the logic you are staring at but loading and saving contract state, (de)serializing JSON arguments, or a cross-contract call you forgot was there. Measure first, change one thing, measure again with the same inputs.

Inside the contract#

Log gas checkpoints with env::used_gas() and env::prepaid_gas()
Rust
use near_sdk::store::LookupMap;
use near_sdk::{env, log, near};

#[near(contract_state)]
pub struct Counter {
    scores: LookupMap<String, u64>,
}

impl Default for Counter {
    fn default() -> Self {
        Self { scores: LookupMap::new(b"s") }
    }
}

#[near]
impl Counter {
    pub fn bump(&mut self, key: String) {
        let start = env::used_gas();

        let current = self.scores.get(&key).copied().unwrap_or(0);
        self.scores.insert(key, current + 1);

        log!(
            "bump: {} Ggas so far, {} Tgas prepaid",
            env::used_gas().saturating_sub(start).as_ggas(),
            env::prepaid_gas().as_tgas()
        );
    }
}

In sandbox tests#

Print total and per-receipt gas with near-workspaces
Rust
#[tokio::test]
async fn bump_gas() -> anyhow::Result<()> {
    let sandbox = near_workspaces::sandbox().await?;
    let wasm = near_workspaces::compile_project("./").await?;
    let contract = sandbox.dev_deploy(&wasm).await?;

    let outcome = contract
        .call("bump")
        .args_json(serde_json::json!({ "key": "alice" }))
        .transact()
        .await?;
    assert!(outcome.is_success());

    println!("total: {} Ggas", outcome.total_gas_burnt.as_ggas());
    for receipt in outcome.receipt_outcomes() {
        println!("{}: {} Ggas", receipt.executor_id, receipt.gas_burnt.as_ggas());
    }
    println!("logs: {:?}", outcome.logs());
    Ok(())
}
  • Run with cargo test -- --nocapture so the println! output is shown.
  • Test the worst case, not the happy path: a large collection, the longest allowed string, the account with the most entries. Gas that grows with input size is what eventually breaks a method.
  • For transactions already on testnet or mainnet, the transaction outcome lists gas burnt and tokens burnt for the transaction and for every receipt it spawned. near-cli can print it, and explorers such as NearBlocks show it per receipt.
  • Record the numbers before and after each change. An optimisation you cannot measure is a guess.

Check yourself

3 questions · progress saved in this browser

  1. 1.You log env::used_gas() as the last line of a change method. Why can the receipt still burn noticeably more?
  2. 2.Which gives the most realistic gas numbers for a method?
  3. 3.What does env::prepaid_gas() return?