The CPU instrumentation module provides per-entrypoint CPU usage tracking for the StreamPay smart contract. It measures the number of CPU instructions consumed by each contract operation to help identify performance bottlenecks and optimize gas usage.
contracts/contracts/streampay-stream/src/instrument.rs
- Per-entrypoint CPU tracking: Records CPU instructions consumed by each contract entrypoint
- Call counting: Tracks how many times each entrypoint has been called
- Statistics: Maintains total CPU, average CPU per call, and maximum CPU per call
- Overflow-safe: Uses saturating arithmetic to prevent overflow errors
- Persistent storage: Metrics are stored in contract storage for historical analysis
pub struct CpuMetric {
pub entrypoint: Symbol, // Name of the entrypoint
pub total_cpu: u64, // Total CPU instructions consumed
pub call_count: u64, // Number of calls
pub max_cpu: u64, // Maximum CPU in a single call
pub last_updated: u64, // Last updated timestamp
}Wrap entrypoint logic with measure_cpu to automatically record CPU usage:
use instrument::measure_cpu;
use soroban_sdk::symbol_short;
pub fn create_stream(env: Env, ...) -> Result<u64, Error> {
let result = measure_cpu(&env, symbol_short!("create_stream"), || {
// Entrypoint logic here
create_stream_impl(env, sender, recipient, token, amount, start, end)
});
result
}Retrieve CPU metrics for a specific entrypoint:
use instrument::get_cpu_metric;
use soroban_sdk::symbol_short;
let metric = get_cpu_metric(&env, symbol_short!("create_stream"));
if let Some(m) = metric {
println!("Total CPU: {}", m.total_cpu);
println!("Call count: {}", m.call_count);
println!("Average CPU: {}", m.total_cpu / m.call_count);
println!("Max CPU: {}", m.max_cpu);
}Get the average CPU per call for an entrypoint:
use instrument::average_cpu_per_call;
use soroban_sdk::symbol_short;
let avg = average_cpu_per_call(&env, symbol_short!("withdraw"));Reset metrics for a specific entrypoint (useful for testing):
use instrument::reset_cpu_metric;
use soroban_sdk::symbol_short;
reset_cpu_metric(&env, symbol_short!("create_stream"));CPU metrics are stored in persistent storage using the following keys:
InstrumentKey::CpuMetric(entrypoint)- Per-entrypoint metricsInstrumentKey::CpuMetricCount- Number of tracked entrypoints
The following entrypoints should be instrumented for CPU tracking:
create_stream- Stream creationcreate_stream_for_org- Org-specific stream creationstart_stream- Stream activationwithdraw- Token withdrawalpause- Stream pauseresume- Stream resumecancel_stream- Stream cancellationsettle- Stream settlement
initialize- Contract initializationinit_with_token_allowlist- Initialization with allowlistset_paused- Pause flag toggleset_admin- Admin transferset_token_allowed- Token allowlist managementset_org_token_allowed- Org token allowlist managementset_max_streams_per_sender- Sender limit configuration
get_stream- Stream retrievalwithdrawable- Withdrawable amount calculationstream_balance- Stream balance calculationis_org_token_allowed- Token allowlist checkmax_streams_per_sender- Sender limit querysender_stream_count- Sender stream countremaining_sender_capacity- Remaining capacity query
To integrate CPU instrumentation into an entrypoint:
use instrument::measure_cpu;
use soroban_sdk::symbol_short;
#[contractimpl]
impl Contract {
pub fn create_stream(
env: Env,
sender: Address,
recipient: Address,
token: Address,
total_amount: i128,
start_time: u64,
end_time: u64,
) -> Result<u64, Error> {
measure_cpu(&env, symbol_short!("create_stream"), || {
// Existing entrypoint logic
require_not_paused(&env)?;
sender.require_auth();
limits::check_sender_limit(&env, &sender)?;
// ... rest of the implementation
})
}
}The module includes comprehensive unit tests:
cd contracts/contracts/streampay-stream
cargo test instrumentTest coverage includes:
- Basic CPU measurement
- Multiple call tracking
- Metric retrieval
- Average calculation
- Metric reset
- Maximum CPU tracking
- Overflow safety
- Overhead: CPU measurement adds minimal overhead (2 CPU instructions for
cpu_instr_consumedcalls) - Storage: Each tracked entrypoint adds one storage entry (~100 bytes)
- Gas: Storage writes for metrics consume additional gas
- Recommendation: Only instrument entrypoints that are called frequently or are performance-critical
- No sensitive data: CPU metrics do not contain sensitive information
- Read-only: Metrics are purely informational and do not affect contract logic
- No auth required: Reading metrics does not require authentication
- Overflow-safe: All arithmetic uses saturating operations to prevent overflow attacks
- High CPU entrypoints: Identify entrypoints with high average CPU per call
- Call frequency: Track which entrypoints are called most frequently
- CPU spikes: Monitor for unusual CPU consumption patterns
- Trends: Track CPU usage over time to identify degradation
Consider setting alerts for:
- Average CPU > 100,000 instructions per call
- Maximum CPU > 1,000,000 instructions per call
- Call count > 10,000 per day
If metrics are not being recorded:
- Verify
measure_cpuis called with correct entrypoint name - Check that the closure executes successfully
- Ensure storage is not full
If CPU values seem incorrect:
- Verify the entrypoint name is consistent across calls
- Check for nested
measure_cpucalls (avoid double-counting) - Ensure the Soroban environment is properly configured
If storage-related errors occur:
- Check available storage capacity
- Consider resetting old metrics
- Reduce the number of instrumented entrypoints
Potential improvements for CPU instrumentation:
- Percentile tracking: Add p50, p95, p99 CPU metrics
- Time windows: Track CPU usage over configurable time windows
- Aggregation: Aggregate metrics by time period (hourly, daily)
- Export: Add entrypoint to export metrics for external analysis
- Thresholds: Add configurable CPU usage thresholds
- Automatic cleanup: Automatically expire old metrics