-
Notifications
You must be signed in to change notification settings - Fork 1k
core: standardize slow block JSON output for cross-client metrics #9660
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
base: main
Are you sure you want to change the base?
core: standardize slow block JSON output for cross-client metrics #9660
Conversation
ed18b5c to
a97b4a2
Compare
|
@CPerezz thanks for this - can you follow the instructions here to add commit signoff which will fix the DCO https://github.com/hyperledger/besu/pull/9660/checks?check_run_id=61142420367 |
Implements execution metrics following the cross-client specification: https://github.com/ethereum/execution-specs/blob/main/docs/execution-metrics-spec.md - Add ExecutionStats class for collecting block processing statistics - Add EXECUTION metric category to BesuMetricCategory - Add execution timing (execution, validation, commit phases) - Add slow block logging in JSON format (threshold: 1000ms) - Track gas usage and transaction counts per block Signed-off-by: CPerezz <[email protected]>
Implement standardized JSON format for slow block logging to enable cross-client performance analysis and protocol research. This change is part of the Cross-Client Execution Metrics initiative proposed by Gary Rong and CPerezz: https://hackmd.io/dg7rizTyTXuCf2LSa2LsyQ The standardized metrics enabled data-driven analysis like the EIP-7907 research: https://ethresear.ch/t/data-driven-analysis-on-eip-7907/23850 JSON format includes: - block: number, hash, gas_used, tx_count - timing: execution_ms, total_ms - throughput: mgas_per_sec - state_reads: accounts, storage_slots, code, code_bytes - state_writes: accounts, storage_slots - cache: account/storage/code hits, misses, hit_rate - evm: sload, sstore, calls, creates Signed-off-by: CPerezz <[email protected]>
Convert timing getter methods from long to double to preserve sub-millisecond precision in slow block JSON output. Changes: - getExecutionTimeMs(), getStateReadTimeMs(), etc: long -> double - Division changed from integer (/ 1_000_000) to float (/ 1_000_000.0) - JSON format string: %d -> %.3f for 3 decimal places - Update test assertions for double comparisons Signed-off-by: CPerezz <[email protected]>
Add tracking for EIP-7702 delegation set/cleared operations as part of the cross-client execution metrics standardization effort. New fields in ExecutionStats: - eip7702DelegationsSet: Number of EIP-7702 delegations set - eip7702DelegationsCleared: Number of EIP-7702 delegations cleared Includes increment methods and JSON output in toSlowBlockJson(). These fields will be 0 for pre-Pectra blocks per spec. Signed-off-by: CPerezz <[email protected]>
06da4e4 to
09fdb2b
Compare
macfarla
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
logging logic looks ok. a few comments.
@CPerezz is this PR meant to add metrics as well? Looks like you've added the category but no metrics
we prob want to check for performance impact
ethereum/core/src/main/java/org/hyperledger/besu/ethereum/mainnet/ExecutionStatsHolder.java
Outdated
Show resolved
Hide resolved
|
|
||
| /** Threshold in milliseconds for slow block logging. */ | ||
| private static final long SLOW_BLOCK_THRESHOLD_MS = | ||
| Long.getLong("besu.execution.slowBlockThresholdMs", 1000L); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
prob worth documenting that this option is configurable via system prop
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do you prefer system prop for standardization reasons? It would be more besu-idiomatic to use a cli flag (which automatically gives you toml and envvar config too)
ethereum/core/src/main/java/org/hyperledger/besu/ethereum/mainnet/AbstractBlockProcessor.java
Outdated
Show resolved
Hide resolved
metrics/core/src/main/java/org/hyperledger/besu/metrics/BesuMetricCategory.java
Outdated
Show resolved
Hide resolved
siladu
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Excited to get these changes in. Have done a quick first pass. I would like to assess performance impact and I need to think about the ThreadLocal approach some more.
| @@ -75,6 +79,12 @@ TransactionReceipt create( | |||
| } | |||
|
|
|||
| private static final Logger LOG = LoggerFactory.getLogger(AbstractBlockProcessor.class); | |||
| private static final Logger SLOW_BLOCK_LOG = LoggerFactory.getLogger("SlowBlock"); | |||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is there a reason to require a separate logger here?
pros: avoids potential noise for existing besu users (including non-Ethereum networks)
cons: you might need some extra config to see these logs - that can be a runtime log4j config though so might suit your use case fine.
|
|
||
| /** Threshold in milliseconds for slow block logging. */ | ||
| private static final long SLOW_BLOCK_THRESHOLD_MS = | ||
| Long.getLong("besu.execution.slowBlockThresholdMs", 1000L); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do you prefer system prop for standardization reasons? It would be more besu-idiomatic to use a cli flag (which automatically gives you toml and envvar config too)
| public final class ExecutionStatsHolder { | ||
|
|
||
| private static final ThreadLocal<ExecutionStats> CURRENT = new ThreadLocal<>(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
At first glance I'm skeptical about this approach. It has clean separation but I need to have a think if there's a more Besu-appropriate way like using a Tracer or even a log4j MDC. Also need to consider performance impact.
| @@ -266,6 +265,9 @@ public void persist(final BlockHeader blockHeader, final StateRootCommitter comm | |||
| success = true; | |||
| } finally { | |||
| if (success) { | |||
| // Track commit time (writing state to DB) | |||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I wonder how informative this stat is going to be with something like RocksDB where writes are fast but compaction/reads are more significant. Still, might be interesting to compare with other clients.
|
|
||
| boolean parallelizedTxFound = false; | ||
| int nbParallelTx = 0; | ||
|
|
||
| // Execution metrics tracking | ||
| final ExecutionStats executionStats = new ExecutionStats(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this intended to run at all times? I would implement this via a Tracer which you can add via ProtocolContext and behind a besu flag to enable/disable when not needed.
The tracer should cover all your use cases from what I can see.
lu-pinto
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As noted in the comment I believe this should be done via a Tracer using dependency injection and not using static calls.
- Remove package.json and package-lock.json (accidental inclusions) - Remove unused EXECUTION metric category from BesuMetricCategory - Remove unused runWith() method from ExecutionStatsHolder Addresses review feedback from macfarla. Signed-off-by: Carlos Perez <[email protected]> Signed-off-by: CPerezz <[email protected]>
Address reviewer feedback by implementing the execution metrics collection as a BlockAwareOperationTracer instead of embedding it directly in AbstractBlockProcessor. Key changes: - Create SlowBlockTracer implementing BlockAwareOperationTracer - Move slow block logging and metrics collection into the tracer - Support tracer composition (wraps delegate tracer) - Centralize state access counters in EvmOperationCounters - Remove direct ExecutionStats management from AbstractBlockProcessor - Delete obsolete unit tests in favor of integration test The SlowBlockTracer is enabled via system property (threshold >= 0) and wraps the existing block import tracer. Future work will convert the system property to a CLI flag for better Besu integration. Signed-off-by: CPerezz <[email protected]>
|
running some nodes with this to verify on our side @CPerezz |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks @CPerezz for the proposal, I really like the idea of standardising these metrics across all clients.
I think the PR is not ready as it is with current design. This is how we should implement these metrics :
- All counters and opcode related metrics should be handled by a tracer, so no change in EVM operations, in the same way debug_trace* RPC calls work
- The tracer will be enabled behind a new flag that adds these metrics, so we can have the possibility to disable them
- The latency metrics can be inserted in the code, but need to take into account parallel execution of the transactions, and execute as less as possible as it is on the hot path.
- In general, the implementation needs to take into account transaction parallel execution and some of the transactions that are executed in parallel are not committed as sequential thread can be faster or in case of a conflit.
| private static final ObjectMapper JSON_MAPPER = new ObjectMapper(); | ||
|
|
||
| private final long slowBlockThresholdMs; | ||
| private final BlockAwareOperationTracer delegate; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There is no need for delegation if we don't want to stop tracing because of an interruption.
| @@ -62,6 +63,8 @@ public OperationResult execute(final MessageFrame frame, final EVM evm) { | |||
| return new OperationResult(cost, ExceptionalHaltReason.INSUFFICIENT_GAS); | |||
| } else { | |||
| frame.pushStackItem(getStorageValue(account, UInt256.fromBytes(key), frame)); | |||
| // Track SLOAD for cross-client execution metrics | |||
| EvmOperationCounters.incrementSload(); | |||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Using a tracer means that we should do explicit calls in EVM Operations.
| */ | ||
| public final class EvmOperationCounters { | ||
|
|
||
| private static final ThreadLocal<Counters> COUNTERS = ThreadLocal.withInitial(Counters::new); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think ThreadLocal is a good idea in its own, but you need to handle the case of parallel execution and conflict detection.
You need to cumulate the results of all threads at the end of block execution and remove non committed transactions counters from parallel executions, otherwise some opcodes will be counted twice. This is a tricky one BTW, cc. @matkt.
| @@ -98,6 +99,7 @@ public int getStackItemsProduced() { | |||
| * @return the {@link Account}, or {@code null} if it does not exist | |||
| */ | |||
| protected Account getAccount(final Address address, final MessageFrame frame) { | |||
| EvmOperationCounters.incrementAccountReads(); | |||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The use of tracer means we should not change the code in hot path.
| @@ -113,6 +115,7 @@ protected Account getAccount(final Address address, final MessageFrame frame) { | |||
| * @return the {@link MutableAccount}, or {@code null} if it does not exist | |||
| */ | |||
| protected MutableAccount getMutableAccount(final Address address, final MessageFrame frame) { | |||
| EvmOperationCounters.incrementAccountReads(); | |||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The use of tracer means we should not change the code in hot path.
| @@ -105,6 +106,9 @@ public OperationResult execute(final MessageFrame frame, final EVM evm) { | |||
| frame.storageWasUpdated(key, newValue); | |||
| frame.getEip7928AccessList().ifPresent(t -> t.addSlotAccessForAccount(address, key)); | |||
|
|
|||
| // Track SSTORE for cross-client execution metrics | |||
| EvmOperationCounters.incrementSstore(); | |||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The use of tracer means we should not change the code in hot path.
| @@ -200,6 +159,9 @@ public void codeSuccess(final MessageFrame frame, final OperationTracer operatio | |||
| final MutableAccount contract = | |||
| frame.getWorldUpdater().getOrCreate(frame.getContractAddress()); | |||
| contract.setCode(contractCode); | |||
| // Track code write for cross-client execution metrics | |||
| EvmOperationCounters.incrementCodeWrites(); | |||
| EvmOperationCounters.addCodeBytesWritten(contractCode.size()); | |||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The use of tracer means we should not change the code in hot path.
| account.setCode(Bytes.concatenate(CODE_DELEGATION_PREFIX, codeDelegationAddress.getBytes())); | ||
| account.setCode(Bytes.concatenate(CODE_DELEGATION_PREFIX, codeDelegationAddress)); | ||
| // Track EIP-7702 delegation set for cross-client execution metrics | ||
| EvmOperationCounters.incrementEip7702DelegationsSet(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The use of tracer means we should not change the code in hot path.
| @@ -73,6 +74,7 @@ default MutableAccount createAccount(final Address address) { | |||
| * #createAccount(Address)} (and thus all his fields will be zero/empty). | |||
| */ | |||
| default MutableAccount getOrCreate(final Address address) { | |||
| EvmOperationCounters.incrementAccountReads(); | |||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The use of tracer means we should not change the code in hot path.
| @@ -105,6 +107,7 @@ default MutableAccount getOrCreateSenderAccount(final Address address) { | |||
| * @return the account {@code address}, or {@code null} if the account does not exist. | |||
| */ | |||
| default MutableAccount getSenderAccount(final MessageFrame frame) { | |||
| EvmOperationCounters.incrementAccountReads(); | |||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The use of tracer means we should not change the code in hot path.
|
As Ameziane said, I think we can use a custom tracer to count opcodes (write, read, etc.).Block processing already supports passing tracers, so we can use the same mechanism. Regarding parallelization: the code already allows passing a custom tracer during parallel execution, which will be different from the block’s tracer. Then, during execution consolidation, if there are no conflicts we can take the tracer from the transaction executed in the background and merge it with the block’s; otherwise, we can re-run the transaction, and the block’s main tracer will be the one that counts. We already have an object to store the result of the transaction executed in parallel (ParallelizedTransactionContext) where we could put the tracing result while waiting for consolidation. |
Summary
Implement standardized JSON format for slow block logging to enable cross-client performance analysis and protocol research.
This change is part of the Cross-Client Execution Metrics initiative proposed by Gary Rong and CPerezz.
Motivation
Standardized execution metrics are critical for:
Real-world example: The EIP-7907 analysis used execution metrics to measure code read latency, per-call overhead scaling, and block execution breakdown. Without standardized metrics across clients, such analysis cannot be validated cross-client.
References
JSON Format
{ "level": "warn", "msg": "Slow block", "block": { "number": ..., "hash": ..., "gas_used": ..., "tx_count": ... }, "timing": { "execution_ms": ..., "total_ms": ... }, "throughput": { "mgas_per_sec": ... }, "state_reads": { "accounts": ..., "storage_slots": ..., "code": ..., "code_bytes": ... }, "state_writes": { "accounts": ..., "storage_slots": ... }, "cache": { "account": { "hits": ..., "misses": ..., "hit_rate": ... }, "storage": { ... }, "code": { ... } }, "evm": { "sload": ..., "sstore": ..., "calls": ..., "creates": ... } }