type EVMPerformanceLogEntry = { calls: number time: number gasUsed: number staticGas?: number dynamicGasUsed?: number } export type EVMPerformanceLogOutput = { calls: number // Amount this opcode/precompile was called totalTime: number // Amount of seconds taken for this opcode/precompile (rounded to 3 digits) avgTimePerCall: number // Avg time per call of this opcode/precompile (rounded to 3 digits) gasUsed: number // Total amount of gas used by this opcode/precompile millionGasPerSecond: number // How much million gas is executed per second (rounded to 3 digits) blocksPerSlot: number // How many blocks is executed per beacon chain slot (rounded to 3 digits) tag: string // opcode/precompile tag staticGasUsed?: number // total static gas units spent dynamicGasUsed?: number // total dynamic gas units spent staticGas?: number // static gas of the opcode } type EVMPerformanceLogs = { [tag: string]: EVMPerformanceLogEntry } const blockGasLimit = 30_000_000 // Block gas limit const slotTime = 12 // Time in seconds per slot // Normalize constant to check if execution time is above one block per slot (>=1) or not (<1) const bpsNormalizer = blockGasLimit / slotTime export class Timer { private startTime: number private runTime = 0 tag: string constructor(tag: string) { this.tag = tag this.startTime = performance.now() } pause() { this.runTime = this.runTime + performance.now() - this.startTime } unpause() { this.startTime = performance.now() } time() { return (performance.now() - this.startTime + this.runTime) / 1000 } } export class EVMPerformanceLogger { private opcodes!: EVMPerformanceLogs private precompiles!: EVMPerformanceLogs private currentTimer?: Timer constructor() { this.clear() } clear() { this.opcodes = {} this.precompiles = {} } getLogs() { // Return nicely formatted logs function getLogsFor(obj: EVMPerformanceLogs) { const output: EVMPerformanceLogOutput[] = [] for (const key in obj) { const field = obj[key] const gasPerSecond = field.gasUsed / field.time const entry: EVMPerformanceLogOutput = { calls: field.calls, totalTime: Math.round(field.time * 1e6) / 1e3, avgTimePerCall: Math.round((field.time / field.calls) * 1e6) / 1e3, gasUsed: field.gasUsed, millionGasPerSecond: Math.round(gasPerSecond / 1e3) / 1e3, blocksPerSlot: Math.round((gasPerSecond / bpsNormalizer) * 1e3) / 1e3, tag: key, } if (field.dynamicGasUsed !== undefined) { // This is an opcode entry entry.staticGas = field.staticGas entry.staticGasUsed = field.staticGas! * field.calls entry.dynamicGasUsed = field.dynamicGasUsed } output.push(entry) } output.sort((a, b) => { return b.millionGasPerSecond - a.millionGasPerSecond }) return output } return { opcodes: getLogsFor(this.opcodes), precompiles: getLogsFor(this.precompiles), } } // Start a new timer // Only one timer can be timing at the same time startTimer(tag: string) { if (this.currentTimer !== undefined) { throw new Error('Cannot have two timers running at the same time') } this.currentTimer = new Timer(tag) return this.currentTimer } // Pauses current timer and returns that timer pauseTimer() { const timer = this.currentTimer if (timer === undefined) { throw new Error('No timer to pause') } timer.pause() this.currentTimer = undefined return timer } // Unpauses current timer and returns that timer unpauseTimer(timer: Timer) { if (this.currentTimer !== undefined) { throw new Error('Cannot unpause timer: another timer is already running') } timer.unpause() this.currentTimer = timer } // Stops a timer from running stopTimer( timer: Timer, gasUsed: number, targetTimer: 'precompiles' | 'opcodes' = 'opcodes', staticGas?: number, dynamicGas?: number ) { if (this.currentTimer !== undefined && this.currentTimer !== timer) { throw new Error('Cannot unpause timer: another timer is already running') } const time = timer.time() const tag = timer.tag this.currentTimer = undefined // Update the fields const target = this[targetTimer] if (target[tag] === undefined) { target[tag] = { calls: 0, time: 0, gasUsed: 0, } } const obj = target[tag] obj.calls++ obj.time += time obj.gasUsed += gasUsed if (targetTimer === 'opcodes') { obj.staticGas = staticGas obj.dynamicGasUsed = (obj.dynamicGasUsed ?? 0) + dynamicGas! } } }