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

Client Memory Optimizations #2675

Merged
merged 8 commits into from
May 4, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions packages/client/bin/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,11 @@ const args: ClientOpts = yargs(hideBin(process.argv))
describe: 'EIP-1459 ENR tree urls to query for peer discovery targets',
array: true,
})
.option('execution', {
describe: 'Start continuous VM execution (pre-Merge setting)',
boolean: true,
default: Config.EXECUTION,
})
.option('numBlocksPerIteration', {
describe: 'Number of blocks to execute in batch mode and logged to console',
number: true,
Expand Down Expand Up @@ -738,6 +743,7 @@ async function run() {
discDns: args.discDns,
discV4: args.discV4,
dnsAddr: args.dnsAddr,
execution: args.execution,
numBlocksPerIteration: args.numBlocksPerIteration,
accountCache: args.accountCache,
storageCache: args.storageCache,
Expand Down
2 changes: 1 addition & 1 deletion packages/client/karma.conf.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ module.exports = function (config) {
ecmaVersion: 12,
},
// sourceMap: true,
exclude: ['async_hooks'],
exclude: ['async_hooks', 'node:v8'],
resolve: {
alias: {
// Hotfix for `multiformats` client browser build error in Node 16, #1346, 2021-07-12
Expand Down
12 changes: 10 additions & 2 deletions packages/client/lib/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,11 @@ export interface ConfigOptions {
*/
dnsNetworks?: string[]

/**
* Start continuous VM execution (pre-Merge setting)
*/
execution?: boolean

/**
* Number of blocks to execute in batch mode and logged to console
*/
Expand Down Expand Up @@ -310,10 +315,11 @@ export class Config {
public static readonly MINPEERS_DEFAULT = 1
public static readonly MAXPEERS_DEFAULT = 25
public static readonly DNSADDR_DEFAULT = '8.8.8.8'
public static readonly EXECUTION = true
public static readonly NUM_BLOCKS_PER_ITERATION = 100
public static readonly ACCOUNT_CACHE = 1000000
public static readonly ACCOUNT_CACHE = 400000
public static readonly STORAGE_CACHE = 200000
public static readonly TRIE_CACHE = 500000
public static readonly TRIE_CACHE = 200000
public static readonly DEBUGCODE_DEFAULT = false
public static readonly SAFE_REORG_DISTANCE = 100
public static readonly SKELETON_FILL_CANONICAL_BACKSTEP = 100
Expand Down Expand Up @@ -345,6 +351,7 @@ export class Config {
public readonly minPeers: number
public readonly maxPeers: number
public readonly dnsAddr: string
public readonly execution: boolean
public readonly numBlocksPerIteration: number
public readonly accountCache: number
public readonly storageCache: number
Expand Down Expand Up @@ -402,6 +409,7 @@ export class Config {
this.minPeers = options.minPeers ?? Config.MINPEERS_DEFAULT
this.maxPeers = options.maxPeers ?? Config.MAXPEERS_DEFAULT
this.dnsAddr = options.dnsAddr ?? Config.DNSADDR_DEFAULT
this.execution = options.execution ?? Config.EXECUTION
this.numBlocksPerIteration = options.numBlocksPerIteration ?? Config.NUM_BLOCKS_PER_ITERATION
this.accountCache = options.accountCache ?? Config.ACCOUNT_CACHE
this.storageCache = options.storageCache ?? Config.STORAGE_CACHE
Expand Down
69 changes: 60 additions & 9 deletions packages/client/lib/execution/vmexecution.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,19 +12,23 @@ import { Lock, bytesToHex, bytesToPrefixedHexString, equalsBytes } from '@ethere
import { VM } from '@ethereumjs/vm'

import { Event } from '../types'
import { short } from '../util'
import { getV8Engine, short } from '../util'
import { debugCodeReplayBlock } from '../util/debug'

import { Execution } from './execution'
import { LevelDB } from './level'
import { ReceiptsManager } from './receipt'

import type { V8Engine } from '../util'
import type { ExecutionOptions } from './execution'
import type { Block } from '@ethereumjs/block'
import type { RunBlockOpts, TxReceipt } from '@ethereumjs/vm'

export class VMExecution extends Execution {
private _lock = new Lock()
// A handle to v8Engine lib for mem stats, assigned on open if running in node
private v8Engine: V8Engine | null = null

public vm: VM
public hardfork: string = ''

Expand All @@ -38,8 +42,8 @@ export class VMExecution extends Execution {
/**
* Display state cache stats every num blocks
*/
private CACHE_STATS_NUM_BLOCKS = 500
private cacheStatsCount = 0
private STATS_NUM_BLOCKS = 500
private statsCount = 0

/**
* Create new VM execution module
Expand Down Expand Up @@ -117,6 +121,11 @@ export class VMExecution extends Execution {
if (this.started || this.vmPromise !== undefined) {
return
}

if (this.v8Engine === null) {
this.v8Engine = await getV8Engine()
}

await this.vm.init()
if (typeof this.vm.blockchain.getIteratorHead !== 'function') {
throw new Error('cannot get iterator head: blockchain has no getIteratorHead function')
Expand Down Expand Up @@ -342,7 +351,7 @@ export class VMExecution extends Execution {
throw Error('Execution stopped')
}
const beforeTS = Date.now()
this.cacheStats(this.vm)
this.stats(this.vm)
const result = await this.vm.runBlock({
block,
root: parentState,
Expand Down Expand Up @@ -471,6 +480,40 @@ export class VMExecution extends Execution {
return numExecuted ?? 0
}

/**
* Start execution
*/
async start(): Promise<boolean> {
const { blockchain } = this.vm
if (this.running || !this.started) {
return false
}

if (typeof blockchain.getIteratorHead !== 'function') {
throw new Error('cannot get iterator head: blockchain has no getIteratorHead function')
}
const vmHeadBlock = await blockchain.getIteratorHead()
if (typeof blockchain.getCanonicalHeadBlock !== 'function') {
throw new Error('cannot get iterator head: blockchain has no getCanonicalHeadBlock function')
}
const canonicalHead = await blockchain.getCanonicalHeadBlock()

const infoStr = `vmHead=${vmHeadBlock.header.number} canonicalHead=${
canonicalHead.header.number
} hardfork=${this.config.execCommon.hardfork()} execution=${this.config.execution}`
if (
!this.config.execCommon.gteHardfork(Hardfork.Paris) &&
this.config.execution &&
vmHeadBlock.header.number < canonicalHead.header.number
) {
this.config.logger.info(`Starting execution run ${infoStr}`)
void this.run(true, true)
} else {
this.config.logger.info(`Skipped execution run ${infoStr}`)
}
return true
}

/**
* Stop VM execution. Returns a promise that resolves once its stopped.
*/
Expand Down Expand Up @@ -523,7 +566,7 @@ export class VMExecution extends Execution {
// we are skipping header validation because the block has been picked from the
// blockchain and header should have already been validated while putBlock
const beforeTS = Date.now()
this.cacheStats(vm)
this.stats(vm)
const res = await vm.runBlock({
block,
root,
Expand Down Expand Up @@ -566,9 +609,9 @@ export class VMExecution extends Execution {
}
}

cacheStats(vm: VM) {
this.cacheStatsCount += 1
if (this.cacheStatsCount === this.CACHE_STATS_NUM_BLOCKS) {
stats(vm: VM) {
this.statsCount += 1
if (this.statsCount === this.STATS_NUM_BLOCKS) {
let stats = (vm.stateManager as any)._accountCache.stats()
this.config.logger.info(
`Account cache stats size=${stats.size} reads=${stats.reads} hits=${stats.hits} writes=${stats.writes}`
Expand All @@ -582,7 +625,15 @@ export class VMExecution extends Execution {
`Trie cache stats size=${tStats.size} reads=${tStats.cache.reads} hits=${tStats.cache.hits} ` +
`writes=${tStats.cache.writes} readsDB=${tStats.db.reads} hitsDB=${tStats.db.hits} writesDB=${tStats.db.writes}`
)
this.cacheStatsCount = 0

if (this.v8Engine !== null) {
const { used_heap_size, heap_size_limit } = this.v8Engine.getHeapStatistics()

const heapUsed = Math.round(used_heap_size / 1000 / 1000) // MB
const percentage = Math.round((100 * used_heap_size) / heap_size_limit)
this.config.logger.info(`Memory stats usage=${heapUsed} MB percentage=${percentage}%`)
}
this.statsCount = 0
}
}
}
4 changes: 1 addition & 3 deletions packages/client/lib/service/fullethereumservice.ts
Original file line number Diff line number Diff line change
Expand Up @@ -162,9 +162,7 @@ export class FullEthereumService extends EthereumService {
}
await super.start()
this.miner?.start()
if (!this.config.execCommon.gteHardfork(Hardfork.Paris)) {
void this.execution.run(true, true)
}
await this.execution.start()
return true
}

Expand Down
8 changes: 6 additions & 2 deletions packages/client/lib/sync/beaconsync.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,9 @@ export class BeaconSynchronizer extends Synchronizer {
await this.skeleton.open()

this.config.events.on(Event.SYNC_FETCHED_BLOCKS, this.processSkeletonBlocks)
this.config.events.on(Event.CHAIN_UPDATED, this.runExecution)
if (this.config.execution) {
this.config.events.on(Event.CHAIN_UPDATED, this.runExecution)
}

const { height: number, td } = this.chain.blocks
const hash = this.chain.blocks.latest!.hash()
Expand Down Expand Up @@ -331,7 +333,9 @@ export class BeaconSynchronizer extends Synchronizer {
async close() {
if (!this.opened) return
this.config.events.removeListener(Event.SYNC_FETCHED_BLOCKS, this.processSkeletonBlocks)
this.config.events.removeListener(Event.CHAIN_UPDATED, this.runExecution)
if (this.config.execution) {
this.config.events.removeListener(Event.CHAIN_UPDATED, this.runExecution)
}
await super.close()
}
}
8 changes: 6 additions & 2 deletions packages/client/lib/sync/fullsync.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,9 @@ export class FullSynchronizer extends Synchronizer {

this.config.events.on(Event.SYNC_FETCHED_BLOCKS, this.processBlocks)
this.config.events.on(Event.SYNC_EXECUTION_VM_ERROR, this.stop)
this.config.events.on(Event.CHAIN_UPDATED, this.runExecution)
if (this.config.execution) {
this.config.events.on(Event.CHAIN_UPDATED, this.runExecution)
}

await this.pool.open()
const { height: number, td } = this.chain.blocks
Expand Down Expand Up @@ -404,7 +406,9 @@ export class FullSynchronizer extends Synchronizer {
async stop(): Promise<boolean> {
this.config.events.removeListener(Event.SYNC_FETCHED_BLOCKS, this.processBlocks)
this.config.events.removeListener(Event.SYNC_EXECUTION_VM_ERROR, this.stop)
this.config.events.removeListener(Event.CHAIN_UPDATED, this.runExecution)
if (this.config.execution) {
this.config.events.removeListener(Event.CHAIN_UPDATED, this.runExecution)
}
return super.stop()
}

Expand Down
1 change: 1 addition & 0 deletions packages/client/lib/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,7 @@ export interface ClientOpts {
minPeers?: number
maxPeers?: number
dnsAddr?: string
execution?: boolean
numBlocksPerIteration?: number
accountCache?: number
storageCache?: number
Expand Down
13 changes: 13 additions & 0 deletions packages/client/lib/util/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,3 +56,16 @@ export function timeDiff(timestamp: number) {
const diff = new Date().getTime() / 1000 - timestamp
return timeDuration(diff)
}

// Dynamically load v8 for tracking mem stats
const isBrowser = new Function('try {return this===window;}catch(e){ return false;}')
export type V8Engine = {
getHeapStatistics: () => { heap_size_limit: number; used_heap_size: number }
}
let v8Engine: V8Engine | null = null
export async function getV8Engine(): Promise<V8Engine | null> {
if (isBrowser() === false && v8Engine === null) {
v8Engine = (await import('node:v8')) as V8Engine
}
return v8Engine
}
27 changes: 23 additions & 4 deletions packages/statemanager/src/stateManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -789,13 +789,32 @@ export class DefaultStateManager implements StateManager {
* Copies the current instance of the `StateManager`
* at the last fully committed point, i.e. as if all current
* checkpoints were reverted.
*
* Note on caches:
* 1. For caches instantiated as an LRU cache type
* the copy() method will instantiate with an ORDERED_MAP cache
* instead, since copied instantances are mostly used in
* short-term usage contexts and LRU cache instantation would create
* a large overhead here.
* 2. Cache values are generally not copied along
*/
copy(): StateManager {
const trie = this._trie.copy(false)
const prefixCodeHashes = this._prefixCodeHashes
let accountCacheOpts = { ...this._accountCacheSettings }
if (!this._accountCacheSettings.deactivate) {
accountCacheOpts = { ...accountCacheOpts, type: CacheType.ORDERED_MAP }
}
let storageCacheOpts = { ...this._storageCacheSettings }
if (!this._storageCacheSettings.deactivate) {
storageCacheOpts = { ...storageCacheOpts, type: CacheType.ORDERED_MAP }
}

return new DefaultStateManager({
trie: this._trie.copy(false),
prefixCodeHashes: this._prefixCodeHashes,
accountCacheOpts: this._accountCacheSettings,
storageCacheOpts: this._storageCacheSettings,
trie,
prefixCodeHashes,
accountCacheOpts,
storageCacheOpts,
})
}

Expand Down
9 changes: 0 additions & 9 deletions packages/statemanager/test/stateManager.account.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,6 @@ import { createAccount } from './util'

tape('StateManager -> General/Account', (t) => {
for (const accountCacheOpts of [{ deactivate: false }, { deactivate: true }]) {
t.test('should instantiate', async (st) => {
const stateManager = new DefaultStateManager({ accountCacheOpts })

st.deepEqual(stateManager._trie.root(), KECCAK256_RLP, 'it has default root')
const res = await stateManager.getStateRoot()
st.deepEqual(res, KECCAK256_RLP, 'it has default root')
st.end()
})

t.test('should set the state root to empty', async (st) => {
const stateManager = new DefaultStateManager({ accountCacheOpts })
st.ok(equalsBytes(stateManager._trie.root(), KECCAK256_RLP), 'it has default root')
Expand Down
50 changes: 50 additions & 0 deletions packages/statemanager/test/statemanager.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { KECCAK256_RLP } from '@ethereumjs/util'
import * as tape from 'tape'

import { CacheType, DefaultStateManager } from '../src'

tape('StateManager -> General', (t) => {
t.test('should instantiate', async (st) => {
const sm = new DefaultStateManager()

st.deepEqual(sm._trie.root(), KECCAK256_RLP, 'it has default root')
const res = await sm.getStateRoot()
st.deepEqual(res, KECCAK256_RLP, 'it has default root')
st.end()
})

t.test('copy()', async (st) => {
let sm = new DefaultStateManager({
prefixCodeHashes: false,
})

let smCopy = sm.copy()
st.equal(
(smCopy as any)._prefixCodeHashes,
(sm as any)._prefixCodeHashes,
'should retain non-default values'
)

sm = new DefaultStateManager({
accountCacheOpts: {
type: CacheType.LRU,
},
storageCacheOpts: {
type: CacheType.LRU,
},
})

smCopy = sm.copy()
st.equal(
(smCopy as any)._accountCacheSettings.type,
CacheType.ORDERED_MAP,
'should switch to ORDERED_MAP account cache on copy()'
)
st.equal(
(smCopy as any)._storageCacheSettings.type,
CacheType.ORDERED_MAP,
'should switch to ORDERED_MAP storage cache on copy()'
)
st.end()
})
})
Loading