Performance & Profiler
Tevm Node ships a built-in profiler that tracks execution time, gas, and other metrics per EVM operation.
Quick Start
Basic Setup
import { createTevmNode } from 'tevm'
const node = createTevmNode({
profiler: {
enabled: true,
includeOpcodes: true,
includePrecompiles: true,
}
})
const vm = await node.getVm()
await vm.runTx({ /* transaction details */ })
const logs = vm.evm.getPerformanceLogs()
vm.evm.clearPerformanceLogs()Complete Example
import { createTevmNode } from 'tevm'
const node = createTevmNode({
profiler: {
enabled: true,
includeOpcodes: true,
includePrecompiles: true,
includeMemory: true,
includeStorage: true,
callStackDepth: 10,
}
})
const vm = await node.getVm()
const deployTx = createTx({ /* deployment transaction */ })
await vm.runTx({ tx: deployTx })
const contractAddress = deployTx.createdAddress
vm.evm.clearPerformanceLogs()
const callTx = createTx({
to: contractAddress,
data: '0xa9059cbb000000000000000000000000123...',
})
await vm.runTx({ tx: callTx })
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 — execution time and gas per opcode
- Call Stack Analysis — full call tree
- Gas Analysis — most gas-hungry operations
- Memory Usage — allocation and access patterns
- Storage I/O — SLOAD/SSTORE impact
- Precompile Usage — built-in and custom precompiles
Log Types
All log entries share a base shape:
interface PerformanceLog {
type: 'opcode' | 'precompile' | 'call' | 'create'
startTime: number // Performance.now() start
endTime: number // Performance.now() end
executionTime: number // ms
gasUsed?: bigint
}interface OpcodeLog extends PerformanceLog {
type: 'opcode'
opcode: string // e.g. "ADD", "SSTORE"
pc: number
stack?: bigint[]
}interface CallLog extends PerformanceLog {
type: 'call'
from: string
to: string
value: bigint
input: Uint8Array
depth: number
}interface PrecompileLog extends PerformanceLog {
type: 'precompile'
address: string
name: string
input: Uint8Array
}Performance Analysis
Opcode Analysis
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
}, {})
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.table(expensiveOpsByTime)Call Tree Analysis
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
}))
const contractTimes = callTree.reduce((acc, call) => {
acc[call.to] = (acc[call.to] || 0) + call.executionTime
return acc
}, {})
const slowestContracts = Object.entries(contractTimes)
.sort(([, a], [, b]) => b - a)
.slice(0, 3)
.map(([address, time]) => `${address}: ${time.toFixed(2)}ms`)Gas Analysis
const gasTimeline = logs
.filter(log => log.gasUsed !== undefined)
.map(log => ({ timestamp: log.startTime, gasUsed: log.gasUsed, type: log.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
}, {})
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
const vm = await node.getVm()
const deployTx = createTx({ data: contractBytecode })
await vm.runTx({ tx: deployTx })
const contractAddress = deployTx.createdAddress
vm.evm.clearPerformanceLogs()
const functionCallTx = createTx({
to: contractAddress,
data: encodeFunctionData({ abi, functionName: 'expensiveFunction', args: [param1, param2] })
})
await vm.runTx({ tx: functionCallTx })
const logs = vm.evm.getPerformanceLogs()
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) {
vm.evm.clearPerformanceLogs()
const deployTx = createTx({ data: impl.bytecode })
await vm.runTx({ tx: deployTx })
const callTx = createTx({ to: deployTx.createdAddress, data: impl.encodedFunctionCall })
await vm.runTx({ tx: callTx })
const logs = vm.evm.getPerformanceLogs()
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
const contractGasUsage = logs
.filter(log => log.type === 'opcode' && log.gasUsed)
.reduce((acc, log) => {
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
}, {})
const gasHotspots = Object.entries(contractGasUsage)
.sort(([, a], [, b]) => Number(b.totalGas - a.totalGas))
.slice(0, 10)Best Practices
Targeted Profiling
Profile specific operations in isolation for accurate results.
vm.evm.clearPerformanceLogs()
await vm.runTx({ tx: specificOperationTx })
const logs = vm.evm.getPerformanceLogs()
const analysis = analyzePerformance(logs)
vm.evm.clearPerformanceLogs()Memory Management
async function profileWithMemoryManagement(operations) {
const results = []
for (const op of operations) {
vm.evm.clearPerformanceLogs()
await vm.runTx({ tx: op.tx })
const logs = vm.evm.getPerformanceLogs()
const summary = summarizeLogs(logs)
results.push({ operation: op.name, summary })
vm.evm.clearPerformanceLogs()
}
return results
}Comparative Analysis
Always measure before/after optimization.
async function measureOptimizationImpact(originalCode, optimizedCode) {
vm.evm.clearPerformanceLogs()
await runImplementation(originalCode)
const beforeMetrics = analyzePerformance(vm.evm.getPerformanceLogs())
vm.evm.clearPerformanceLogs()
await runImplementation(optimizedCode)
const afterMetrics = analyzePerformance(vm.evm.getPerformanceLogs())
return {
timeImprovement: (1 - afterMetrics.totalTime / beforeMetrics.totalTime) * 100,
gasImprovement: (1 - Number(afterMetrics.totalGas) / Number(beforeMetrics.totalGas)) * 100,
details: compareMetrics(beforeMetrics, afterMetrics)
}
}Production Use
const node = createTevmNode({
profiler: {
enabled: process.env.ENABLE_PROFILING === 'true',
includeOpcodes: false,
samplingRate: 0.01, // Profile 1% of operations
includeMemory: false,
includeStorage: false,
includeHighLevelCalls: true
}
})
