From c317d5f19129cd6846ebe05cbe907a0c1300222c Mon Sep 17 00:00:00 2001
From: zeroXbrock <2791467+zeroXbrock@users.noreply.github.com>
Date: Fri, 13 Feb 2026 16:07:53 -0800
Subject: [PATCH 1/8] cleanup report charts
- better heatmap labels
- less cluttered time-to-inclusion x-axis labels
---
crates/report/src/template.html.handlebars | 21 ++++++++++++++-------
1 file changed, 14 insertions(+), 7 deletions(-)
diff --git a/crates/report/src/template.html.handlebars b/crates/report/src/template.html.handlebars
index 235912e8..6f64d188 100644
--- a/crates/report/src/template.html.handlebars
+++ b/crates/report/src/template.html.handlebars
@@ -337,10 +337,20 @@
.map(slot => `0x${slot.slice(0,4)}...${slot.slice(-4)}`);
const data = {{data.chart_data.heatmap.matrix}};
+ // Pre-compute total writes per slot (y-axis index)
+ const slotTotals = new Array(storageSlots.length).fill(0);
+ data.forEach(function(entry) {
+ slotTotals[entry[1]] += entry[2];
+ });
+
// Specify the configuration items and data for the chart
var option = {
tooltip: {
- position: 'top'
+ position: 'top',
+ formatter: function(params) {
+ const slotIdx = params.value[1];
+ return `${storageSlots[slotIdx]}
Block: ${blocks[params.value[0]]}
Writes: ${params.value[2]}
Total slot writes: ${slotTotals[slotIdx]}`;
+ }
},
grid: {
height: '70%',
@@ -380,10 +390,7 @@
type: 'heatmap',
data: data,
label: {
- show: true,
- formatter: function (params) {
- return params.value[2] > 0 ? params.value[2] : '-';
- }
+ show: false
},
emphasis: {
itemStyle: {
@@ -466,9 +473,9 @@
data: buckets,
nameGap: 2,
axisLabel: {
- interval: 0,
+ interval: buckets.length > 20 ? Math.floor(buckets.length / 20) - 1 : 0,
rotate: 45
- }
+ },
},
yAxis: {
type: 'value',
From f88f5c53eaaae7ba22fc3dabc15510b4c23b5243 Mon Sep 17 00:00:00 2001
From: zeroXbrock <2791467+zeroXbrock@users.noreply.github.com>
Date: Fri, 13 Feb 2026 16:09:25 -0800
Subject: [PATCH 2/8] bugfix: unchecked error in reports (if txs are not found)
---
crates/report/src/block_trace.rs | 7 +++----
1 file changed, 3 insertions(+), 4 deletions(-)
diff --git a/crates/report/src/block_trace.rs b/crates/report/src/block_trace.rs
index 4d302929..a1754490 100644
--- a/crates/report/src/block_trace.rs
+++ b/crates/report/src/block_trace.rs
@@ -139,12 +139,11 @@ pub async fn get_block_traces(
) -> Result> {
// get tx traces for all txs in all_blocks
let mut all_traces = vec![];
- if full_blocks.is_empty() {
+ let total_txs: usize = full_blocks.iter().map(|b| b.transactions.len()).sum();
+ if full_blocks.is_empty() || total_txs == 0 {
return Ok(all_traces);
}
- let (sender, mut receiver) = tokio::sync::mpsc::channel::(
- full_blocks.iter().map(|b| b.transactions.len()).sum(),
- );
+ let (sender, mut receiver) = tokio::sync::mpsc::channel::(total_txs);
for block in full_blocks {
let mut tx_tasks = vec![];
From b644442b06e2a73e20e6957e3ca406b3cd41e6c5 Mon Sep 17 00:00:00 2001
From: zeroXbrock <2791467+zeroXbrock@users.noreply.github.com>
Date: Fri, 13 Feb 2026 16:15:57 -0800
Subject: [PATCH 3/8] bugfix: handle bad params in fill_block to prevent panic
---
crates/cli/src/default_scenarios/fill_block.rs | 11 ++++++++++-
1 file changed, 10 insertions(+), 1 deletion(-)
diff --git a/crates/cli/src/default_scenarios/fill_block.rs b/crates/cli/src/default_scenarios/fill_block.rs
index b5173e52..8620ef6d 100644
--- a/crates/cli/src/default_scenarios/fill_block.rs
+++ b/crates/cli/src/default_scenarios/fill_block.rs
@@ -51,7 +51,16 @@ pub async fn fill_block(
block_gas_limit.unwrap_or(30_000_000)
};
- let num_txs = txs_per_block.unwrap_or(txs_per_second.unwrap_or_default());
+ let num_txs = match (txs_per_block, txs_per_second) {
+ (Some(0), _) | (_, Some(0)) => {
+ return Err(CliError::Args(crate::commands::error::ArgsError::SpamRateNotFound));
+ }
+ (Some(n), _) => n,
+ (_, Some(n)) => n,
+ (None, None) => {
+ return Err(CliError::Args(crate::commands::error::ArgsError::SpamRateNotFound));
+ }
+ };
let gas_per_tx = gas_limit / num_txs;
info!("Attempting to fill blocks with {gas_limit} gas; sending {num_txs} txs, each with gas limit {gas_per_tx}.");
From 44cbbd013b803e7b686e3cf89652a29b5733e491 Mon Sep 17 00:00:00 2001
From: zeroXbrock <2791467+zeroXbrock@users.noreply.github.com>
Date: Fri, 13 Feb 2026 17:14:13 -0800
Subject: [PATCH 4/8] collect metrics for batched eth_sendRawTransaction calls
---
crates/core/src/test_scenario.rs | 13 ++++++++++++-
1 file changed, 12 insertions(+), 1 deletion(-)
diff --git a/crates/core/src/test_scenario.rs b/crates/core/src/test_scenario.rs
index 56fcd735..0eaa2f80 100644
--- a/crates/core/src/test_scenario.rs
+++ b/crates/core/src/test_scenario.rs
@@ -1123,6 +1123,7 @@ where
})
.collect();
+ let hist = self.prometheus.hist.get();
tasks.push(tokio::task::spawn(async move {
// Build json-rpc batch payload with multiple eth_sendRawTransaction requests
let mut requests = Vec::with_capacity(signed_chunk.len());
@@ -1138,9 +1139,19 @@ where
}));
}
+ // === PROMETHEUS LATENCY METRICS ===
+ let mut timer = hist.as_ref().map(|h| {
+ h.with_label_values(&["eth_sendRawTransaction"])
+ .start_timer()
+ });
+
let resp = match http_client.post(rpc_url).json(&requests).send().await {
- Ok(r) => r,
+ Ok(r) => {
+ timer.take().map(|t| t.observe_duration());
+ r
+ }
Err(e) => {
+ timer.take().map(|t| t.observe_duration());
warn!("failed to send JSON-RPC batch: {e:?}");
return;
}
From 4677423697ebb1a037c00ef1fea6bb3be703867e Mon Sep 17 00:00:00 2001
From: zeroXbrock <2791467+zeroXbrock@users.noreply.github.com>
Date: Fri, 13 Feb 2026 17:25:04 -0800
Subject: [PATCH 5/8] chore: fmt
---
crates/cli/src/default_scenarios/fill_block.rs | 8 ++++++--
1 file changed, 6 insertions(+), 2 deletions(-)
diff --git a/crates/cli/src/default_scenarios/fill_block.rs b/crates/cli/src/default_scenarios/fill_block.rs
index 8620ef6d..0713d0f4 100644
--- a/crates/cli/src/default_scenarios/fill_block.rs
+++ b/crates/cli/src/default_scenarios/fill_block.rs
@@ -53,12 +53,16 @@ pub async fn fill_block(
let num_txs = match (txs_per_block, txs_per_second) {
(Some(0), _) | (_, Some(0)) => {
- return Err(CliError::Args(crate::commands::error::ArgsError::SpamRateNotFound));
+ return Err(CliError::Args(
+ crate::commands::error::ArgsError::SpamRateNotFound,
+ ));
}
(Some(n), _) => n,
(_, Some(n)) => n,
(None, None) => {
- return Err(CliError::Args(crate::commands::error::ArgsError::SpamRateNotFound));
+ return Err(CliError::Args(
+ crate::commands::error::ArgsError::SpamRateNotFound,
+ ));
}
};
let gas_per_tx = gas_limit / num_txs;
From 35e4127bfd2146989bbf9f425e4daa1c5d2a4362 Mon Sep 17 00:00:00 2001
From: zeroXbrock <2791467+zeroXbrock@users.noreply.github.com>
Date: Fri, 13 Feb 2026 17:25:45 -0800
Subject: [PATCH 6/8] chore: clippy
---
crates/core/src/test_scenario.rs | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/crates/core/src/test_scenario.rs b/crates/core/src/test_scenario.rs
index 0eaa2f80..fe5ab2de 100644
--- a/crates/core/src/test_scenario.rs
+++ b/crates/core/src/test_scenario.rs
@@ -1147,11 +1147,11 @@ where
let resp = match http_client.post(rpc_url).json(&requests).send().await {
Ok(r) => {
- timer.take().map(|t| t.observe_duration());
+ if let Some(t) = timer.take() { t.observe_duration() }
r
}
Err(e) => {
- timer.take().map(|t| t.observe_duration());
+ if let Some(t) = timer.take() { t.observe_duration() }
warn!("failed to send JSON-RPC batch: {e:?}");
return;
}
From eef4e934ee05043e061ca249816091b895f78c38 Mon Sep 17 00:00:00 2001
From: zeroXbrock <2791467+zeroXbrock@users.noreply.github.com>
Date: Fri, 13 Feb 2026 19:22:28 -0800
Subject: [PATCH 7/8] chore: fmt
---
crates/core/src/test_scenario.rs | 8 ++++++--
1 file changed, 6 insertions(+), 2 deletions(-)
diff --git a/crates/core/src/test_scenario.rs b/crates/core/src/test_scenario.rs
index fe5ab2de..538f12c8 100644
--- a/crates/core/src/test_scenario.rs
+++ b/crates/core/src/test_scenario.rs
@@ -1147,11 +1147,15 @@ where
let resp = match http_client.post(rpc_url).json(&requests).send().await {
Ok(r) => {
- if let Some(t) = timer.take() { t.observe_duration() }
+ if let Some(t) = timer.take() {
+ t.observe_duration()
+ }
r
}
Err(e) => {
- if let Some(t) = timer.take() { t.observe_duration() }
+ if let Some(t) = timer.take() {
+ t.observe_duration()
+ }
warn!("failed to send JSON-RPC batch: {e:?}");
return;
}
From c4a110dc62c42b57433dc2b301e26a712a52a4b8 Mon Sep 17 00:00:00 2001
From: zeroXbrock <2791467+zeroXbrock@users.noreply.github.com>
Date: Tue, 17 Feb 2026 14:11:03 -0800
Subject: [PATCH 8/8] chore: update changelogs
---
CHANGELOG.md | 1 +
crates/report/CHANGELOG.md | 1 +
2 files changed, 2 insertions(+)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 671b8312..9fc88c0e 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- added contender version to bottom of reports ([#452](https://github.com/flashbots/contender/pull/452/changes))
- enable custom data dir at runtime ([453](https://github.com/flashbots/contender/pull/453/changes))
+- clean up html report UI, support batched `eth_sendRawTransaction` latency metrics ([#455](https://github.com/flashbots/contender/pull/455))
## [0.8.1](https://github.com/flashbots/contender/releases/tag/v0.8.1) - 2026-02-09
diff --git a/crates/report/CHANGELOG.md b/crates/report/CHANGELOG.md
index 7b1cbaeb..9892e20c 100644
--- a/crates/report/CHANGELOG.md
+++ b/crates/report/CHANGELOG.md
@@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- added contender version to bottom of reports ([#452](https://github.com/flashbots/contender/pull/452/changes))
- use `std::path::Path` instead of `str` where applicable ([453](https://github.com/flashbots/contender/pull/453/changes))
+- clean up html report UI, support batched `eth_sendRawTransaction` latency metrics ([#455](https://github.com/flashbots/contender/pull/455/changes))
## [0.8.1](https://github.com/flashbots/contender/releases/tag/v0.8.1) - 2026-02-09