diff --git a/beacon_chain/spec/beaconstate.nim b/beacon_chain/spec/beaconstate.nim index 42f6902945..11d50546b1 100644 --- a/beacon_chain/spec/beaconstate.nim +++ b/beacon_chain/spec/beaconstate.nim @@ -9,10 +9,11 @@ import tables, algorithm, math, sequtils, options, json_serialization/std/sets, chronicles, stew/bitseqs, ../extras, ../ssz, - ./crypto, ./datatypes, ./digest, ./helpers, ./validator + ./crypto, ./datatypes, ./digest, ./helpers, ./validator, + ../../nbench/bench_lab # https://github.com/ethereum/eth2.0-specs/blob/v0.9.3/specs/core/0_beacon-chain.md#is_valid_merkle_branch -func is_valid_merkle_branch*(leaf: Eth2Digest, branch: openarray[Eth2Digest], depth: uint64, index: uint64, root: Eth2Digest): bool = +func is_valid_merkle_branch*(leaf: Eth2Digest, branch: openarray[Eth2Digest], depth: uint64, index: uint64, root: Eth2Digest): bool {.nbench.}= ## Check if ``leaf`` at ``index`` verifies against the Merkle ``root`` and ## ``branch``. var @@ -48,7 +49,7 @@ func decrease_balance*( # https://github.com/ethereum/eth2.0-specs/blob/v0.8.4/specs/core/0_beacon-chain.md#deposits func process_deposit*( - state: var BeaconState, deposit: Deposit, flags: UpdateFlags = {}): bool = + state: var BeaconState, deposit: Deposit, flags: UpdateFlags = {}): bool {.nbench.}= # Process an Eth1 deposit, registering a validator or increasing its balance. # Verify the Merkle branch @@ -194,7 +195,7 @@ func initialize_beacon_state_from_eth1*( eth1_block_hash: Eth2Digest, eth1_timestamp: uint64, deposits: openArray[Deposit], - flags: UpdateFlags = {}): BeaconState = + flags: UpdateFlags = {}): BeaconState {.nbench.}= ## Get the genesis ``BeaconState``. ## ## Before the beacon chain starts, validators will register in the Eth1 chain @@ -315,7 +316,7 @@ func is_eligible_for_activation(state: BeaconState, validator: Validator): validator.activation_epoch == FAR_FUTURE_EPOCH # https://github.com/ethereum/eth2.0-specs/blob/v0.9.3/specs/core/0_beacon-chain.md#registry-updates -proc process_registry_updates*(state: var BeaconState) = +proc process_registry_updates*(state: var BeaconState) {.nbench.}= ## Process activation eligibility and ejections ## Try to avoid caching here, since this could easily become undefined @@ -500,7 +501,7 @@ proc check_attestation*( proc process_attestation*( state: var BeaconState, attestation: Attestation, flags: UpdateFlags, - stateCache: var StateCache): bool = + stateCache: var StateCache): bool {.nbench.}= # In the spec, attestation validation is mixed with state mutation, so here # we've split it into two functions so that the validation logic can be # reused when looking for suitable blocks to include in attestations. diff --git a/beacon_chain/spec/state_transition_block.nim b/beacon_chain/spec/state_transition_block.nim index 9bad7cf78c..7c6d007378 100644 --- a/beacon_chain/spec/state_transition_block.nim +++ b/beacon_chain/spec/state_transition_block.nim @@ -35,7 +35,8 @@ import algorithm, collections/sets, chronicles, options, sequtils, sets, tables, ../extras, ../ssz, metrics, - beaconstate, crypto, datatypes, digest, helpers, validator + beaconstate, crypto, datatypes, digest, helpers, validator, + ../../nbench/bench_lab # https://github.com/ethereum/eth2.0-metrics/blob/master/metrics.md#additional-metrics declareGauge beacon_current_live_validators, "Number of active validators that successfully included attestation on chain for current epoch" # On block @@ -46,7 +47,7 @@ declareGauge beacon_processed_deposits_total, "Number of total deposits included # https://github.com/ethereum/eth2.0-specs/blob/v0.9.3/specs/core/0_beacon-chain.md#block-header proc process_block_header*( state: var BeaconState, blck: BeaconBlock, flags: UpdateFlags, - stateCache: var StateCache): bool = + stateCache: var StateCache): bool {.nbench.}= # Verify that the slots match if not (blck.slot == state.slot): notice "Block header: slot mismatch", @@ -89,7 +90,7 @@ proc process_block_header*( # https://github.com/ethereum/eth2.0-specs/blob/v0.9.2/specs/core/0_beacon-chain.md#randao proc process_randao( state: var BeaconState, body: BeaconBlockBody, flags: UpdateFlags, - stateCache: var StateCache): bool = + stateCache: var StateCache): bool {.nbench.}= let epoch = state.get_current_epoch() proposer_index = get_beacon_proposer_index(state, stateCache) @@ -125,7 +126,7 @@ proc process_randao( true # https://github.com/ethereum/eth2.0-specs/blob/v0.9.3/specs/core/0_beacon-chain.md#eth1-data -func process_eth1_data(state: var BeaconState, body: BeaconBlockBody) = +func process_eth1_data(state: var BeaconState, body: BeaconBlockBody) {.nbench.}= state.eth1_data_votes.add body.eth1_data if state.eth1_data_votes.count(body.eth1_data) * 2 > SLOTS_PER_ETH1_VOTING_PERIOD: @@ -141,7 +142,7 @@ func is_slashable_validator(validator: Validator, epoch: Epoch): bool = # https://github.com/ethereum/eth2.0-specs/blob/v0.9.3/specs/core/0_beacon-chain.md#proposer-slashings proc process_proposer_slashing*( state: var BeaconState, proposer_slashing: ProposerSlashing, - flags: UpdateFlags, stateCache: var StateCache): bool = + flags: UpdateFlags, stateCache: var StateCache): bool {.nbench.}= if proposer_slashing.proposer_index.int >= state.validators.len: notice "Proposer slashing: invalid proposer index" return false @@ -187,7 +188,7 @@ proc process_proposer_slashing*( proc processProposerSlashings( state: var BeaconState, blck: BeaconBlock, flags: UpdateFlags, - stateCache: var StateCache): bool = + stateCache: var StateCache): bool {.nbench.}= if len(blck.body.proposer_slashings) > MAX_PROPOSER_SLASHINGS: notice "PropSlash: too many!", proposer_slashings = len(blck.body.proposer_slashings) @@ -217,7 +218,7 @@ proc process_attester_slashing*( state: var BeaconState, attester_slashing: AttesterSlashing, stateCache: var StateCache - ): bool = + ): bool {.nbench.}= let attestation_1 = attester_slashing.attestation_1 attestation_2 = attester_slashing.attestation_2 @@ -251,7 +252,7 @@ proc process_attester_slashing*( # https://github.com/ethereum/eth2.0-specs/blob/v0.9.3/specs/core/0_beacon-chain.md#attester-slashings proc processAttesterSlashings(state: var BeaconState, blck: BeaconBlock, - stateCache: var StateCache): bool = + stateCache: var StateCache): bool {.nbench.}= # Process ``AttesterSlashing`` operation. if len(blck.body.attester_slashings) > MAX_ATTESTER_SLASHINGS: notice "Attester slashing: too many!" @@ -265,7 +266,7 @@ proc processAttesterSlashings(state: var BeaconState, blck: BeaconBlock, # https://github.com/ethereum/eth2.0-specs/blob/v0.8.4/specs/core/0_beacon-chain.md#attestations proc processAttestations( state: var BeaconState, blck: BeaconBlock, flags: UpdateFlags, - stateCache: var StateCache): bool = + stateCache: var StateCache): bool {.nbench.}= ## Each block includes a number of attestations that the proposer chose. Each ## attestation represents an update to a specific shard and is signed by a ## committee of validators. @@ -285,7 +286,7 @@ proc processAttestations( true # https://github.com/ethereum/eth2.0-specs/blob/v0.8.4/specs/core/0_beacon-chain.md#deposits -proc processDeposits(state: var BeaconState, blck: BeaconBlock): bool = +proc processDeposits(state: var BeaconState, blck: BeaconBlock): bool {.nbench.}= if not (len(blck.body.deposits) <= MAX_DEPOSITS): notice "processDeposits: too many deposits" return false @@ -301,7 +302,7 @@ proc processDeposits(state: var BeaconState, blck: BeaconBlock): bool = proc process_voluntary_exit*( state: var BeaconState, signed_voluntary_exit: SignedVoluntaryExit, - flags: UpdateFlags): bool = + flags: UpdateFlags): bool {.nbench.}= let voluntary_exit = signed_voluntary_exit.message @@ -361,7 +362,7 @@ proc process_voluntary_exit*( true -proc processVoluntaryExits(state: var BeaconState, blck: BeaconBlock, flags: UpdateFlags): bool = +proc processVoluntaryExits(state: var BeaconState, blck: BeaconBlock, flags: UpdateFlags): bool {.nbench.}= if len(blck.body.voluntary_exits) > MAX_VOLUNTARY_EXITS: notice "[Block processing - Voluntary Exit]: too many exits!" return false @@ -372,7 +373,7 @@ proc processVoluntaryExits(state: var BeaconState, blck: BeaconBlock, flags: Upd proc processBlock*( state: var BeaconState, blck: BeaconBlock, flags: UpdateFlags, - stateCache: var StateCache): bool = + stateCache: var StateCache): bool {.nbench.}= ## When there's a new block, we need to verify that the block is sane and ## update the state accordingly diff --git a/beacon_chain/spec/state_transition_epoch.nim b/beacon_chain/spec/state_transition_epoch.nim index 0877981eff..d156d1947e 100644 --- a/beacon_chain/spec/state_transition_epoch.nim +++ b/beacon_chain/spec/state_transition_epoch.nim @@ -37,7 +37,8 @@ import stew/[bitseqs, bitops2], chronicles, json_serialization/std/sets, metrics, ../ssz, beaconstate, crypto, datatypes, digest, helpers, validator, - state_transition_helpers + state_transition_helpers, + ../../nbench/bench_lab # Logging utilities # -------------------------------------------------------- @@ -102,7 +103,7 @@ func get_attesting_balance( # https://github.com/ethereum/eth2.0-specs/blob/v0.9.3/specs/core/0_beacon-chain.md#justification-and-finalization proc process_justification_and_finalization*( - state: var BeaconState, stateCache: var StateCache) = + state: var BeaconState, stateCache: var StateCache) {.nbench.}= logScope: pcs = "process_justification_and_finalization" @@ -243,7 +244,7 @@ func get_base_reward(state: BeaconState, index: ValidatorIndex, # https://github.com/ethereum/eth2.0-specs/blob/v0.9.2/specs/core/0_beacon-chain.md#rewards-and-penalties-1 func get_attestation_deltas(state: BeaconState, stateCache: var StateCache): - tuple[a: seq[Gwei], b: seq[Gwei]] = + tuple[a: seq[Gwei], b: seq[Gwei]] {.nbench.}= let previous_epoch = get_previous_epoch(state) total_balance = get_total_active_balance(state) @@ -339,7 +340,7 @@ func get_attestation_deltas(state: BeaconState, stateCache: var StateCache): # https://github.com/ethereum/eth2.0-specs/blob/v0.9.3/specs/core/0_beacon-chain.md#rewards-and-penalties-1 func process_rewards_and_penalties( - state: var BeaconState, cache: var StateCache) = + state: var BeaconState, cache: var StateCache) {.nbench.}= if get_current_epoch(state) == GENESIS_EPOCH: return @@ -367,7 +368,7 @@ func process_slashings*(state: var BeaconState) = decrease_balance(state, index.ValidatorIndex, penalty) # https://github.com/ethereum/eth2.0-specs/blob/v0.9.3/specs/core/0_beacon-chain.md#final-updates -func process_final_updates*(state: var BeaconState) = +func process_final_updates*(state: var BeaconState) {.nbench.}= let current_epoch = get_current_epoch(state) next_epoch = current_epoch + 1 @@ -407,7 +408,7 @@ func process_final_updates*(state: var BeaconState) = state.current_epoch_attestations = @[] # https://github.com/ethereum/eth2.0-specs/blob/v0.9.3/specs/core/0_beacon-chain.md#epoch-processing -proc process_epoch*(state: var BeaconState) = +proc process_epoch*(state: var BeaconState) {.nbench.}= # @proc are placeholders trace "process_epoch", diff --git a/beacon_chain/state_transition.nim b/beacon_chain/state_transition.nim index ccacc1fdb2..efab6f831a 100644 --- a/beacon_chain/state_transition.nim +++ b/beacon_chain/state_transition.nim @@ -34,7 +34,8 @@ import collections/sets, chronicles, sets, ./extras, ./ssz, metrics, ./spec/[datatypes, digest, helpers, validator], - ./spec/[state_transition_block, state_transition_epoch] + ./spec/[state_transition_block, state_transition_epoch], + ../nbench/bench_lab # https://github.com/ethereum/eth2.0-metrics/blob/master/metrics.md#additional-metrics declareGauge beacon_current_validators, """Number of status="pending|active|exited|withdrawable" validators in current epoch""" # On epoch transition @@ -44,7 +45,7 @@ declareGauge beacon_previous_validators, """Number of status="pending|active|exi # --------------------------------------------------------------- # https://github.com/ethereum/eth2.0-specs/blob/v0.9.2/specs/core/0_beacon-chain.md#beacon-chain-state-transition-function -func process_slot*(state: var BeaconState) = +func process_slot*(state: var BeaconState) {.nbench.}= # Cache state root let previous_state_root = hash_tree_root(state) state.state_roots[state.slot mod SLOTS_PER_HISTORICAL_ROOT] = @@ -81,7 +82,7 @@ func get_epoch_validator_count(state: BeaconState): int64 = result += 1 # https://github.com/ethereum/eth2.0-specs/blob/v0.9.2/specs/core/0_beacon-chain.md#beacon-chain-state-transition-function -proc process_slots*(state: var BeaconState, slot: Slot) = +proc process_slots*(state: var BeaconState, slot: Slot) {.nbench.}= doAssert state.slot <= slot # Catch up to the target slot @@ -108,7 +109,7 @@ proc verifyStateRoot(state: BeaconState, blck: BeaconBlock): bool = true proc state_transition*( - state: var BeaconState, blck: BeaconBlock, flags: UpdateFlags): bool = + state: var BeaconState, blck: BeaconBlock, flags: UpdateFlags): bool {.nbench.}= ## Time in the beacon chain moves by slots. Every time (haha.) that happens, ## we will update the beacon state. Normally, the state updates will be driven ## by the contents of a new block, but it may happen that the block goes diff --git a/nbench/README.md b/nbench/README.md new file mode 100644 index 0000000000..45af9a59b9 --- /dev/null +++ b/nbench/README.md @@ -0,0 +1,70 @@ +# Nimbus-bench + +Nbench is a profiler dedicated to the Nimbus Beacon Chain. + +It is built as a domain specific profiler that aims to be +as unintrusive as possible while providing complementary reports +to dedicated tools like ``perf``, ``Apple Instruments`` or ``Intel Vtune`` +that allows you to dive deep down to a specific line or assembly instructions. + +In particular, those tools cannot tell you that your cryptographic subsystem +or your parsing routines or your random number generation should be revisited, +may sample at to high a resolution (millisecond) instead of per-function statistics, +and are much less useful without debugging symbols which requires a lot of space. +I.e. ``perf`` and other generic profiler tools give you the laser-thin focused pictures +while nbench strives to give you the big picture. + +Features +- by default nbench will collect the number of calls and time spent in + each function. +- like ncli or nfuzz, you can provide nbench isolated scenarios in SSZ format + to analyze Nimbus behaviour. + +## Usage + +``` +nim c -d:const_preset=mainnet -d:nbench -d:release -o:build/nbench nbench/nbench.nim +export SCENARIOS=tests/official/fixtures/tests-v0.9.3/mainnet/phase0 + +# Full state transition +build/nbench cmdFullStateTransition -d="${SCENARIOS}"/sanity/blocks/pyspec_tests/voluntary_exit/ -q=2 + +# Slot processing +build/nbench cmdSlotProcessing -d="${SCENARIOS}"/sanity/slots/pyspec_tests/slots_1 + +# Block header processing +build/nbench cmdBlockProcessing --blockProcessingCat=catBlockHeader -d="${SCENARIOS}"/operations/block_header/pyspec_tests/proposer_slashed/ + +# Proposer slashing +build/nbench cmdBlockProcessing --blockProcessingCat=catProposerSlashings -d="${SCENARIOS}"/operations/proposer_slashing/pyspec_tests/invalid_proposer_index/ + +# Attester slashing +build/nbench cmdBlockProcessing --blockProcessingCat=catAttesterSlashings -d="${SCENARIOS}"/operations/attester_slashing/pyspec_tests/success_surround/ + +# Attestation processing +build/nbench cmdBlockProcessing --blockProcessingCat=catAttestations -d="${SCENARIOS}"/operations/attestation/pyspec_tests/success_multi_proposer_index_iterations/ + +# Deposit processing +build/nbench cmdBlockProcessing --blockProcessingCat=catDeposits -d="${SCENARIOS}"/operations/deposit/pyspec_tests/new_deposit_max/ + +# Voluntary exit +build/nbench cmdBlockProcessing --blockProcessingCat=catVoluntaryExits -d="${SCENARIOS}"/operations/voluntary_exit/pyspec_tests/validator_exit_in_future/ +``` + +## Running the whole test suite + +Warning: this is a proof-of-concept, there is a slight degree of interleaving in output. +Furthermore benchmarks are run in parallel and might interfere which each other. + +``` +nim c -d:const_preset=mainnet -d:nbench -d:release -o:build/nbench nbench/nbench.nim +nim c -o:build/nbench_tests nbench/nbench_official_fixtures.nim +nbench_tests --nbench=build/nbench --tests=tests/official/fixtures/tests-v0.9.3/mainnet/ +``` + +## TODO Reporting +- Dumping as CSV files also for archival, perf regression suite and/or data mining. +- Piggybacking on eth-metrics and can report over Prometheus or StatsD. +- you can augment it via label pragmas that can be applied file-wide + to tag "cryptography", "block_transition", "database" to have a global view + of the system. diff --git a/nbench/bench_lab.nim b/nbench/bench_lab.nim new file mode 100644 index 0000000000..7a457e43a9 --- /dev/null +++ b/nbench/bench_lab.nim @@ -0,0 +1,135 @@ +# beacon_chain +# Copyright (c) 2018 Status Research & Development GmbH +# Licensed and distributed under either of +# * MIT license (license terms in the root directory or at https://opensource.org/licenses/MIT). +# * Apache v2 license (license terms in the root directory or at https://www.apache.org/licenses/LICENSE-2.0). +# at your option. This file may not be copied, modified, or distributed except according to those terms. + +import + # Standard lib + macros, std/[monotimes, times], + # Internal + platforms/x86 + +# Bench laboratory +# -------------------------------------------------- +# +# This file defines support data structures to enable profiling. + +# Utils +# -------------------------------------------------- +const someGcc = defined(gcc) or defined(llvm_gcc) or defined(clang) or defined(icc) +const hasThreadSupport = defined(threads) + +proc atomicInc*(memLoc: var int64, x = 1'i64): int64 = + when someGcc and hasThreadSupport: + result = atomicAddFetch(memLoc.addr, x, ATOMIC_RELAXED) + elif defined(vcc) and hasThreadSupport: + result = addAndFetch(memLoc.addr, x) + result += x + else: + memloc += x + result = memLoc + +# Types +# -------------------------------------------------- + +type + Metadata* = object + procName*: string + module: string + package: string + tag: string # Can be change to multi-tags later + # TODO - replace by eth-metrics once we figure out a CSV/JSON/Console backend + numCalls*: int64 + cumulatedTimeNs*: int64 # in nanoseconds + cumulatedCycles*: int64 + +var ctBenchMetrics*{.compileTime.}: seq[Metadata] + ## Metrics are collected here, this is just a temporary holder of compileTime values + ## Unfortunately the "seq" is emptied when passing the compileTime/runtime boundaries + ## due to Nim bugs + +var BenchMetrics*: seq[Metadata] + ## We can't directly use it at compileTime because it doesn't exist. + ## We need `BenchMetrics = static(ctBenchMetrics)` + ## To transfer the compileTime content to runtime at an opportune time. + +template ntag(tagname: string){.pragma.} + ## This will allow tagging proc in the future with + ## "crypto", "ssz", "block_transition", "epoch_transition" ... + +# Symbols +# -------------------------------------------------- + +template fnEntry(name: string, id: int, startTime, startCycle: untyped): untyped = + ## Bench tracing to insert on function entry + {.noSideEffect.}: + discard BenchMetrics[id].numCalls.atomicInc() + let startTime = getMonoTime() + let startCycle = getTicks() + +const nbench_trace {.booldefine.} = off # For manual "debug-echo"-style timing. +when nbench_trace: + # strformat doesn't work in templates. + from strutils import alignLeft, formatFloat + +template fnExit(name: string, id: int, startTime, startCycle: untyped): untyped = + ## Bench tracing to insert before each function exit + {.noSideEffect.}: + let stopCycle = getTicks() + let stopTime = getMonoTime() + let elapsedCycles = stopCycle - startCycle + let elapsedTime = inNanoseconds(stopTime - startTime) + + discard BenchMetrics[id].cumulatedTimeNs.atomicInc(elapsedTime) + discard BenchMetrics[id].cumulatedCycles.atomicInc(elapsedCycles) + + when nbench_trace: + # Advice: Use "when name == relevantProc" to isolate specific procedures. + # strformat doesn't work in templates. + echo static(alignLeft(name, 50)), + "Time (ms): ", alignLeft(formatFloat(elapsedTime.float64 * 1e-6, precision=3), 10), + "Cycles (billions): ", formatFloat(elapsedCycles.float64 * 1e-9, precision=3) + +macro nbenchAnnotate(procAst: untyped): untyped = + procAst.expectKind({nnkProcDef, nnkFuncDef}) + + let id = ctBenchMetrics.len + let name = procAst[0] + # TODO, get the module and the package the proc is coming from + # and the tag "crypto", "ssz", "block_transition", "epoch_transition" ... + + ctBenchMetrics.add Metadata(procName: $name, numCalls: 0, cumulatedTimeNs: 0, cumulatedCycles: 0) + var newBody = newStmtList() + let startTime = genSym(nskLet, "nbench_" & $name & "_startTime_") + let startCycle = genSym(nskLet, "nbench_" & $name & "_startCycles_") + newBody.add getAst(fnEntry($name, id, startTime, startCycle)) + newbody.add nnkDefer.newTree(getAst(fnExit($name, id, startTime, startCycle))) + newBody.add procAst.body + + procAst.body = newBody + result = procAst + +template nbench*(procBody: untyped): untyped = + when defined(nbench): + nbenchAnnotate(procBody) + else: + procBody + +# Sanity checks +# --------------------------------------------------- + +when isMainModule: + + expandMacros: + proc foo(x: int): int{.nbench.} = + echo "Hey hey hey" + result = x + + BenchMetrics = static(ctBenchMetrics) + + echo BenchMetrics + discard foo(10) + echo BenchMetrics + doAssert BenchMetrics[0].numCalls == 1 diff --git a/nbench/foo.nim b/nbench/foo.nim new file mode 100644 index 0000000000..748905344a --- /dev/null +++ b/nbench/foo.nim @@ -0,0 +1,5 @@ +import scenarios, confutils + +let scenario = ScenarioConf.load() + +echo scenario.attestation diff --git a/nbench/nbench.nim b/nbench/nbench.nim new file mode 100644 index 0000000000..d3677cdd29 --- /dev/null +++ b/nbench/nbench.nim @@ -0,0 +1,111 @@ +# beacon_chain +# Copyright (c) 2018 Status Research & Development GmbH +# Licensed and distributed under either of +# * MIT license (license terms in the root directory or at https://opensource.org/licenses/MIT). +# * Apache v2 license (license terms in the root directory or at https://www.apache.org/licenses/LICENSE-2.0). +# at your option. This file may not be copied, modified, or distributed except according to those terms. + +import + # Standard library + os, + # Status libraries + confutils, serialization, + # Beacon-chain + ../beacon_chain/spec/datatypes, + # Bench specific + scenarios, bench_lab, reports + +# Example: +# build/nbench cmdFullStateTransition -d + +# Nimbus Bench +# -------------------------------------------------- +# +# Run select scenarios and get statistics on Nimbus runtime behaviour + +when not defined(nbench): + {.error: "`nbench` requires `-d:nbench` flag to enable tracing on procedures.".} + +proc main() = + # TODO versioning + echo "Nimbus bench, preset \"", const_preset, '\"' + + BenchMetrics = static(ctBenchMetrics) # Make compile-time data available at runtime + let scenario = ScenarioConf.load() + + case scenario.cmd + of cmdFullStateTransition: + runFullTransition( + scenario.scenarioDir.string, + scenario.preState, + scenario.blocksPrefix, + scenario.blocksQty, + scenario.skipBLS + ) + of cmdSlotProcessing: + runProcessSlots( + scenario.scenarioDir.string, + scenario.preState, + scenario.numSlots + ) + of cmdBlockProcessing: + case scenario.blockProcessingCat + of catBlockHeader: + runProcessBlockHeader( + scenario.scenarioDir.string, + scenario.preState, + "block", # Pending https://github.com/status-im/nim-confutils/issues/11 + # scenario.attesterSlashing + scenario.skipBLS + ) + of catProposerSlashings: + runProcessProposerSlashing( + scenario.scenarioDir.string, + scenario.preState, + "proposer_slashing", # Pending https://github.com/status-im/nim-confutils/issues/11 + # scenario.attesterSlashing + scenario.skipBLS + ) + of catAttesterSlashings: + runProcessAttesterSlashing( + scenario.scenarioDir.string, + scenario.preState, + "attester_slashing" # Pending https://github.com/status-im/nim-confutils/issues/11 + # scenario.attesterSlashing + ) + of catAttestations: + runProcessAttestation( + scenario.scenarioDir.string, + scenario.preState, + "attestation", # Pending https://github.com/status-im/nim-confutils/issues/11 + # scenario.attestation, + scenario.skipBLS + ) + of catDeposits: + runProcessDeposit( + scenario.scenarioDir.string, + scenario.preState, + "deposit", # Pending https://github.com/status-im/nim-confutils/issues/11 + # scenario.deposit, + scenario.skipBLS + ) + of catVoluntaryExits: + runProcessVoluntaryExits( + scenario.scenarioDir.string, + scenario.preState, + "voluntary_exit", # Pending https://github.com/status-im/nim-confutils/issues/11 + # scenario.voluntary_exit, + scenario.skipBLS + ) + else: + quit "Unsupported" + else: + quit "Unsupported" + + # TODO: Nimbus not fine-grained enough in UpdateFlags + let flags = if scenario.skipBLS: "[skipBLS, skipStateRootVerification]" + else: "[withBLS, withStateRootVerification]" + reportCli(BenchMetrics, const_preset, flags) + +when isMainModule: + main() diff --git a/nbench/nbench.nim.cfg b/nbench/nbench.nim.cfg new file mode 100644 index 0000000000..1bc8b701fa --- /dev/null +++ b/nbench/nbench.nim.cfg @@ -0,0 +1 @@ +-d:nbench diff --git a/nbench/nbench_official_fixtures.nim b/nbench/nbench_official_fixtures.nim new file mode 100644 index 0000000000..5e03632735 --- /dev/null +++ b/nbench/nbench_official_fixtures.nim @@ -0,0 +1,70 @@ +# beacon_chain +# Copyright (c) 2018 Status Research & Development GmbH +# Licensed and distributed under either of +# * MIT license (license terms in the root directory or at https://opensource.org/licenses/MIT). +# * Apache v2 license (license terms in the root directory or at https://www.apache.org/licenses/LICENSE-2.0). +# at your option. This file may not be copied, modified, or distributed except according to those terms. + +import + # Standard library + os, osproc, strformat, + # Status libraries + confutils + +# Nimbus Bench Batch +# -------------------------------------------------- +# This script calls Nimbus bench in parallel batch +# to run a series of benchmarks from the official SSZ tests + +type + CmdLists = seq[string] + +proc collectTarget(cmds: var CmdLists, nbench, name, cmd, cat, path: string) = + echo "----------------------------------------" + echo "Collecting ", name, " transitions" + echo "----------------------------------------" + for folder in walkDirRec(path, yieldFilter = {pcDir}, relative = true): + echo "Found: ", folder + var cat = cat + if cmd == "cmdBlockProcessing": + cat = "--blockProcessingCat=" & cat + cmds.add &"{nbench} {cmd} {cat} -d={path/folder}" + +proc collectBenchTargets(nbench, basePath: string): CmdLists = + block: # Full state transitions + echo "----------------------------------------" + echo "Collecting full state transitions" + echo "----------------------------------------" + let path = basePath/"phase0"/"sanity"/"blocks"/"pyspec_tests" + for folder in walkDirRec(path, yieldFilter = {pcDir}, relative = true): + var countBlocks = 0 + for _ in walkFiles(path/folder/"blocks_*.ssz"): + inc countBlocks + echo "Found: ", folder, " with ", countBlocks, " blocks" + result.add &"{nbench} cmdFullStateTransition -d={path/folder} -q={$countBlocks}" + block: # Slot processing + let path = basePath/"phase0"/"sanity"/"slots"/"pyspec_tests" + result.collectTarget(nbench, "slot", "cmdSlotProcessing", "", path) + block: # Attestation + let path = basePath/"phase0"/"operations"/"attestation"/"pyspec_tests" + result.collectTarget(nbench, "attestation", "cmdBlockProcessing", "catAttestations", path) + block: # Attester_slashing + let path = basePath/"phase0"/"operations"/"attester_slashing"/"pyspec_tests" + result.collectTarget(nbench, "attester_slashing", "cmdBlockProcessing", "catAttesterSlashings", path) + block: # block_header + let path = basePath/"phase0"/"operations"/"block_header"/"pyspec_tests" + result.collectTarget(nbench, "block_header", "cmdBlockProcessing", "catBlockHeader", path) + block: # deposit + let path = basePath/"phase0"/"operations"/"deposit"/"pyspec_tests" + result.collectTarget(nbench, "deposit", "cmdBlockProcessing", "catDeposits", path) + block: # proposer_slashing + let path = basePath/"phase0"/"operations"/"proposer_slashing"/"pyspec_tests" + result.collectTarget(nbench, "proposer_slashing", "cmdBlockProcessing", "catProposerSlashings", path) + block: # voluntary_exit + let path = basePath/"phase0"/"operations"/"voluntary_exit"/"pyspec_tests" + result.collectTarget(nbench, "voluntary_exit", "cmdBlockProcessing", "catVoluntaryExits", path) + +cli do(nbench: string, tests: string): + let cmdLists = collectBenchTargets(nbench, tests) + let err = execProcesses(cmdLists) + quit err diff --git a/nbench/platforms/x86.nim b/nbench/platforms/x86.nim new file mode 100644 index 0000000000..45ce936054 --- /dev/null +++ b/nbench/platforms/x86.nim @@ -0,0 +1,125 @@ +# beacon_chain +# Copyright (c) 2018 Status Research & Development GmbH +# Licensed and distributed under either of +# * MIT license (license terms in the root directory or at https://opensource.org/licenses/MIT). +# * Apache v2 license (license terms in the root directory or at https://www.apache.org/licenses/LICENSE-2.0). +# at your option. This file may not be copied, modified, or distributed except according to those terms. + +# Cpu Name +# ------------------------------------------------------- + +{.passC:"-std=gnu99".} # TODO may conflict with milagro "-std=c99" + +proc cpuID(eaxi, ecxi: int32): tuple[eax, ebx, ecx, edx: int32] = + when defined(vcc): + proc cpuidVcc(cpuInfo: ptr int32; functionID: int32) + {.importc: "__cpuidex", header: "intrin.h".} + cpuidVcc(addr result.eax, eaxi, ecxi) + else: + var (eaxr, ebxr, ecxr, edxr) = (0'i32, 0'i32, 0'i32, 0'i32) + asm """ + cpuid + :"=a"(`eaxr`), "=b"(`ebxr`), "=c"(`ecxr`), "=d"(`edxr`) + :"a"(`eaxi`), "c"(`ecxi`)""" + (eaxr, ebxr, ecxr, edxr) + +proc cpuName*(): string = + var leaves {.global.} = cast[array[48, char]]([ + cpuID(eaxi = 0x80000002'i32, ecxi = 0), + cpuID(eaxi = 0x80000003'i32, ecxi = 0), + cpuID(eaxi = 0x80000004'i32, ecxi = 0)]) + result = $cast[cstring](addr leaves[0]) + +# Counting cycles +# ------------------------------------------------------- + +# From Linux +# +# The RDTSC instruction is not ordered relative to memory +# access. The Intel SDM and the AMD APM are both vague on this +# point, but empirically an RDTSC instruction can be +# speculatively executed before prior loads. An RDTSC +# immediately after an appropriate barrier appears to be +# ordered as a normal load, that is, it provides the same +# ordering guarantees as reading from a global memory location +# that some other imaginary CPU is updating continuously with a +# time stamp. +# +# From Intel SDM +# https://www.intel.com/content/dam/www/public/us/en/documents/white-papers/ia-32-ia-64-benchmark-code-execution-paper.pdf + +proc getTicks*(): int64 {.inline.} = + when defined(vcc): + proc rdtsc(): int64 {.sideeffect, importc: "__rdtsc", header: "".} + proc lfence() {.importc: "__mm_lfence", header: "".} + + lfence() + return rdtsc() + + else: + when defined(amd64): + var lo, hi: int64 + # TODO: Provide a compile-time flag for RDTSCP support + # and use it instead of lfence + RDTSC + {.emit: """asm volatile( + "lfence\n" + "rdtsc\n" + : "=a"(`lo`), "=d"(`hi`) + : + : "memory" + );""".} + return (hi shl 32) or lo + else: # 32-bit x86 + # TODO: Provide a compile-time flag for RDTSCP support + # and use it instead of lfence + RDTSC + {.emit: """asm volatile( + "lfence\n" + "rdtsc\n" + : "=a"(`result`) + : + : "memory" + );""".} + +# Sanity check +# ------------------------------------------------------- + +when isMainModule: + + import std/[times, monotimes, math, volatile, os] + + block: # CpuName + echo "Your CPU is: " + echo " ", cpuName() + + block: # Cycle Count + echo "The cost of an int64 modulo operation on your platform is:" + + # Dealing with compiler optimization on microbenchmarks is hard + {.pragma: volatile, codegenDecl: "volatile $# $#".} + + proc modNtimes(a, b: int64, N: int) {.noinline.} = + var c{.volatile.}: int64 + for i in 0 ..< N: + c.addr.volatileStore(a.unsafeAddr.volatileLoad() mod b.unsafeAddr.volatileLoad()) + + let a {.volatile.} = 1000003'i64 # a prime number + let b {.volatile.} = 10007'i64 # another prime number + let N {.volatile.} = 3_000_000 + + let startMono = getMonoTime() + let startCycles = getTicks() + modNtimes(a, b, N) + let stopCycles = getTicks() + let stopMono = getMonoTime() + + + let elapsedMono = inNanoseconds(stopMono - startMono) + let elapsedCycles = stopCycles - startCycles + let timerResolutionGHz = round(elapsedCycles.float32 / elapsedMono.float32, 3) + + echo " ", (elapsedCycles) div N, " cycles" + echo " ", (elapsedMono) div N, " ns/iter" + echo " ", timerResolutionGHz, " GHz (timer resolution)" + + block: # CPU Frequency + discard # TODO, surprisingly this is very complex diff --git a/nbench/reports.nim b/nbench/reports.nim new file mode 100644 index 0000000000..aa4000455e --- /dev/null +++ b/nbench/reports.nim @@ -0,0 +1,50 @@ +# beacon_chain +# Copyright (c) 2018 Status Research & Development GmbH +# Licensed and distributed under either of +# * MIT license (license terms in the root directory or at https://opensource.org/licenses/MIT). +# * Apache v2 license (license terms in the root directory or at https://www.apache.org/licenses/LICENSE-2.0). +# at your option. This file may not be copied, modified, or distributed except according to those terms. + +import + # Standard library + strformat, strutils, + # Bench + bench_lab + +template cpuX86(body: untyped): untyped = + when defined(i386) or defined(amd64): + body + +cpuX86: + import platforms/x86 + +# Reporting benchmark result +# ------------------------------------------------------- + +proc reportCli*(metrics: seq[Metadata], preset, flags: string) = + + cpuX86: + let name = cpuName() + echo "\nCPU: ", name + + # https://blog.trailofbits.com/2019/10/03/tsc-frequency-for-all-better-profiling-and-benchmarking/ + # https://www.agner.org/optimize/blog/read.php?i=838 + echo "The CPU Cycle Count is indicative only. It cannot be used to compare across systems, works at your CPU nominal frequency and is sensitive to overclocking, throttling and frequency scaling (powersaving and Turbo Boost)." + + const lineSep = &"""|{'-'.repeat(50)}|{'-'.repeat(14)}|{'-'.repeat(15)}|{'-'.repeat(17)}|{'-'.repeat(26)}|{'-'.repeat(26)}|""" + echo "\n" + echo lineSep + echo &"""|{"Procedures (" & preset & ')':^50}|{"# of Calls":^14}|{"Time (ms)":^15}|{"Avg Time (ms)":^17}|{"CPU cycles (in billions)":^26}|{"Avg cycles (in billions)":^26}|""" + echo &"""|{flags:^50}|{' '.repeat(14)}|{' '.repeat(15)}|{' '.repeat(17)}|{"indicative only":^26}|{"indicative only":^26}|""" + echo lineSep + for m in metrics: + if m.numCalls == 0: + continue + # TODO: running variance / standard deviation but the Welford method is quite costly. + # https://nim-lang.org/docs/stats.html / https://en.wikipedia.org/wiki/Algorithms_for_calculating_variance#Welford's_online_algorithm + let cumulTimeMs = m.cumulatedTimeNs.float64 * 1e-6 + let avgTimeMs = cumulTimeMs / m.numCalls.float64 + let cumulCyclesBillions = m.cumulatedCycles.float64 * 1e-9 + let avgCyclesBillions = cumulCyclesBillions / m.numCalls.float64 + echo &"""|{m.procName:<50}|{m.numCalls:>14}|{cumulTimeMs:>15.3f}|{avgTimeMs:>17.3f}|{cumulCyclesBillions:>26.3f}|{avgCyclesBillions:>26.3f}|""" + echo lineSep diff --git a/nbench/scenarios.nim b/nbench/scenarios.nim new file mode 100644 index 0000000000..9bfa9e8584 --- /dev/null +++ b/nbench/scenarios.nim @@ -0,0 +1,209 @@ +# beacon_chain +# Copyright (c) 2018 Status Research & Development GmbH +# Licensed and distributed under either of +# * MIT license (license terms in the root directory or at https://opensource.org/licenses/MIT). +# * Apache v2 license (license terms in the root directory or at https://www.apache.org/licenses/LICENSE-2.0). +# at your option. This file may not be copied, modified, or distributed except according to those terms. + +import + # Standard library + os, + # Status libraries + confutils/defs, serialization, + # Beacon-chain + ../beacon_chain/spec/[datatypes, crypto, beaconstate, validator, state_transition_block], + ../beacon_chain/[ssz, state_transition, extras] + +# Nimbus Bench - Scenario configuration +# -------------------------------------------------- + +type + StartupCommand* = enum + noCommand + cmdFullStateTransition + cmdSlotProcessing + cmdBlockProcessing + cmdEpochProcessing + + BlockProcessingCat* = enum + catBlockHeader + catRANDAO + catEth1Data + catProposerSlashings + catAttesterSlashings + catAttestations + catDeposits + catVoluntaryExits + + ScenarioConf* = object + scenarioDir* {. + desc: "The directory of your benchmark scenario" + name: "scenario-dir" + abbr: "d" + required .}: InputDir + preState* {. + desc: "The name of your pre-state (without .ssz)" + name: "pre" + abbr: "p" + defaultValue: "pre".}: string + blocksPrefix* {. + desc: "The prefix of your blocks file, for exemple \"blocks_\" for blocks in the form \"blocks_XX.ssz\"" + name: "blocks-prefix" + abbr: "b" + defaultValue: "blocks_".}: string + blocksQty* {. + desc: "The number of blocks to process for this transition. Blocks should start at 0." + name: "block-quantity" + abbr: "q" + defaultValue: 1.}: int + skipBLS*{. + desc: "Skip BLS public keys and signature verification" + name: "skip-bls" + defaultValue: true.}: bool + case cmd*{. + command + defaultValue: noCommand }: StartupCommand + of noCommand: + discard + of cmdFullStateTransition: + discard + of cmdSlotProcessing: + numSlots* {. + desc: "The number of slots the pre-state will be advanced by" + name: "num-slots" + abbr: "s" + defaultValue: 1.}: uint64 + of cmdBlockProcessing: + case blockProcessingCat* {. + desc: "block transitions" + # name: "process-blocks" # Pending https://github.com/status-im/nim-confutils/issues/10 + implicitlySelectable + required .}: BlockProcessingCat + of catBlockHeader: + blockHeader*{. + desc: "Block header filename (without .ssz)" + name: "block-header" + defaultValue: "block".}: string + of catRANDAO: + discard + of catEth1Data: + discard + of catProposerSlashings: + proposerSlashing*{. + desc: "Proposer slashing filename (without .ssz)" + name: "proposer-slashing" + defaultValue: "proposer_slashing".}: string + of catAttesterSlashings: + attesterSlashing*{. + desc: "Attester slashing filename (without .ssz)" + name: "attester-slashing" + defaultValue: "attester_slashing".}: string + of catAttestations: + attestation*{. + desc: "Attestation filename (without .ssz)" + name: "attestation" + defaultValue: "attestation".}: string + of catDeposits: + deposit*{. + desc: "Deposit filename (without .ssz)" + name: "deposit" + defaultValue: "deposit".}: string + of catVoluntaryExits: + voluntaryExit*{. + desc: "Voluntary Exit filename (without .ssz)" + name: "voluntary_exit" + defaultValue: "voluntary_exit".}: string + of cmdEpochProcessing: + discard + +proc parseSSZ(path: string, T: typedesc): T = + try: + result = SSZ.loadFile(path, T) + except SerializationError as err: + writeStackTrace() + stderr.write "SSZ load issue for file \"", path, "\"\n" + stderr.write err.formatMsg(path), "\n" + quit 1 + except CatchableError as err: + writeStackTrace() + stderr.write "SSZ load issue for file \"", path, "\"\n" + quit 1 + +proc runFullTransition*(dir, preState, blocksPrefix: string, blocksQty: int, skipBLS: bool) = + let prePath = dir / preState & ".ssz" + + var state: ref BeaconState + new state + echo "Running: ", prePath + state[] = parseSSZ(prePath, BeaconState) + + for i in 0 ..< blocksQty: + let blockPath = dir / blocksPrefix & $i & ".ssz" + echo "Processing: ", blockPath + + let blck = parseSSZ(blockPath, SignedBeaconBlock) + let flags = if skipBLS: {skipValidation} # TODO: this also skips state root verification + else: {} + let success = state_transition(state[], blck.message, flags) + echo "State transition status: ", if success: "SUCCESS ✓" else: "FAILURE ⚠️" + +proc runProcessSlots*(dir, preState: string, numSlots: uint64) = + let prePath = dir / preState & ".ssz" + + var state: ref BeaconState + new state + echo "Running: ", prePath + state[] = parseSSZ(prePath, BeaconState) + + process_slots(state[], state.slot + numSlots) + +template processScenarioImpl( + dir, preState: string, skipBLS: bool, + transitionFn, paramName: untyped, + ConsensusObject: typedesc, + needFlags, needCache: static bool): untyped = + let prePath = dir/preState & ".ssz" + + var state: ref BeaconState + new state + echo "Running: ", prePath + state[] = parseSSZ(prePath, BeaconState) + + var consObj: ref `ConsensusObject` + new consObj + when needCache: + var cache = get_empty_per_epoch_cache() + when needFlags: + let flags = if skipBLS: {skipValidation} # TODO: this also skips state root verification + else: {} + + let consObjPath = dir/paramName & ".ssz" + echo "Processing: ", consObjPath + consObj[] = parseSSZ(consObjPath, ConsensusObject) + + when needFlags and needCache: + let success = transitionFn(state[], consObj[], flags, cache) + elif needFlags: + let success = transitionFn(state[], consObj[], flags) + elif needCache: + let success = transitionFn(state[], consObj[], cache) + else: + let success = transitionFn(state[], consObj[]) + + echo astToStr(transitionFn) & " status: ", if success: "SUCCESS ✓" else: "FAILURE ⚠️" + +template genProcessScenario(name, transitionFn, paramName: untyped, ConsensusObject: typedesc, needFlags, needCache: static bool): untyped = + when needFlags: + proc `name`*(dir, preState, `paramName`: string, skipBLS: bool) = + processScenarioImpl(dir, preState, skipBLS, transitionFn, paramName, ConsensusObject, needFlags, needCache) + else: + proc `name`*(dir, preState, `paramName`: string) = + # skipBLS is a dummy to avoid undeclared identifier + processScenarioImpl(dir, preState, skipBLS = false, transitionFn, paramName, ConsensusObject, needFlags, needCache) + +genProcessScenario(runProcessBlockHeader, process_block_header, block_header, BeaconBlock, needFlags = true, needCache = true) +genProcessScenario(runProcessProposerSlashing, process_proposer_slashing, proposer_slashing, ProposerSlashing, needFlags = true, needCache = true) +genProcessScenario(runProcessAttestation, process_attestation, attestation, Attestation, needFlags = true, needCache = true) +genProcessScenario(runProcessAttesterSlashing, process_attester_slashing, att_slash, AttesterSlashing, needFlags = false, needCache = true) +genProcessScenario(runProcessDeposit, process_deposit, deposit, Deposit, needFlags = true, needCache = false) +genProcessScenario(runProcessVoluntaryExits, process_voluntary_exit, deposit, SignedVoluntaryExit, needFlags = true, needCache = false) diff --git a/tests/official/test_fixture_operations_attester_slashings.nim b/tests/official/test_fixture_operations_attester_slashings.nim index a680d47412..f08a2b0c3f 100644 --- a/tests/official/test_fixture_operations_attester_slashings.nim +++ b/tests/official/test_fixture_operations_attester_slashings.nim @@ -30,10 +30,7 @@ template runTest(identifier: untyped) = proc `testImpl _ operations_attester_slashing _ identifier`() = - var flags: UpdateFlags var prefix: string - if not existsFile(testDir/"meta.yaml"): - flags.incl skipValidation if existsFile(testDir/"post.ssz"): prefix = "[Valid] " else: