-
Notifications
You must be signed in to change notification settings - Fork 1k
Add cross-client execution metrics and slow block logging #9834
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
Open
macfarla
wants to merge
34
commits into
hyperledger:main
Choose a base branch
from
macfarla:feature/execution-metrics-standardization-rebased
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+4,928
−81
Open
Changes from all commits
Commits
Show all changes
34 commits
Select commit
Hold shift + click to select a range
219f327
Add cross-client execution metrics and slow block logging
macfarla 7a49bc7
read tracking in state layer
macfarla 2059e04
500ms default for testing
macfarla 1fd91f7
use cli option everywhere
macfarla 5f85026
Merge branch 'main' of github.com:hyperledger/besu into feature/execu…
macfarla d68e23c
truncate decimal values in logs
macfarla 1cc9fee
move persist
macfarla 6cfc70d
state_read_ms
macfarla a01c659
gas used
macfarla b1bdbf9
Merge branch 'main' of github.com:hyperledger/besu into feature/execu…
macfarla db89b64
handle slow tracer background tracer
macfarla 7daa67e
Merge branch 'main' of github.com:hyperledger/besu into feature/execu…
macfarla 04d42b9
Merge branch 'main' of github.com:hyperledger/besu into feature/execu…
macfarla ef037ad
formatting imports
macfarla 4d6988d
test mock fix
macfarla ce5da33
Merge branch 'main' of github.com:hyperledger/besu into feature/execu…
macfarla 003365d
config toml fix
macfarla bb8eebc
fix AT
macfarla 3d88ad5
Merge branch 'main' of github.com:hyperledger/besu into feature/execu…
macfarla 24f967c
fix toml test file
macfarla 70311c0
Merge branch 'main' of github.com:macfarla/besu into feature/executio…
macfarla 5c90a96
remove delegate pattern
macfarla 1bf8a25
Add tracer type to block processing log; generalize EIP-7702 test mes…
macfarla 1ab105f
generalise regex
macfarla 698432f
formatting
macfarla 13458d9
Merge branch 'main' of github.com:hyperledger/besu into feature/execu…
macfarla d54a0f9
Merge branch 'main' of github.com:hyperledger/besu into feature/execu…
macfarla ed2b6ce
Merge branch 'main' of github.com:hyperledger/besu into feature/execu…
macfarla 0d40e84
avoid precompile address range in test
macfarla 6c7116c
review comments: update comments, copyright and stream -> loop in par…
macfarla 43f57a4
simplify cli options to single slow-block-threshold
macfarla e6c9a44
Merge branch 'main' of github.com:hyperledger/besu into feature/execu…
macfarla b9de3b9
rename and copyright
macfarla 4445bd6
Merge branch 'main' of github.com:hyperledger/besu into feature/execu…
macfarla File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
312 changes: 312 additions & 0 deletions
312
...ptanceTest/java/org/hyperledger/besu/tests/acceptance/SlowBlockMetricsValidationTest.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,312 @@ | ||
| /* | ||
| * Copyright contributors to Hyperledger Besu. | ||
| * | ||
| * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with | ||
| * the License. You may obtain a copy of the License at | ||
| * | ||
| * http://www.apache.org/licenses/LICENSE-2.0 | ||
| * | ||
| * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on | ||
| * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the | ||
| * specific language governing permissions and limitations under the License. | ||
| * | ||
| * SPDX-License-Identifier: Apache-2.0 | ||
| */ | ||
| package org.hyperledger.besu.tests.acceptance; | ||
|
|
||
| import static org.assertj.core.api.Assertions.assertThat; | ||
|
|
||
| import org.hyperledger.besu.tests.acceptance.dsl.AcceptanceTestBase; | ||
| import org.hyperledger.besu.tests.acceptance.dsl.account.Account; | ||
| import org.hyperledger.besu.tests.acceptance.dsl.node.BesuNode; | ||
| import org.hyperledger.besu.tests.acceptance.dsl.node.configuration.BesuNodeConfigurationBuilder; | ||
| import org.hyperledger.besu.tests.acceptance.slowblock.model.ExpectedMetrics; | ||
| import org.hyperledger.besu.tests.acceptance.slowblock.model.MetricsTestScenario; | ||
| import org.hyperledger.besu.tests.acceptance.slowblock.model.TaggedBlock; | ||
| import org.hyperledger.besu.tests.acceptance.slowblock.report.SlowBlockMetricsReportGenerator; | ||
| import org.hyperledger.besu.tests.web3j.generated.SimpleStorage; | ||
|
|
||
| import java.io.IOException; | ||
| import java.math.BigInteger; | ||
| import java.nio.file.Path; | ||
| import java.nio.file.Paths; | ||
| import java.util.ArrayList; | ||
| import java.util.LinkedHashSet; | ||
| import java.util.List; | ||
| import java.util.Set; | ||
| import java.util.function.UnaryOperator; | ||
| import java.util.regex.Matcher; | ||
| import java.util.regex.Pattern; | ||
| import java.util.stream.Collectors; | ||
|
|
||
| import com.fasterxml.jackson.databind.JsonNode; | ||
| import com.fasterxml.jackson.databind.ObjectMapper; | ||
| import org.junit.jupiter.api.BeforeEach; | ||
| import org.junit.jupiter.api.Test; | ||
|
|
||
| /** | ||
| * End-to-end acceptance test for validating slow block metrics. This test sends various transaction | ||
| * types to a local Besu node, captures slow block logs, and validates that all JSON fields are | ||
| * correctly populated. | ||
| * | ||
| * <p>The test uses a QBFT node (BFT consensus) with a threshold of 0ms to ensure ALL blocks are | ||
| * logged as slow blocks. QBFT is used instead of dev mode because it automatically produces blocks, | ||
| * which is required for contract deployment and transaction execution. | ||
| * | ||
| * <p><b>Note on EIP-7702:</b> Testing EIP-7702 delegation metrics requires a Prague-enabled genesis | ||
| * and the Engine API for block production. See {@code CodeDelegationTransactionAcceptanceTest} for | ||
| * comprehensive EIP-7702 testing. The EIP-7702 metrics (eip7702_delegations_set/cleared) are | ||
| * validated to be present in the JSON structure but may be 0 in this test as the genesis doesn't | ||
| * enable EIP-7702. | ||
| */ | ||
| public class SlowBlockMetricsValidationTest extends AcceptanceTestBase { | ||
|
|
||
| private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); | ||
|
|
||
| // Pattern to match slow block JSON in console output | ||
| // Matches from the start marker to the final closing braces on the same line, | ||
| // without depending on any specific field order. | ||
| private static final Pattern SLOW_BLOCK_PATTERN = | ||
| Pattern.compile("(\\{\"level\":\"warn\",\"msg\":\"Slow block\"[^\\n]*\\}\\})"); | ||
|
|
||
| // All expected JSON field paths (38 fields - matching geth + Besu extras) | ||
| // Now sourced from ExpectedMetrics for consistency | ||
| private static final List<String> REQUIRED_FIELDS = ExpectedMetrics.ALL_METRIC_PATHS; | ||
|
|
||
| // Report output path | ||
| private static final Path REPORT_OUTPUT_PATH = | ||
| Paths.get("build/reports/slow-block-metrics-analysis.md"); | ||
|
|
||
| private BesuNode devNode; | ||
|
|
||
| @BeforeEach | ||
| public void setUp() throws Exception { | ||
| // Create a QBFT node with: | ||
| // - BFT consensus that automatically produces blocks | ||
| // - 0ms slow block threshold (log ALL blocks as slow) | ||
| // We use a config modifier to add the environment variable for slow block threshold | ||
| final UnaryOperator<BesuNodeConfigurationBuilder> configModifier = | ||
| builder -> builder.extraCLIOptions(List.of("--slow-block-threshold=0")); | ||
|
|
||
| devNode = besu.createQbftNode("qbft-metrics-node", configModifier); | ||
|
|
||
| // Start console capture BEFORE starting the node | ||
| cluster.startConsoleCapture(); | ||
| cluster.start(devNode); | ||
|
|
||
| // Wait for blockchain to progress (QBFT produces blocks automatically) | ||
| cluster.verify(blockchain.reachesHeight(devNode, 1, 30)); | ||
| } | ||
|
|
||
| @Test | ||
| public void shouldCaptureSlowBlockMetricsForVariousTransactions() throws Exception { | ||
| // 1. Send ETH transfer | ||
| final Account sender = accounts.getPrimaryBenefactor(); | ||
| final Account recipient = accounts.createAccount("recipient"); | ||
| devNode.execute(accountTransactions.createTransfer(sender, recipient, 1)); | ||
|
|
||
| // 2. Deploy contract | ||
| final SimpleStorage contract = | ||
| devNode.execute(contractTransactions.createSmartContract(SimpleStorage.class)); | ||
|
|
||
| // 3. Call contract to write storage (SSTORE) | ||
| contract.set(BigInteger.valueOf(42)).send(); | ||
|
|
||
| // 4. Call contract again to read/write (SLOAD + SSTORE) | ||
| contract.set(BigInteger.valueOf(100)).send(); | ||
|
|
||
| // 5. Read contract storage (triggers SLOAD without SSTORE) | ||
| contract.get().send(); | ||
|
|
||
| // Wait a moment for blocks to be processed | ||
| Thread.sleep(2000); | ||
|
|
||
| // Get console output and parse slow block logs | ||
| String consoleOutput = cluster.getConsoleContents(); | ||
| List<JsonNode> slowBlocks = parseSlowBlockLogs(consoleOutput); | ||
|
|
||
| // Assertions | ||
| assertThat(slowBlocks).as("Should capture at least one slow block log").isNotEmpty(); | ||
|
|
||
| // Validate fields in the last slow block | ||
| JsonNode lastBlock = slowBlocks.get(slowBlocks.size() - 1); | ||
|
|
||
| // Check all required fields are present | ||
| List<String> missingFields = new ArrayList<>(); | ||
| for (String fieldPath : REQUIRED_FIELDS) { | ||
| String jsonPointerPath = "/" + fieldPath.replace("/", "/"); | ||
| JsonNode fieldNode = lastBlock.at(jsonPointerPath); | ||
| if (fieldNode.isMissingNode()) { | ||
| missingFields.add(fieldPath); | ||
| } | ||
| } | ||
|
|
||
| assertThat(missingFields) | ||
| .as("All required fields should be present in slow block JSON. Missing: " + missingFields) | ||
| .isEmpty(); | ||
|
|
||
| // Tag blocks with their transaction types and generate comprehensive report | ||
| List<TaggedBlock> taggedBlocks = tagBlocksWithMetricsTestScenarios(slowBlocks); | ||
| generateComprehensiveReport(taggedBlocks); | ||
|
|
||
| // Also print legacy console report for quick verification | ||
| printReport(slowBlocks, lastBlock, missingFields); | ||
| } | ||
|
|
||
| /** | ||
| * Tag each captured slow block with its transaction type based on block metrics. Uses heuristics | ||
| * based on EVM opcode counts, state read/write patterns, and transaction count to classify each | ||
| * block. | ||
| * | ||
| * <p>Classification priority (first match wins): | ||
| * | ||
| * <ol> | ||
| * <li>Genesis block (block number 0) | ||
| * <li>Empty block (no transactions) | ||
| * <li>Contract deployment (CREATE opcode or code writes) | ||
| * <li>Storage write with read (SLOAD + SSTORE with code interaction) | ||
| * <li>Storage write only (SSTORE with code interaction) | ||
| * <li>Storage read only (SLOAD without SSTORE) | ||
| * <li>ETH transfer (transactions without contract code interaction) | ||
| * </ol> | ||
| */ | ||
| private List<TaggedBlock> tagBlocksWithMetricsTestScenarios(final List<JsonNode> slowBlocks) { | ||
| List<TaggedBlock> taggedBlocks = new ArrayList<>(); | ||
|
|
||
| for (int i = 0; i < slowBlocks.size(); i++) { | ||
| JsonNode block = slowBlocks.get(i); | ||
| long blockNumber = block.at("/block/number").asLong(); | ||
| int txCount = block.at("/block/tx_count").asInt(); | ||
| long codeWrites = block.at("/state_writes/code").asLong(); | ||
| long codeReads = block.at("/state_reads/code").asLong(); | ||
| long creates = block.at("/evm/creates").asLong(); | ||
| long sstore = block.at("/evm/sstore").asLong(); | ||
| long sload = block.at("/evm/sload").asLong(); | ||
| long calls = block.at("/evm/calls").asLong(); | ||
|
|
||
| // Determine transaction type based on block metrics | ||
| MetricsTestScenario txType; | ||
| if (blockNumber == 0) { | ||
| // Genesis block | ||
| txType = MetricsTestScenario.GENESIS; | ||
| } else if (txCount == 0) { | ||
| // Empty consensus block (no user transactions) | ||
| txType = MetricsTestScenario.EMPTY_BLOCK; | ||
| } else if (creates > 0 || codeWrites > 0) { | ||
| // Contract deployment: CREATE/CREATE2 opcode executed or code written to state | ||
| txType = MetricsTestScenario.CONTRACT_DEPLOY; | ||
| } else if (sload > 0 && sstore > 0 && codeReads > 0) { | ||
| // Storage read-modify-write: contract reads then writes storage | ||
| txType = MetricsTestScenario.STORAGE_WRITE; | ||
| } else if (sstore > 0 && codeReads > 0) { | ||
| // Storage write only: contract writes to storage slot | ||
| txType = MetricsTestScenario.STORAGE_WRITE; | ||
| } else if (sload > 0 && sstore == 0 && codeReads > 0) { | ||
| // Storage read only: contract reads storage without writing | ||
| txType = MetricsTestScenario.STORAGE_READ; | ||
| } else if (calls > 0 && codeReads > 0) { | ||
| // Contract call without storage access | ||
| txType = MetricsTestScenario.CONTRACT_CALL; | ||
| } else if (txCount > 0) { | ||
| // Simple ETH transfer: has transactions but no contract code interaction | ||
| txType = MetricsTestScenario.ETH_TRANSFER; | ||
| } else { | ||
| // Fallback for unidentified patterns | ||
| txType = MetricsTestScenario.EMPTY_BLOCK; | ||
| } | ||
|
|
||
| taggedBlocks.add(new TaggedBlock(block, txType)); | ||
| } | ||
|
|
||
| return taggedBlocks; | ||
| } | ||
|
|
||
| /** Generate a comprehensive markdown report with expected vs actual analysis for all metrics. */ | ||
| private void generateComprehensiveReport(final List<TaggedBlock> taggedBlocks) | ||
| throws IOException { | ||
| SlowBlockMetricsReportGenerator generator = | ||
| new SlowBlockMetricsReportGenerator(taggedBlocks, "QBFT (BFT Consensus)"); | ||
|
|
||
| // Generate and write the report | ||
| generator.generateReport(REPORT_OUTPUT_PATH); | ||
|
|
||
| // Also print to console for immediate visibility in test output | ||
| System.out.println("\n" + generator.generateReportString()); | ||
| } | ||
|
|
||
| private List<JsonNode> parseSlowBlockLogs(final String consoleOutput) { | ||
| List<JsonNode> allBlocks = new ArrayList<>(); | ||
| Matcher matcher = SLOW_BLOCK_PATTERN.matcher(consoleOutput); | ||
|
|
||
| while (matcher.find()) { | ||
| try { | ||
| String json = matcher.group(); | ||
| JsonNode node = OBJECT_MAPPER.readTree(json); | ||
| allBlocks.add(node); | ||
| } catch (Exception e) { | ||
| // Skip malformed JSON | ||
| } | ||
| } | ||
|
|
||
| // Deduplicate by block hash to avoid duplicate entries | ||
| // (blocks may be logged multiple times during validation/import) | ||
| Set<String> seenHashes = new LinkedHashSet<>(); | ||
| return allBlocks.stream() | ||
| .filter(block -> seenHashes.add(block.at("/block/hash").asText())) | ||
| .collect(Collectors.toList()); | ||
| } | ||
|
|
||
| private void printReport( | ||
| final List<JsonNode> slowBlocks, final JsonNode lastBlock, final List<String> missingFields) { | ||
| StringBuilder report = new StringBuilder(); | ||
|
|
||
| report.append("\n"); | ||
| report.append("═══════════════════════════════════════════════════════════════\n"); | ||
| report.append(" SLOW BLOCK METRICS VALIDATION REPORT\n"); | ||
| report.append("═══════════════════════════════════════════════════════════════\n"); | ||
| report.append("\n"); | ||
|
|
||
| // Summary | ||
| report.append("SUMMARY\n"); | ||
| report.append("-------\n"); | ||
| report.append(String.format("Blocks Processed: %d%n", slowBlocks.size())); | ||
| report.append(String.format("Missing Fields: %d%n", missingFields.size())); | ||
| if (!missingFields.isEmpty()) { | ||
| report.append(String.format(" -> %s%n", missingFields)); | ||
| } | ||
| report.append("\n"); | ||
|
|
||
| // Sample metrics from last block | ||
| if (lastBlock != null) { | ||
| report.append("SAMPLE METRICS (last block)\n"); | ||
| report.append("---------------------------\n"); | ||
| report.append(String.format("Block number: %s%n", lastBlock.at("/block/number").asText())); | ||
| report.append(String.format("Gas used: %s%n", lastBlock.at("/block/gas_used").asText())); | ||
| report.append(String.format("Tx count: %s%n", lastBlock.at("/block/tx_count").asText())); | ||
| report.append( | ||
| String.format("Execution time: %s ms%n", lastBlock.at("/timing/execution_ms").asText())); | ||
| report.append( | ||
| String.format("Account reads: %s%n", lastBlock.at("/state_reads/accounts").asText())); | ||
| report.append( | ||
| String.format("Account writes: %s%n", lastBlock.at("/state_writes/accounts").asText())); | ||
| report.append( | ||
| String.format( | ||
| "Storage reads: %s%n", lastBlock.at("/state_reads/storage_slots").asText())); | ||
| report.append( | ||
| String.format( | ||
| "Storage writes: %s%n", lastBlock.at("/state_writes/storage_slots").asText())); | ||
| report.append(String.format("Code reads: %s%n", lastBlock.at("/state_reads/code").asText())); | ||
| report.append( | ||
| String.format("Code writes: %s%n", lastBlock.at("/state_writes/code").asText())); | ||
| report.append(String.format("SLOAD: %s%n", lastBlock.at("/evm/sload").asText())); | ||
| report.append(String.format("SSTORE: %s%n", lastBlock.at("/evm/sstore").asText())); | ||
| report.append(String.format("CALLS: %s%n", lastBlock.at("/evm/calls").asText())); | ||
| report.append(String.format("CREATES: %s%n", lastBlock.at("/evm/creates").asText())); | ||
| } | ||
|
|
||
| report.append("\n"); | ||
| report.append("═══════════════════════════════════════════════════════════════\n"); | ||
|
|
||
| System.out.println(report); | ||
| } | ||
| } | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
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.