Skip to content

Performance & Profiler

The performance profiler tracks execution time, gas usage, and other metrics for every operation in the EVM, giving you deep insights into how your contracts perform.

Quick Start

Basic Setup

import { createTevmNode } from 'tevm'
 
const node = createTevmNode({ 
  profiler: { 
    enabled: true, 
    includeOpcodes: true, // Track individual opcodes 
    includePrecompiles: true, // Track precompiled contracts 
  } 
})
 
// Execute some transactions or function calls
const vm = await node.getVm() 
 
// Run your contract operations
await vm.runTx({ /* transaction details */ }) 
 
// Get performance logs
const logs = vm.evm.getPerformanceLogs() 
 
// Clear logs when done
vm.evm.clearPerformanceLogs() 

Complete Example

import { createTevmNode } from 'tevm'
 
// Create a node with profiling enabled
const node = createTevmNode({ 
  profiler: { 
    enabled: true, 
    includeOpcodes: true, 
    includePrecompiles: true, 
    includeMemory: true, // Include memory operations 
    includeStorage: true, // Include storage operations 
    callStackDepth: 10, // Maximum call stack depth to track 
  } 
})
 
// Set up the contract
const vm = await node.getVm() 
const deployTx = createTx({ /* deployment transaction */ }) 
await vm.runTx({ tx: deployTx }) 
const contractAddress = deployTx.createdAddress 
 
// Profile a specific function call
// Clear previous logs first
vm.evm.clearPerformanceLogs() 
 
// Call the contract function
const callTx = createTx({ 
  to: contractAddress, 
  data: '0xa9059cbb000000000000000000000000123...', // function selector + params 
}) 
await vm.runTx({ tx: callTx }) 
 
// Analyze the performance
const logs = vm.evm.getPerformanceLogs() 
const analysis = analyzePerformance(logs) 
 
console.log(`Total execution time: ${analysis.totalTime}ms`) 
console.log(`Total gas used: ${analysis.totalGas}`) 
console.log(`Hotspots: ${JSON.stringify(analysis.hotspots)}`) 

Profiler Configuration

  • ⚙️ Opcode Tracking - Track execution time and gas usage for individual EVM opcodes
  • 📞 Call Stack Analysis - Profile the entire call tree to identify expensive contract interactions
  • ⛽ Gas Analysis - Identify operations that consume the most gas
  • 💾 Memory Usage - Track memory allocation and access patterns
  • 💽 Storage I/O - Measure the impact of storage reads and writes
  • 🧩 Precompile Usage - Profile built-in and custom precompiled contracts

Log Types

Performance Log Base Type

All log entries share these properties:

interface PerformanceLog {
  type: 'opcode' | 'precompile' | 'call' | 'create'
  startTime: number     // Performance.now() start timestamp
  endTime: number       // Performance.now() end timestamp
  executionTime: number // Duration in milliseconds
  gasUsed?: bigint      // Gas consumed by this operation
}

Opcode-Specific Logs

Detailed information about each opcode execution:

interface OpcodeLog extends PerformanceLog {
  type: 'opcode'
  opcode: string  // Opcode name (e.g., "ADD", "SSTORE")
  pc: number      // Program counter position
  stack?: bigint[] // Stack contents (if enabled)
}

Call and Contract Logs

Information about contract interactions:

interface CallLog extends PerformanceLog {
  type: 'call'
  from: string    // Caller address
  to: string      // Target address
  value: bigint   // ETH value transferred
  input: Uint8Array // Call data
  depth: number   // Call stack depth
}

Precompile Logs

Information about precompiled contract executions:

interface PrecompileLog extends PerformanceLog {
  type: 'precompile'
  address: string  // Precompile address
  name: string     // Precompile name if known
  input: Uint8Array // Input data
}

Performance Analysis

Opcode Analysis

Identify which opcodes consume the most time and gas:

// Group logs by opcode
const opcodeStats = logs 
  .filter(log => log.type === 'opcode') 
  .reduce((acc, log) => { 
    const key = log.opcode 
    acc[key] = acc[key] || { count: 0, totalTime: 0, totalGas: 0n } 
    acc[key].count++
    acc[key].totalTime += log.executionTime 
    acc[key].totalGas += log.gasUsed ?? 0n
    return acc 
  }, {}) 
 
// Find most expensive operations by time
const expensiveOpsByTime = Object.entries(opcodeStats) 
  .sort(([, a], [, b]) => b.totalTime - a.totalTime) 
  .slice(0, 5) 
  .map(([opcode, stats]) => ({ 
    opcode, 
    count: stats.count, 
    totalTimeMs: stats.totalTime.toFixed(2), 
    avgTimeMs: (stats.totalTime / stats.count).toFixed(2) 
  })) 
 
console.log('Most time-consuming opcodes:') 
console.table(expensiveOpsByTime) 

Call Tree Analysis

Analyze contract interactions and call patterns:

// Map call depth and patterns
const callTree = logs 
  .filter(log => log.type === 'call') 
  .map(log => ({ 
    from: log.from, 
    to: log.to, 
    executionTime: log.executionTime, 
    gasUsed: log.gasUsed, 
    depth: log.depth 
  }))
 
// Group by contract address
const contractTimes = callTree.reduce((acc, call) => { 
  acc[call.to] = (acc[call.to] || 0) + call.executionTime 
  return acc 
}, {}) 
 
// Find most time-consuming contracts
const slowestContracts = Object.entries(contractTimes) 
  .sort(([, a], [, b]) => b - a) 
  .slice(0, 3) 
  .map(([address, time]) =>
    `${address}: ${time.toFixed(2)}ms`
  ) 

Gas Analysis

Track gas consumption patterns:

// Track gas usage over time
const gasTimeline = logs 
  .filter(log => log.gasUsed !== undefined) 
  .map(log => ({ 
    timestamp: log.startTime, 
    gasUsed: log.gasUsed, 
    type: log.type 
  })) 
 
// Calculate gas efficiency by operation type
const gasEfficiencyByType = gasTimeline.reduce((acc, log) => { 
  acc[log.type] = acc[log.type] || { totalGas: 0n, totalTime: 0, count: 0 } 
  acc[log.type].totalGas += log.gasUsed 
  acc[log.type].totalTime += log.executionTime 
  acc[log.type].count++
  return acc 
}, {}) 
 
// Calculate gas per millisecond for each operation type
Object.entries(gasEfficiencyByType).forEach(([type, stats]) => {
  const gasPerMs = Number(stats.totalGas) / stats.totalTime
  console.log(`${type}: ${gasPerMs.toFixed(2)} gas/ms (${stats.count} operations)`)
})

Use Cases

Smart Contract Optimization

// Deploy the contract first
const vm = await node.getVm()
const deployTx = createTx({ data: contractBytecode })
await vm.runTx({ tx: deployTx })
const contractAddress = deployTx.createdAddress
 
// Clear logs before profiling the specific function
vm.evm.clearPerformanceLogs()
 
// Call the function you want to optimize
const functionCallTx = createTx({
  to: contractAddress,
  data: encodeFunctionData({ 
    abi,
    functionName: 'expensiveFunction',
    args: [param1, param2]
  })
})
 
await vm.runTx({ tx: functionCallTx })
const logs = vm.evm.getPerformanceLogs()
 
// Analyze to find optimization opportunities
const hotspots = identifyHotspots(logs)
console.log('Optimization targets:', hotspots)

Compare Different Implementations

async function compareImplementations(implementations) {
  const results = []
  const vm = await node.getVm()
 
  for (const impl of implementations) {
    // Clear previous logs
    vm.evm.clearPerformanceLogs()
    
    // Deploy this implementation
    const deployTx = createTx({ data: impl.bytecode })
    await vm.runTx({ tx: deployTx })
    
    // Call with standard test case
    const callTx = createTx({
      to: deployTx.createdAddress,
      data: impl.encodedFunctionCall
    })
    
    await vm.runTx({ tx: callTx })
    const logs = vm.evm.getPerformanceLogs()
    
    // Collect metrics
    results.push(analyzePerformance(logs, impl.name))
  }
 
  return compareResults(results)
}
 
// Example output:
// Implementation A: 1.2ms, 45,000 gas
// Implementation B: 0.8ms, 32,000 gas (29% improvement)

Gas Optimization

// Find gas-intensive parts of your contract
const contractGasUsage = logs
  .filter(log => log.type === 'opcode' && log.gasUsed)
  .reduce((acc, log) => {
    // Group by program counter to identify code locations
    const pcKey = log.pc.toString().padStart(4, '0')
    acc[pcKey] = acc[pcKey] || { 
      opcode: log.opcode,
      count: 0, 
      totalGas: 0n 
    }
    
    acc[pcKey].count++
    acc[pcKey].totalGas += log.gasUsed
    
    return acc
  }, {})
 
// Identify gas hotspots
const gasHotspots = Object.entries(contractGasUsage)
  .sort(([, a], [, b]) => Number(b.totalGas - a.totalGas))
  .slice(0, 10)

Best Practices

Targeted Profiling

// Clear logs before the specific operation you want to profile
vm.evm.clearPerformanceLogs()
 
// Run only the operation you want to profile
await vm.runTx({ tx: specificOperationTx })
 
// Analyze just that operation
const logs = vm.evm.getPerformanceLogs()
const analysis = analyzePerformance(logs)
 
// Clear logs when done to prevent memory build-up
vm.evm.clearPerformanceLogs()

Memory Management

// For long-running sessions, clear logs periodically
async function profileWithMemoryManagement(operations) {
  const results = []
  
  for (const op of operations) {
    // Clear before each operation
    vm.evm.clearPerformanceLogs()
    
    // Run the operation
    await vm.runTx({ tx: op.tx })
    
    // Process logs immediately
    const logs = vm.evm.getPerformanceLogs()
    const summary = summarizeLogs(logs) // Extract just what you need
    
    // Save only the summary
    results.push({
      operation: op.name,
      summary
    })
    
    // Clear immediately to free memory
    vm.evm.clearPerformanceLogs()
  }
  
  return results
}

Comparative Analysis

// Compare before and after optimization
async function measureOptimizationImpact(originalCode, optimizedCode) {
  // Profile original implementation
  vm.evm.clearPerformanceLogs()
  await runImplementation(originalCode)
  const beforeLogs = vm.evm.getPerformanceLogs()
  const beforeMetrics = analyzePerformance(beforeLogs)
  
  // Profile optimized implementation
  vm.evm.clearPerformanceLogs()
  await runImplementation(optimizedCode)
  const afterLogs = vm.evm.getPerformanceLogs()
  const afterMetrics = analyzePerformance(afterLogs)
  
  // Calculate improvements
  return {
    timeImprovement: (1 - afterMetrics.totalTime / beforeMetrics.totalTime) * 100,
    gasImprovement: (1 - Number(afterMetrics.totalGas) / Number(beforeMetrics.totalGas)) * 100,
    details: compareMetrics(beforeMetrics, afterMetrics)
  }
}

Production Use

// For production environments
const node = createTevmNode({
  profiler: {
    // Only enable in specific environments
    enabled: process.env.ENABLE_PROFILING === 'true',
    
    // Use selective profiling to reduce overhead
    includeOpcodes: false,      // Disable full opcode tracking
    samplingRate: 0.01,         // Only profile 1% of operations
    includeMemory: false,       // Skip memory tracking
    includeStorage: false,      // Skip storage tracking
    
    // Only track high-level calls for a system overview
    includeHighLevelCalls: true
  }
})

Related Resources

VM & Submodules
Low-level access to the EVM core
Gas Estimation
Methods for estimating transaction gas costs
Transaction Pool
Understanding transaction lifecycle and processing
Solidity Optimizer
Learn about Solidity's built-in optimizer techniques