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
}
})