Profiling and Optimizing Contracts
This guide shows you how to profile your Aztec transactions to identify bottlenecks and optimize gas usage.
Prerequisites
aztec-nargoinstalled (see installation)aztec-walletinstalled (part of Sandbox)- Aztec contract deployed and ready to test
 - Basic understanding of proving and gate counts
 
Profile with aztec-wallet
Step 1: Import test accounts
aztec-wallet import-test-accounts
Step 2: Deploy your contract
aztec-wallet deploy MyContractArtifact \
  --from accounts:test0 \
  --args <constructor_args> \
  -a mycontract
Step 3: Set up initial state
aztec-wallet send setup_state \
  -ca mycontract \
  --args <setup_args> \
  -f test0
Step 4: Profile a transaction
Instead of send, use profile with the same parameters:
aztec-wallet profile private_function \
  -ca mycontract \
  --args <function_args> \
  -f accounts:test0
Step 5: Analyze the output
Gate count per circuit:
   SchnorrAccount:entrypoint                          Gates: 21,724     Acc: 21,724
   private_kernel_init                                Gates: 45,351     Acc: 67,075
   MyContract:private_function                        Gates: 31,559     Acc: 98,634
   private_kernel_inner                               Gates: 78,452     Acc: 177,086
   private_kernel_reset                               Gates: 91,444     Acc: 268,530
   private_kernel_tail                                Gates: 31,201     Acc: 299,731
Total gates: 299,731
The output shows:
- Gate count per circuit component
 - Accumulated gate count
 - Total gates for the entire transaction
 
Profile with aztec.js
Profile Modes
gates: Shows gate counts per circuitexecution-steps: Detailed execution tracefull: Complete profiling information
Step 1: Profile a transaction
const result = await contract.methods
  .my_function(args)
  .profile({ 
    from: address,
    profileMode: 'gates',
    skipProofGeneration: false 
  });
console.log('Gate count:', result.gateCount);
Step 2: Profile deployment
const deploy = await Contract.deploy(args).profile({ from: address, profileMode: 'full' });
Experimental
Flamegraph generation is experimental and may not be available in all versions.
Generate flamegraphs (if available)
Generate and view
# Compile first
aztec-nargo compile
# Generate flamegraph
aztec flamegraph target/contract.json function_name
# Serve locally
SERVE=1 aztec flamegraph target/contract.json function_name
Reading Flamegraphs
- Width = Time in operation
 - Height = Call depth
 - Wide sections = Optimization targets
 
Common optimizations
Key Metrics
- Gate count: Circuit complexity
 - Kernel overhead: Per-function cost
 - Storage access: Read/write operations
 
Optimization Pattern
Batch operations to reduce kernel circuit overhead.
// ❌ Multiple kernel invocations
for i in 0..3 {
    transfer_single(amounts[i], recipients[i]);
}
// ✅ Single kernel invocation
for i in 0..3 {
    let note = Note::new(amounts[i], recipients[i]);
    storage.notes.at(recipients[i]).insert(note);
}
Storage Optimization
Group storage reads to reduce overhead.
// Read once, use multiple times
let values = [storage.v1.get(), storage.v2.get(), storage.v3.get()];
for v in values {
    assert(v > 0);
}
Minimize note operations
Note Aggregation
Combine multiple small notes into fewer larger ones to reduce proving overhead.
// ❌ Many small notes = high overhead
for value in values {
    storage.notes.insert(Note::new(value, owner));
}
// ✅ Single aggregated note = lower overhead
let total = values.reduce(|a, b| a + b);
storage.notes.insert(Note::new(total, owner));
Profile different scenarios
Profile with different inputs
# Small values
aztec-wallet profile function -ca mycontract --args 10 -f test0
# Large values
aztec-wallet profile function -ca mycontract --args 1000000 -f test0
Profile execution modes
// Profile gates only
await contract.methods.function().profile({ profileMode: 'gates' });
// Profile execution steps
await contract.methods.function().profile({ profileMode: 'execution-steps' });
// Full profile
await contract.methods.function().profile({ profileMode: 'full' });
Skip proof generation for faster iteration
await contract.methods.function().profile({
  profileMode: 'gates',
  skipProofGeneration: true  // Faster but less accurate
});
Interpret profiling results
Gate count guidelines
- < 50,000 gates: Excellent performance
 - 50,000 - 200,000 gates: Acceptable for most use cases
 - 200,000 - 500,000 gates: May cause delays, consider optimizing
 - > 500,000 gates: Requires optimization for production
 
Common optimization targets
- private_kernel_inner - Reduce nested function calls
 - private_kernel_reset - Minimize note nullifications
 - Contract functions - Optimize computation logic
 - private_kernel_tail - Reduce public function calls
 
Best practices
Development workflow
- Profile early - Establish baseline metrics
 - Profile often - Check impact of changes
 - Profile realistically - Use production-like data
 - Document findings - Track optimization progress
 
Optimization priorities
- User-facing functions - Optimize most-used features first
 - Critical paths - Focus on transaction bottlenecks
 - Batch operations - Combine related operations
 - Cache calculations - Store reusable results
 
Next steps
- Learn about gas optimization techniques
 - Review benchmarking best practices