Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

EVM: bump opcode coverage + add performance tester #3198

Draft
wants to merge 15 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/evm/package.json
Expand Up @@ -46,6 +46,7 @@
"lint:fix": "../../config/cli/lint-fix.sh",
"prepublishOnly": "../../config/cli/prepublish.sh",
"profiling": "0x ./benchmarks/run.js profiling",
"profiler:report": "tsx ./test/opcodes/profiler.ts",
"test": "npm run test:node && npm run test:browser",
"test:browser": "npx vitest run -c=vitest.config.browser.ts --browser.provider=playwright --browser.name=webkit --browser.headless",
"test:node": "npx vitest run",
Expand Down
6 changes: 3 additions & 3 deletions packages/evm/src/opcodes/codes.ts
Expand Up @@ -53,7 +53,7 @@ type OpcodeEntry = { [key: number]: { name: string; isAsync: boolean; dynamicGas
type OpcodeEntryFee = OpcodeEntry & { [key: number]: { fee: number } }

// Base opcode list. The opcode list is extended in future hardforks
const opcodes: OpcodeEntry = {
export const opcodes: OpcodeEntry = {
// 0x0 range - arithmetic ops
// name, async
0x00: { name: 'STOP', isAsync: false, dynamicGas: false },
Expand Down Expand Up @@ -212,7 +212,7 @@ const opcodes: OpcodeEntry = {
// If the base gas cost of any of the operations change, then these should also be added to this list.
// If there are context variables changed (such as "warm slot reads") which are not the base gas fees,
// Then this does not have to be added.
const hardforkOpcodes: { hardfork: Hardfork; opcodes: OpcodeEntry }[] = [
export const hardforkOpcodes: { hardfork: Hardfork; opcodes: OpcodeEntry }[] = [
{
hardfork: Hardfork.Homestead,
opcodes: {
Expand Down Expand Up @@ -266,7 +266,7 @@ const hardforkOpcodes: { hardfork: Hardfork; opcodes: OpcodeEntry }[] = [
},
]

const eipOpcodes: { eip: number; opcodes: OpcodeEntry }[] = [
export const eipOpcodes: { eip: number; opcodes: OpcodeEntry }[] = [
{
eip: 1153,
opcodes: {
Expand Down
2 changes: 2 additions & 0 deletions packages/evm/src/opcodes/functions.ts
Expand Up @@ -385,6 +385,8 @@ export const handlers: Map<number, OpHandler> = new Map([
}

const c = b >> a
// TODO check if this is faster:
// runState.stack.push(BigInt.asUintN(256, c))
if (isSigned) {
const shiftedOutWidth = BIGINT_255 - a
const mask = (MAX_INTEGER_BIGINT >> shiftedOutWidth) << shiftedOutWidth
Expand Down
103 changes: 103 additions & 0 deletions packages/evm/test/opcodes/profiler.ts
@@ -0,0 +1,103 @@
/* eslint no-console: 0 */

import { Chain, Common, Hardfork } from '@ethereumjs/common'
import { hexToBytes } from '@ethereumjs/util'

import { EVM } from '../../src/index.js'

import { opcodeTests } from './tests.js'
import {
createBytecode,
createOpcodeTest,
getOpcodeByte,
getOpcodeTestName,
makeLoopCode,
} from './utils.js'

// Config
const hardfork = Hardfork.Shanghai
const GAS_LIMIT = BigInt(30_000_000) // 30M gas (mainnet max gas per block)
// End config

const GAS_TARGET = 30_000_000
const SECONDS = 12

const MGasPerSecondTarget = GAS_TARGET / SECONDS / 1e6

const common = new Common({ chain: Chain.Mainnet, hardfork })

let testCount = 0
for (const testSet in opcodeTests) {
const testCases = opcodeTests[testSet]
for (const opcodeName in testCases) {
const tests = testCases[opcodeName]
for (const _ of tests) {
testCount++
}
}
}

const POP = getOpcodeByte('POP')

const profilerResults: {
testName: string
opcode: string
mGasPerSecond: number
}[] = []

let progress = 0

async function profile() {
for (const testSet in opcodeTests) {
const testCases = opcodeTests[testSet]
for (const opcodeName in testCases) {
const tests = testCases[opcodeName]
for (const test of tests) {
const testName = getOpcodeTestName(opcodeName, test.stack, test.name)
const code = makeLoopCode(
createBytecode([createOpcodeTest(test.stack, opcodeName, 'none'), POP])
)
const evm = new EVM({ common, profiler: { enabled: true } })
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
const evm = new EVM({ common, profiler: { enabled: true } })
common.events.removeAllListeners()

Since we're reusing the same common each time, we should clear listeners because the evm adds a new listener here and I see memory leak errors when I run the profiler script.

await evm.runCode({
code: hexToBytes(code),
gasLimit: GAS_LIMIT,
})
const logs = evm.getPerformanceLogs()
for (const item of logs.opcodes) {
if (item.tag === opcodeName) {
profilerResults.push({
testName,
opcode: opcodeName,
mGasPerSecond: item.millionGasPerSecond,
})
break
}
}
evm.clearPerformanceLogs()
progress++
console.log(`Progress: ${progress}/${testCount}`)
}
}
}
const sorted = profilerResults.sort((a, b) => {
return b.mGasPerSecond - a.mGasPerSecond
})
console.log()
console.log('-------------------------------------------------------------------------')
console.log(`Profiler report for hardfork ${hardfork}, gas limit: ${Number(GAS_LIMIT)}`)
console.log(`Any MGas/s higher than ${MGasPerSecondTarget} is OK`)
console.log('-------------------------------------------------------------------------')
console.log()
console.log('MGas/s'.padEnd(10, ' '), 'Opcode'.padEnd(12, ' '), 'Test name')
console.log('-------------------------------------------------------------------------')

for (const entry of sorted) {
console.log(
entry.mGasPerSecond.toString().padEnd(10, ' '),
entry.opcode.padEnd(12, ' '),
entry.testName
)
}
}

void profile()
24 changes: 24 additions & 0 deletions packages/evm/test/opcodes/testOpcodes.spec.ts
@@ -0,0 +1,24 @@
import { describe, it } from 'vitest'

import { opcodeTests } from './tests.js'
import { runOpcodeTest } from './utils.js'

for (const testSet in opcodeTests) {
describe(`should test ${testSet} tests`, () => {
const testCases = opcodeTests[testSet]
for (const opcodeName in testCases) {
it(`should test opcode ${opcodeName}`, async () => {
const testDataArray = testCases[opcodeName]
for (const testData of testDataArray) {
await runOpcodeTest({
testName: testData.name,
opcodeName,
expected: testData.expected,
expectedReturnType: 'topStack',
input: testData.stack,
})
}
})
}
})
}