Nbench - Flexible benchmarking of Nimbus internals (#641)

* nbench PoC

* Remove the yaml files from the example scenarios

* update README with current status

* Add an alternative implementation that uses defer

* Forgot to add the old proc body

* slots-processing

* allow benching state_transition failures

* Add Attestations processing (workaround confutils bug:
- https://github.com/status-im/nim-confutils/issues/10
- https://github.com/status-im/nim-confutils/issues/11
- https://github.com/status-im/nim-confutils/issues/12

* Add CLI command in the readme

* Filter report and add notes about CPU cycles

* Report averages

* Add debugecho style time/cycle print

* Report when we skip BLS and state root verification

* Update to 0.9.3

* Generalize scenario parsing

* Support all block processing scenarios

* parallel bench runner PoC

* gitBetter load issues reporting (the load issues were invalid signature and expected to fail)
This commit is contained in:
Mamy Ratsimbazafy 2019-12-20 17:14:43 +01:00 committed by GitHub
parent 417f96213d
commit 106352aff3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 809 additions and 32 deletions

View File

@ -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.

View File

@ -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

View File

@ -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",

View File

@ -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

70
nbench/README.md Normal file
View File

@ -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.

135
nbench/bench_lab.nim Normal file
View File

@ -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

5
nbench/foo.nim Normal file
View File

@ -0,0 +1,5 @@
import scenarios, confutils
let scenario = ScenarioConf.load()
echo scenario.attestation

111
nbench/nbench.nim Normal file
View File

@ -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()

1
nbench/nbench.nim.cfg Normal file
View File

@ -0,0 +1 @@
-d:nbench

View File

@ -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

125
nbench/platforms/x86.nim Normal file
View File

@ -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: "<intrin.h>".}
proc lfence() {.importc: "__mm_lfence", header: "<intrin.h>".}
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

50
nbench/reports.nim Normal file
View File

@ -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

209
nbench/scenarios.nim Normal file
View File

@ -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)

View File

@ -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: