mirror of
https://github.com/paradigmxyz/reth.git
synced 2026-04-30 03:01:58 -04:00
Compare commits
27 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
aab08b82ea | ||
|
|
6d357545ef | ||
|
|
2740793e42 | ||
|
|
0142769191 | ||
|
|
e1dc93e24f | ||
|
|
33ac869a85 | ||
|
|
ec982f8686 | ||
|
|
47cef33a0d | ||
|
|
9529de4cf2 | ||
|
|
5a9dd02301 | ||
|
|
d71a0c0c7b | ||
|
|
2be3788481 | ||
|
|
adbec3218d | ||
|
|
2e5560b444 | ||
|
|
1f3fd5da2e | ||
|
|
3ab7cb98aa | ||
|
|
d3088e171c | ||
|
|
2c443a3dcb | ||
|
|
4b444069a5 | ||
|
|
25d371817a | ||
|
|
4b0fa8a330 | ||
|
|
df22d38224 | ||
|
|
e4ec836a46 | ||
|
|
d3c42fc718 | ||
|
|
8171cee927 | ||
|
|
61cfcd8195 | ||
|
|
b646f4559c |
5
.changelog/fast-seals-play.md
Normal file
5
.changelog/fast-seals-play.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
reth-transaction-pool: minor
|
||||
---
|
||||
|
||||
Added `consensus_ref` method to `PoolTransaction` trait for borrowing consensus transactions without cloning.
|
||||
6
.changelog/merry-koalas-nod.md
Normal file
6
.changelog/merry-koalas-nod.md
Normal file
@@ -0,0 +1,6 @@
|
||||
---
|
||||
reth-rpc-eth-api: minor
|
||||
reth-rpc-server-types: minor
|
||||
---
|
||||
|
||||
Added `eth_getStorageValues` RPC method for batch storage slot retrieval across multiple addresses.
|
||||
5
.changelog/proud-wolves-spin.md
Normal file
5
.changelog/proud-wolves-spin.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
reth-storage-api: patch
|
||||
---
|
||||
|
||||
Added `Arc` to `auto_impl` derive for storage-api traits to support automatic `Arc` wrapper implementations.
|
||||
2
.github/workflows/lint.yml
vendored
2
.github/workflows/lint.yml
vendored
@@ -124,7 +124,7 @@ jobs:
|
||||
- uses: rui314/setup-mold@v1
|
||||
- uses: dtolnay/rust-toolchain@master
|
||||
with:
|
||||
toolchain: "1.88" # MSRV
|
||||
toolchain: "1.93" # MSRV
|
||||
- uses: mozilla-actions/sccache-action@v0.0.9
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
|
||||
458
Cargo.lock
generated
458
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -1,7 +1,7 @@
|
||||
[workspace.package]
|
||||
version = "1.11.0"
|
||||
edition = "2024"
|
||||
rust-version = "1.88"
|
||||
rust-version = "1.93"
|
||||
license = "MIT OR Apache-2.0"
|
||||
homepage = "https://paradigmxyz.github.io/reth"
|
||||
repository = "https://github.com/paradigmxyz/reth"
|
||||
@@ -27,7 +27,6 @@ members = [
|
||||
"crates/engine/invalid-block-hooks/",
|
||||
"crates/engine/local",
|
||||
"crates/engine/primitives/",
|
||||
"crates/engine/service",
|
||||
"crates/engine/tree/",
|
||||
"crates/engine/util/",
|
||||
"crates/era",
|
||||
@@ -110,7 +109,6 @@ members = [
|
||||
"crates/storage/errors/",
|
||||
"crates/storage/libmdbx-rs/",
|
||||
"crates/storage/libmdbx-rs/mdbx-sys/",
|
||||
"crates/storage/mdbx-viz/",
|
||||
"crates/storage/nippy-jar/",
|
||||
"crates/storage/provider/",
|
||||
"crates/storage/storage-api/",
|
||||
@@ -350,7 +348,6 @@ reth-ecies = { path = "crates/net/ecies" }
|
||||
reth-engine-local = { path = "crates/engine/local" }
|
||||
reth-engine-primitives = { path = "crates/engine/primitives", default-features = false }
|
||||
reth-engine-tree = { path = "crates/engine/tree" }
|
||||
reth-engine-service = { path = "crates/engine/service" }
|
||||
reth-engine-util = { path = "crates/engine/util" }
|
||||
reth-era = { path = "crates/era" }
|
||||
reth-era-downloader = { path = "crates/era-downloader" }
|
||||
@@ -378,7 +375,6 @@ reth-invalid-block-hooks = { path = "crates/engine/invalid-block-hooks" }
|
||||
reth-ipc = { path = "crates/rpc/ipc" }
|
||||
reth-libmdbx = { path = "crates/storage/libmdbx-rs" }
|
||||
reth-mdbx-sys = { path = "crates/storage/libmdbx-rs/mdbx-sys" }
|
||||
reth-mdbx-viz = { path = "crates/storage/mdbx-viz" }
|
||||
reth-metrics = { path = "crates/metrics" }
|
||||
reth-net-banlist = { path = "crates/net/banlist" }
|
||||
reth-net-nat = { path = "crates/net/nat" }
|
||||
@@ -667,6 +663,7 @@ cipher = "0.4.3"
|
||||
comfy-table = "7.0"
|
||||
concat-kdf = "0.1.0"
|
||||
crossbeam-channel = "0.5.13"
|
||||
crossbeam-utils = "0.8"
|
||||
crossterm = "0.29.0"
|
||||
csv = "1.3.0"
|
||||
ctrlc = "3.4"
|
||||
|
||||
@@ -19,10 +19,11 @@ pre-build = [
|
||||
image = "ubuntu:24.04"
|
||||
pre-build = [
|
||||
"apt update",
|
||||
"apt install --yes gcc gcc-riscv64-linux-gnu libclang-dev make",
|
||||
"apt install --yes gcc gcc-riscv64-linux-gnu g++-riscv64-linux-gnu libclang-dev make",
|
||||
]
|
||||
env.passthrough = [
|
||||
"CARGO_TARGET_RISCV64GC_UNKNOWN_LINUX_GNU_LINKER=riscv64-linux-gnu-gcc",
|
||||
"CXX_riscv64gc_unknown_linux_gnu=riscv64-linux-gnu-g++",
|
||||
]
|
||||
|
||||
[build.env]
|
||||
|
||||
@@ -93,7 +93,7 @@ When updating this, also update:
|
||||
- .github/workflows/lint.yml
|
||||
-->
|
||||
|
||||
The Minimum Supported Rust Version (MSRV) of this project is [1.88.0](https://blog.rust-lang.org/2025/06/26/Rust-1.88.0/).
|
||||
The Minimum Supported Rust Version (MSRV) of this project is [1.93.0](https://blog.rust-lang.org/2026/01/22/Rust-1.93.0/).
|
||||
|
||||
See the docs for detailed instructions on how to [build from source](https://reth.rs/installation/source/).
|
||||
|
||||
|
||||
@@ -190,7 +190,6 @@ min-trace-logs = [
|
||||
"reth-node-core/min-trace-logs",
|
||||
]
|
||||
|
||||
pageviz = ["reth-db/pageviz", "reth-node-builder/pageviz"]
|
||||
rocksdb = ["reth-ethereum-cli/rocksdb", "reth-node-core/rocksdb"]
|
||||
edge = ["rocksdb"]
|
||||
|
||||
|
||||
@@ -87,8 +87,6 @@ tokio-stream.workspace = true
|
||||
reqwest.workspace = true
|
||||
url.workspace = true
|
||||
metrics.workspace = true
|
||||
memmap2.workspace = true
|
||||
rayon.workspace = true
|
||||
|
||||
# io
|
||||
fdlimit.workspace = true
|
||||
|
||||
@@ -1,564 +0,0 @@
|
||||
use clap::Parser;
|
||||
use eyre::Context;
|
||||
use memmap2::Mmap;
|
||||
use rayon::prelude::*;
|
||||
use reth_db_api::tables::Tables;
|
||||
use reth_db_common::DbTool;
|
||||
use reth_node_builder::NodeTypesWithDB;
|
||||
use reth_node_core::dirs::{ChainPath, DataDirPath};
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
fs,
|
||||
io::Write,
|
||||
path::PathBuf,
|
||||
time::Instant,
|
||||
};
|
||||
|
||||
const PAGEHDRSZ: usize = 20;
|
||||
const NODESIZE: usize = 8;
|
||||
const P_BRANCH: u16 = 0x01;
|
||||
const P_LEAF: u16 = 0x02;
|
||||
const P_LARGE: u16 = 0x04;
|
||||
const P_META: u16 = 0x08;
|
||||
const P_DUPFIX: u16 = 0x20;
|
||||
const N_BIG: u8 = 0x01;
|
||||
const N_TREE: u8 = 0x02;
|
||||
const N_DUP: u8 = 0x04;
|
||||
const P_INVALID: u32 = 0xFFFFFFFF;
|
||||
const UNREFERENCED: u8 = 0xFF;
|
||||
const DBI_META: u8 = 0xFE;
|
||||
|
||||
const META_GEO: usize = 0x14;
|
||||
const META_FREE_DB: usize = 0x28;
|
||||
const META_MAIN_DB: usize = 0x58;
|
||||
const META_TXNID_A: usize = 0x08;
|
||||
const META_TXNID_B: usize = 0xB0;
|
||||
const GEO_NOW: usize = 0x0C;
|
||||
const TREE_ROOT: usize = 0x08;
|
||||
const TREE_BRANCH_PAGES: usize = 0x0C;
|
||||
const TREE_LEAF_PAGES: usize = 0x10;
|
||||
const TREE_LARGE_PAGES: usize = 0x14;
|
||||
const TREE_ITEMS: usize = 0x20;
|
||||
const MDBX_MAGIC: u64 = 0x59659DBDEF4C11;
|
||||
|
||||
fn u16_le(buf: &[u8], off: usize) -> u16 {
|
||||
u16::from_le_bytes([buf[off], buf[off + 1]])
|
||||
}
|
||||
|
||||
fn u32_le(buf: &[u8], off: usize) -> u32 {
|
||||
u32::from_le_bytes([buf[off], buf[off + 1], buf[off + 2], buf[off + 3]])
|
||||
}
|
||||
|
||||
fn u64_le(buf: &[u8], off: usize) -> u64 {
|
||||
u64::from_le_bytes(buf[off..off + 8].try_into().unwrap())
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct TreeDescriptor {
|
||||
root: u32,
|
||||
branch_pages: u32,
|
||||
leaf_pages: u32,
|
||||
large_pages: u32,
|
||||
items: u64,
|
||||
}
|
||||
|
||||
impl TreeDescriptor {
|
||||
fn parse(buf: &[u8], off: usize) -> Self {
|
||||
Self {
|
||||
root: u32_le(buf, off + TREE_ROOT),
|
||||
branch_pages: u32_le(buf, off + TREE_BRANCH_PAGES),
|
||||
leaf_pages: u32_le(buf, off + TREE_LEAF_PAGES),
|
||||
large_pages: u32_le(buf, off + TREE_LARGE_PAGES),
|
||||
items: u64_le(buf, off + TREE_ITEMS),
|
||||
}
|
||||
}
|
||||
|
||||
fn total_pages(&self) -> u64 {
|
||||
self.branch_pages as u64 + self.leaf_pages as u64 + self.large_pages as u64
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct DBIInfo {
|
||||
name: String,
|
||||
dbi_index: u8,
|
||||
tree: TreeDescriptor,
|
||||
}
|
||||
|
||||
fn page_flags(buf: &[u8], page_off: usize) -> u16 {
|
||||
u16_le(buf, page_off + 0x0A)
|
||||
}
|
||||
|
||||
fn page_nkeys(buf: &[u8], page_off: usize) -> usize {
|
||||
u16_le(buf, page_off + 0x0C) as usize / 2
|
||||
}
|
||||
|
||||
fn page_overflow_count(buf: &[u8], page_off: usize) -> u32 {
|
||||
u32_le(buf, page_off + 0x0C)
|
||||
}
|
||||
|
||||
fn mark(owner_map: &mut [u8], pgno: usize, dbi_index: u8, conflicts: &mut u64) -> bool {
|
||||
if pgno >= owner_map.len() {
|
||||
return false;
|
||||
}
|
||||
let prev = owner_map[pgno];
|
||||
if prev == UNREFERENCED {
|
||||
owner_map[pgno] = dbi_index;
|
||||
return true;
|
||||
}
|
||||
if prev != dbi_index {
|
||||
*conflicts += 1;
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
|
||||
fn walk_tree_collect(
|
||||
buf: &[u8],
|
||||
ps: usize,
|
||||
root_pgno: u32,
|
||||
page_count: usize,
|
||||
dbi_index: u8,
|
||||
) -> Vec<(usize, u8)> {
|
||||
if root_pgno == P_INVALID || root_pgno as usize >= page_count {
|
||||
return Vec::new();
|
||||
}
|
||||
|
||||
let mut result: Vec<(usize, u8)> = Vec::new();
|
||||
let mut visited = vec![false; page_count];
|
||||
let mut stack: Vec<u32> = vec![root_pgno];
|
||||
|
||||
while let Some(pgno) = stack.pop() {
|
||||
let pgno_usize = pgno as usize;
|
||||
if pgno_usize >= page_count || visited[pgno_usize] {
|
||||
continue;
|
||||
}
|
||||
visited[pgno_usize] = true;
|
||||
result.push((pgno_usize, dbi_index));
|
||||
|
||||
let page_off = pgno_usize * ps;
|
||||
if page_off + PAGEHDRSZ > buf.len() {
|
||||
continue;
|
||||
}
|
||||
let flags = page_flags(buf, page_off);
|
||||
|
||||
if flags & P_BRANCH != 0 {
|
||||
let nkeys = page_nkeys(buf, page_off);
|
||||
for i in 0..nkeys {
|
||||
let idx_off = page_off + PAGEHDRSZ + i * 2;
|
||||
if idx_off + 2 > buf.len() { break; }
|
||||
let node_rel = u16_le(buf, idx_off) as usize;
|
||||
let node_abs = page_off + PAGEHDRSZ + node_rel;
|
||||
if node_abs + 4 > buf.len() { break; }
|
||||
stack.push(u32_le(buf, node_abs));
|
||||
}
|
||||
} else if flags & P_LEAF != 0 && flags & P_DUPFIX == 0 {
|
||||
let nkeys = page_nkeys(buf, page_off);
|
||||
for i in 0..nkeys {
|
||||
let idx_off = page_off + PAGEHDRSZ + i * 2;
|
||||
if idx_off + 2 > buf.len() { break; }
|
||||
let node_rel = u16_le(buf, idx_off) as usize;
|
||||
let node_abs = page_off + PAGEHDRSZ + node_rel;
|
||||
if node_abs + NODESIZE > buf.len() { break; }
|
||||
|
||||
let mn_flags = buf[node_abs + 4];
|
||||
let ksize = u16_le(buf, node_abs + 6) as usize;
|
||||
let data_off = node_abs + NODESIZE + ksize;
|
||||
|
||||
if mn_flags & N_BIG != 0 {
|
||||
if data_off + 4 > buf.len() { continue; }
|
||||
let ov_pgno = u32_le(buf, data_off) as usize;
|
||||
if ov_pgno >= page_count { continue; }
|
||||
let ov_page_off = ov_pgno * ps;
|
||||
if ov_page_off + PAGEHDRSZ > buf.len() { continue; }
|
||||
let ov_count = page_overflow_count(buf, ov_page_off) as usize;
|
||||
for op in ov_pgno..ov_pgno + ov_count {
|
||||
if op < page_count && !visited[op] {
|
||||
visited[op] = true;
|
||||
result.push((op, dbi_index));
|
||||
}
|
||||
}
|
||||
} else if (mn_flags & (N_DUP | N_TREE)) == (N_DUP | N_TREE) {
|
||||
if data_off + 48 > buf.len() { continue; }
|
||||
let sub = TreeDescriptor::parse(buf, data_off);
|
||||
if sub.root != P_INVALID {
|
||||
stack.push(sub.root);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if flags & P_LARGE != 0 {
|
||||
let ov_count = page_overflow_count(buf, page_off) as usize;
|
||||
for op in (pgno_usize + 1)..(pgno_usize + ov_count) {
|
||||
if op < page_count && !visited[op] {
|
||||
visited[op] = true;
|
||||
result.push((op, dbi_index));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
fn discover_named_dbis(
|
||||
buf: &[u8],
|
||||
ps: usize,
|
||||
main_root: u32,
|
||||
owner_map: &mut [u8],
|
||||
conflicts: &mut u64,
|
||||
dbi_main: u8,
|
||||
dbi_start: u8,
|
||||
) -> Vec<DBIInfo> {
|
||||
if main_root == P_INVALID || main_root as usize >= owner_map.len() {
|
||||
return Vec::new();
|
||||
}
|
||||
|
||||
let mut named: Vec<DBIInfo> = Vec::new();
|
||||
let mut next_index = dbi_start;
|
||||
let mut stack: Vec<u32> = vec![main_root];
|
||||
|
||||
while let Some(pgno) = stack.pop() {
|
||||
let pgno_usize = pgno as usize;
|
||||
if pgno_usize >= owner_map.len() { continue; }
|
||||
if !mark(owner_map, pgno_usize, dbi_main, conflicts) { continue; }
|
||||
|
||||
let page_off = pgno_usize * ps;
|
||||
if page_off + PAGEHDRSZ > buf.len() { continue; }
|
||||
let flags = page_flags(buf, page_off);
|
||||
|
||||
if flags & P_BRANCH != 0 {
|
||||
let nkeys = page_nkeys(buf, page_off);
|
||||
for i in 0..nkeys {
|
||||
let idx_off = page_off + PAGEHDRSZ + i * 2;
|
||||
if idx_off + 2 > buf.len() { break; }
|
||||
let node_rel = u16_le(buf, idx_off) as usize;
|
||||
let node_abs = page_off + PAGEHDRSZ + node_rel;
|
||||
if node_abs + 4 > buf.len() { break; }
|
||||
let child_pgno = u32_le(buf, node_abs);
|
||||
stack.push(child_pgno);
|
||||
}
|
||||
} else if flags & P_LEAF != 0 && flags & P_DUPFIX == 0 {
|
||||
let nkeys = page_nkeys(buf, page_off);
|
||||
for i in 0..nkeys {
|
||||
let idx_off = page_off + PAGEHDRSZ + i * 2;
|
||||
if idx_off + 2 > buf.len() { break; }
|
||||
let node_rel = u16_le(buf, idx_off) as usize;
|
||||
let node_abs = page_off + PAGEHDRSZ + node_rel;
|
||||
if node_abs + NODESIZE > buf.len() { break; }
|
||||
|
||||
let mn_flags = buf[node_abs + 4];
|
||||
let ksize = u16_le(buf, node_abs + 6) as usize;
|
||||
let data_off = node_abs + NODESIZE + ksize;
|
||||
|
||||
if mn_flags & N_TREE != 0 {
|
||||
let key_off = node_abs + NODESIZE;
|
||||
if key_off + ksize > buf.len() || data_off + 48 > buf.len() { continue; }
|
||||
let name_bytes = &buf[key_off..key_off + ksize];
|
||||
let name = String::from_utf8_lossy(
|
||||
name_bytes.split(|&b| b == 0).next().unwrap_or(name_bytes)
|
||||
).to_string();
|
||||
let tree = TreeDescriptor::parse(buf, data_off);
|
||||
named.push(DBIInfo { name, dbi_index: next_index, tree });
|
||||
next_index = next_index.saturating_add(1);
|
||||
} else if mn_flags & N_BIG != 0 {
|
||||
if data_off + 4 > buf.len() { continue; }
|
||||
let ov_pgno = u32_le(buf, data_off) as usize;
|
||||
if ov_pgno >= owner_map.len() { continue; }
|
||||
let ov_page_off = ov_pgno * ps;
|
||||
if ov_page_off + PAGEHDRSZ > buf.len() { continue; }
|
||||
let ov_count = page_overflow_count(buf, ov_page_off) as usize;
|
||||
for op in ov_pgno..ov_pgno + ov_count {
|
||||
mark(owner_map, op, dbi_main, conflicts);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
named
|
||||
}
|
||||
|
||||
fn mark_free_pages(
|
||||
buf: &[u8],
|
||||
ps: usize,
|
||||
free_root: u32,
|
||||
owner_map: &mut [u8],
|
||||
conflicts: &mut u64,
|
||||
dbi_free: u8,
|
||||
) -> u64 {
|
||||
if free_root == P_INVALID || free_root as usize >= owner_map.len() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
let mut marked: u64 = 0;
|
||||
let mut stack: Vec<u32> = vec![free_root];
|
||||
|
||||
while let Some(pgno) = stack.pop() {
|
||||
let pgno_usize = pgno as usize;
|
||||
if pgno_usize >= owner_map.len() { continue; }
|
||||
if !mark(owner_map, pgno_usize, dbi_free, conflicts) { continue; }
|
||||
marked += 1;
|
||||
|
||||
let page_off = pgno_usize * ps;
|
||||
if page_off + PAGEHDRSZ > buf.len() { continue; }
|
||||
let flags = page_flags(buf, page_off);
|
||||
|
||||
if flags & P_BRANCH != 0 {
|
||||
let nkeys = page_nkeys(buf, page_off);
|
||||
for i in 0..nkeys {
|
||||
let idx_off = page_off + PAGEHDRSZ + i * 2;
|
||||
if idx_off + 2 > buf.len() { break; }
|
||||
let node_rel = u16_le(buf, idx_off) as usize;
|
||||
let child_pgno = u32_le(buf, page_off + PAGEHDRSZ + node_rel);
|
||||
stack.push(child_pgno);
|
||||
}
|
||||
} else if flags & P_LEAF != 0 && flags & P_DUPFIX == 0 {
|
||||
let nkeys = page_nkeys(buf, page_off);
|
||||
for i in 0..nkeys {
|
||||
let idx_off = page_off + PAGEHDRSZ + i * 2;
|
||||
if idx_off + 2 > buf.len() { break; }
|
||||
let node_rel = u16_le(buf, idx_off) as usize;
|
||||
let node_abs = page_off + PAGEHDRSZ + node_rel;
|
||||
if node_abs + NODESIZE > buf.len() { break; }
|
||||
|
||||
let mn_flags = buf[node_abs + 4];
|
||||
let ksize = u16_le(buf, node_abs + 6) as usize;
|
||||
let dsize = u32_le(buf, node_abs) as usize;
|
||||
let data_off = node_abs + NODESIZE + ksize;
|
||||
|
||||
if mn_flags & N_BIG != 0 {
|
||||
if data_off + 4 > buf.len() { continue; }
|
||||
let ov_pgno = u32_le(buf, data_off) as usize;
|
||||
if ov_pgno >= owner_map.len() { continue; }
|
||||
let ov_page_off = ov_pgno * ps;
|
||||
if ov_page_off + PAGEHDRSZ > buf.len() { continue; }
|
||||
let ov_count = page_overflow_count(buf, ov_page_off) as usize;
|
||||
for op in ov_pgno..ov_pgno + ov_count {
|
||||
if mark(owner_map, op, dbi_free, conflicts) { marked += 1; }
|
||||
}
|
||||
let ov_data_off = ov_pgno * ps + PAGEHDRSZ;
|
||||
if dsize >= 4 && ov_data_off + 4 <= buf.len() {
|
||||
let pnl_count = u32_le(buf, ov_data_off) as usize;
|
||||
for j in 0..pnl_count {
|
||||
let fp_off = ov_data_off + 4 + j * 4;
|
||||
if fp_off + 4 > buf.len() { break; }
|
||||
let fp = u32_le(buf, fp_off) as usize;
|
||||
if fp < owner_map.len() && owner_map[fp] == UNREFERENCED {
|
||||
owner_map[fp] = dbi_free;
|
||||
marked += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if dsize >= 4 && data_off + 4 <= buf.len() {
|
||||
let pnl_count = u32_le(buf, data_off) as usize;
|
||||
for j in 0..pnl_count {
|
||||
let fp_off = data_off + 4 + j * 4;
|
||||
if fp_off + 4 > buf.len() { break; }
|
||||
let fp = u32_le(buf, fp_off) as usize;
|
||||
if fp < owner_map.len() && owner_map[fp] == UNREFERENCED {
|
||||
owner_map[fp] = dbi_free;
|
||||
marked += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
marked
|
||||
}
|
||||
|
||||
#[derive(Parser, Debug)]
|
||||
pub struct Command {
|
||||
#[arg(short, long, default_value = "owner_map.bin")]
|
||||
output: PathBuf,
|
||||
}
|
||||
|
||||
impl Command {
|
||||
pub fn execute<N: NodeTypesWithDB>(
|
||||
self,
|
||||
data_dir: ChainPath<DataDirPath>,
|
||||
_tool: &DbTool<N>,
|
||||
) -> eyre::Result<()> {
|
||||
let db_path = data_dir.db().join("mdbx.dat");
|
||||
eyre::ensure!(db_path.exists(), "mdbx.dat not found at {:?}", db_path);
|
||||
|
||||
let t0 = Instant::now();
|
||||
|
||||
let file = fs::File::open(&db_path)
|
||||
.wrap_err_with(|| format!("Failed to open {}", db_path.display()))?;
|
||||
let mmap = unsafe { Mmap::map(&file)? };
|
||||
let buf: &[u8] = &mmap;
|
||||
|
||||
if buf.len() < PAGEHDRSZ + 0xC0 {
|
||||
eyre::bail!("File too small for MDBX meta page");
|
||||
}
|
||||
let flags0 = u16_le(buf, 0x0A);
|
||||
if flags0 & P_META == 0 {
|
||||
eyre::bail!("Page 0 missing P_META flag");
|
||||
}
|
||||
let magic = u64_le(buf, PAGEHDRSZ);
|
||||
if (magic >> 8) != MDBX_MAGIC {
|
||||
eyre::bail!("MDBX magic mismatch");
|
||||
}
|
||||
|
||||
let candidates = [4096usize, 8192, 16384, 32768, 65536];
|
||||
let mut ps = 4096usize;
|
||||
let geo_now_raw = u32_le(buf, PAGEHDRSZ + META_GEO + GEO_NOW) as usize;
|
||||
for &candidate in &candidates {
|
||||
let mapped = geo_now_raw * candidate;
|
||||
if mapped >= buf.len() / 2 && mapped <= buf.len() * 4 {
|
||||
ps = candidate;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
let mut best_txnid: u64 = 0;
|
||||
let mut best_meta_base: usize = 0;
|
||||
for pgno in 0..3usize {
|
||||
let page_base = pgno * ps;
|
||||
let meta_base = page_base + PAGEHDRSZ;
|
||||
if meta_base + 0xC0 > buf.len() { continue; }
|
||||
let pflags = u16_le(buf, page_base + 0x0A);
|
||||
if pflags & P_META == 0 { continue; }
|
||||
let m = u64_le(buf, meta_base);
|
||||
if (m >> 8) != MDBX_MAGIC { continue; }
|
||||
let txnid_a = u64_le(buf, meta_base + META_TXNID_A);
|
||||
let txnid_b = u64_le(buf, meta_base + META_TXNID_B);
|
||||
if txnid_a == txnid_b && txnid_a > best_txnid {
|
||||
best_txnid = txnid_a;
|
||||
best_meta_base = meta_base;
|
||||
}
|
||||
}
|
||||
eyre::ensure!(best_txnid > 0, "No valid meta page found");
|
||||
|
||||
let page_count = u32_le(buf, best_meta_base + META_GEO + GEO_NOW) as usize;
|
||||
let free_tree = TreeDescriptor::parse(buf, best_meta_base + META_FREE_DB);
|
||||
let main_tree = TreeDescriptor::parse(buf, best_meta_base + META_MAIN_DB);
|
||||
|
||||
println!("MDBX owner map generator (parallel)");
|
||||
println!(" page_size: {ps}");
|
||||
println!(" page_count: {page_count}");
|
||||
println!(" txnid: {best_txnid}");
|
||||
println!(" FreeDB root: {} ({} items)", free_tree.root, free_tree.items);
|
||||
println!(" MainDB root: {} ({} items)", main_tree.root, main_tree.items);
|
||||
println!();
|
||||
|
||||
let mut owner_map = vec![UNREFERENCED; page_count];
|
||||
let mut conflicts: u64 = 0;
|
||||
|
||||
for pgno in 0..std::cmp::min(3, page_count) {
|
||||
owner_map[pgno] = DBI_META;
|
||||
}
|
||||
|
||||
println!("Discovering named DBIs via MainDB walk...");
|
||||
let discovered = discover_named_dbis(
|
||||
buf, ps, main_tree.root, &mut owner_map, &mut conflicts, 1, 2,
|
||||
);
|
||||
|
||||
let mut name_to_dbi: HashMap<&str, u8> = HashMap::new();
|
||||
for (idx, table) in Tables::ALL.iter().enumerate() {
|
||||
name_to_dbi.insert(table.name(), (idx + 2) as u8);
|
||||
}
|
||||
|
||||
let mut named_dbis: Vec<DBIInfo> = Vec::new();
|
||||
let mut remap: HashMap<u8, u8> = HashMap::new();
|
||||
for dbi in &discovered {
|
||||
if let Some(&real_dbi) = name_to_dbi.get(dbi.name.as_str()) {
|
||||
remap.insert(dbi.dbi_index, real_dbi);
|
||||
named_dbis.push(DBIInfo {
|
||||
name: dbi.name.clone(),
|
||||
dbi_index: real_dbi,
|
||||
tree: dbi.tree.clone(),
|
||||
});
|
||||
} else {
|
||||
named_dbis.push(dbi.clone());
|
||||
}
|
||||
}
|
||||
|
||||
for byte in owner_map.iter_mut() {
|
||||
if let Some(&new_dbi) = remap.get(byte) {
|
||||
*byte = new_dbi;
|
||||
}
|
||||
}
|
||||
|
||||
println!("Found {} named DBI(s)", named_dbis.len());
|
||||
|
||||
println!("Walking FreeDB / GC...");
|
||||
let free_marked = mark_free_pages(buf, ps, free_tree.root, &mut owner_map, &mut conflicts, 0);
|
||||
println!("FreeDB: {} pages marked", free_marked);
|
||||
|
||||
println!("Walking {} named DBIs in parallel...", named_dbis.len());
|
||||
let t_par = Instant::now();
|
||||
|
||||
let results: Vec<(String, u8, Vec<(usize, u8)>)> = named_dbis
|
||||
.par_iter()
|
||||
.filter(|dbi| dbi.tree.root != P_INVALID)
|
||||
.map(|dbi| {
|
||||
let pages = walk_tree_collect(buf, ps, dbi.tree.root, page_count, dbi.dbi_index);
|
||||
(dbi.name.clone(), dbi.dbi_index, pages)
|
||||
})
|
||||
.collect();
|
||||
|
||||
for (name, dbi_index, pages) in &results {
|
||||
let count = pages.len();
|
||||
for &(pgno, idx) in pages {
|
||||
mark(&mut owner_map, pgno, idx, &mut conflicts);
|
||||
}
|
||||
println!(" [{:2}] {:30} {:>10} pages", dbi_index, name, count);
|
||||
}
|
||||
|
||||
println!("Parallel walk done in {:.2}s", t_par.elapsed().as_secs_f64());
|
||||
|
||||
let elapsed = t0.elapsed();
|
||||
|
||||
let mut counts: HashMap<u8, u64> = HashMap::new();
|
||||
let mut unreferenced: u64 = 0;
|
||||
for &b in &owner_map {
|
||||
if b == UNREFERENCED {
|
||||
unreferenced += 1;
|
||||
} else {
|
||||
*counts.entry(b).or_default() += 1;
|
||||
}
|
||||
}
|
||||
|
||||
println!();
|
||||
println!("Walk complete in {:.2}s", elapsed.as_secs_f64());
|
||||
println!(" Total pages: {page_count}");
|
||||
println!(" Unreferenced: {unreferenced}");
|
||||
println!(" Conflicts: {conflicts}");
|
||||
println!();
|
||||
|
||||
if let Some(&c) = counts.get(&DBI_META) {
|
||||
println!(" [0xFE] {:30} {:>10} pages", "<meta>", c);
|
||||
}
|
||||
if let Some(&c) = counts.get(&0) {
|
||||
println!(" [ 0] {:30} {:>10} pages", "<free/GC>", c);
|
||||
}
|
||||
if let Some(&c) = counts.get(&1) {
|
||||
println!(" [ 1] {:30} {:>10} pages", "<main>", c);
|
||||
}
|
||||
let mut sorted_dbis = named_dbis.clone();
|
||||
sorted_dbis.sort_by_key(|d| d.dbi_index);
|
||||
for dbi in &sorted_dbis {
|
||||
let walked = counts.get(&dbi.dbi_index).copied().unwrap_or(0);
|
||||
let expected = dbi.tree.total_pages();
|
||||
let mismatch = if expected > 0 && walked != expected {
|
||||
format!(" !! MISMATCH (tree_t says {})", expected)
|
||||
} else {
|
||||
String::new()
|
||||
};
|
||||
println!(" [{:3}] {:30} {:>10} pages{}", dbi.dbi_index, dbi.name, walked, mismatch);
|
||||
}
|
||||
println!(" [0xFF] {:30} {:>10} pages", "<unreferenced>", unreferenced);
|
||||
|
||||
let mut out = fs::File::create(&self.output)?;
|
||||
out.write_all(&owner_map)?;
|
||||
println!();
|
||||
println!("Written {} bytes to {}", owner_map.len(), self.output.display());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -21,7 +21,6 @@ mod settings;
|
||||
mod state;
|
||||
mod static_file_header;
|
||||
mod stats;
|
||||
pub mod gen_ownermap;
|
||||
/// DB List TUI
|
||||
mod tui;
|
||||
|
||||
@@ -72,8 +71,6 @@ pub enum Subcommands {
|
||||
AccountStorage(account_storage::Command),
|
||||
/// Gets account state and storage at a specific block
|
||||
State(state::Command),
|
||||
/// Generate a page owner map for MDBX page visualization
|
||||
GenOwnermap(gen_ownermap::Command),
|
||||
}
|
||||
|
||||
impl<C: ChainSpecParser<ChainSpec: EthChainSpec + EthereumHardforks>> Command<C> {
|
||||
@@ -217,11 +214,6 @@ impl<C: ChainSpecParser<ChainSpec: EthChainSpec + EthereumHardforks>> Command<C>
|
||||
command.execute(&tool)?;
|
||||
});
|
||||
}
|
||||
Subcommands::GenOwnermap(command) => {
|
||||
db_exec!(self.env, tool, N, AccessRights::RO, {
|
||||
command.execute(data_dir, &tool)?;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
||||
@@ -285,7 +285,6 @@ fn verify_and_repair<N: ProviderNodeTypes>(tool: &DbTool<N>) -> eyre::Result<()>
|
||||
// (We can't just use `upsert` method with a dup cursor, it's not properly
|
||||
// supported)
|
||||
let nibbles = StoredNibblesSubKey(path);
|
||||
let entry = StorageTrieEntry { nibbles: nibbles.clone(), node };
|
||||
if storage_trie_cursor
|
||||
.seek_by_key_subkey(account, nibbles.clone())?
|
||||
.filter(|v| v.nibbles == nibbles)
|
||||
@@ -293,6 +292,7 @@ fn verify_and_repair<N: ProviderNodeTypes>(tool: &DbTool<N>) -> eyre::Result<()>
|
||||
{
|
||||
storage_trie_cursor.delete_current()?;
|
||||
}
|
||||
let entry = StorageTrieEntry { nibbles, node };
|
||||
storage_trie_cursor.upsert(account, &entry)?;
|
||||
}
|
||||
Output::Progress(path) => {
|
||||
|
||||
@@ -1,47 +0,0 @@
|
||||
[package]
|
||||
name = "reth-engine-service"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
rust-version.workspace = true
|
||||
license.workspace = true
|
||||
homepage.workspace = true
|
||||
repository.workspace = true
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[dependencies]
|
||||
# reth
|
||||
reth-consensus.workspace = true
|
||||
reth-engine-tree.workspace = true
|
||||
reth-evm.workspace = true
|
||||
reth-network-p2p.workspace = true
|
||||
reth-payload-builder.workspace = true
|
||||
reth-provider.workspace = true
|
||||
reth-prune.workspace = true
|
||||
reth-stages-api.workspace = true
|
||||
reth-tasks.workspace = true
|
||||
reth-node-types.workspace = true
|
||||
reth-chainspec.workspace = true
|
||||
reth-engine-primitives.workspace = true
|
||||
reth-trie-db.workspace = true
|
||||
|
||||
# async
|
||||
futures.workspace = true
|
||||
pin-project.workspace = true
|
||||
|
||||
# misc
|
||||
|
||||
[dev-dependencies]
|
||||
reth-engine-tree = { workspace = true, features = ["test-utils"] }
|
||||
reth-ethereum-consensus.workspace = true
|
||||
reth-ethereum-engine-primitives.workspace = true
|
||||
reth-evm-ethereum.workspace = true
|
||||
reth-exex-types.workspace = true
|
||||
reth-primitives-traits.workspace = true
|
||||
reth-node-ethereum.workspace = true
|
||||
reth-trie-db.workspace = true
|
||||
|
||||
alloy-eips.workspace = true
|
||||
tokio = { workspace = true, features = ["sync"] }
|
||||
tokio-stream.workspace = true
|
||||
@@ -1,12 +0,0 @@
|
||||
//! Engine service implementation.
|
||||
|
||||
#![doc(
|
||||
html_logo_url = "https://raw.githubusercontent.com/paradigmxyz/reth/main/assets/reth-docs.png",
|
||||
html_favicon_url = "https://avatars0.githubusercontent.com/u/97369466?s=256",
|
||||
issue_tracker_base_url = "https://github.com/paradigmxyz/reth/issues/"
|
||||
)]
|
||||
#![cfg_attr(docsrs, feature(doc_cfg))]
|
||||
#![cfg_attr(not(test), warn(unused_crate_dependencies))]
|
||||
|
||||
/// Engine Service
|
||||
pub mod service;
|
||||
@@ -1,229 +0,0 @@
|
||||
use futures::{Stream, StreamExt};
|
||||
use pin_project::pin_project;
|
||||
use reth_chainspec::EthChainSpec;
|
||||
use reth_consensus::FullConsensus;
|
||||
use reth_engine_primitives::{BeaconEngineMessage, ConsensusEngineEvent};
|
||||
use reth_engine_tree::{
|
||||
backfill::PipelineSync,
|
||||
download::BasicBlockDownloader,
|
||||
engine::{EngineApiKind, EngineApiRequest, EngineApiRequestHandler, EngineHandler},
|
||||
persistence::PersistenceHandle,
|
||||
tree::{EngineApiTreeHandler, EngineValidator, TreeConfig},
|
||||
};
|
||||
pub use reth_engine_tree::{
|
||||
chain::{ChainEvent, ChainOrchestrator},
|
||||
engine::EngineApiEvent,
|
||||
};
|
||||
use reth_evm::ConfigureEvm;
|
||||
use reth_network_p2p::BlockClient;
|
||||
use reth_node_types::{BlockTy, NodeTypes};
|
||||
use reth_payload_builder::PayloadBuilderHandle;
|
||||
use reth_provider::{
|
||||
providers::{BlockchainProvider, ProviderNodeTypes},
|
||||
ProviderFactory, StorageSettingsCache,
|
||||
};
|
||||
use reth_prune::PrunerWithFactory;
|
||||
use reth_stages_api::{MetricEventsSender, Pipeline};
|
||||
use reth_tasks::TaskSpawner;
|
||||
use reth_trie_db::ChangesetCache;
|
||||
use std::{
|
||||
pin::Pin,
|
||||
sync::Arc,
|
||||
task::{Context, Poll},
|
||||
};
|
||||
|
||||
/// Alias for consensus engine stream.
|
||||
pub type EngineMessageStream<T> = Pin<Box<dyn Stream<Item = BeaconEngineMessage<T>> + Send + Sync>>;
|
||||
|
||||
/// Alias for chain orchestrator.
|
||||
type EngineServiceType<N, Client> = ChainOrchestrator<
|
||||
EngineHandler<
|
||||
EngineApiRequestHandler<
|
||||
EngineApiRequest<<N as NodeTypes>::Payload, <N as NodeTypes>::Primitives>,
|
||||
<N as NodeTypes>::Primitives,
|
||||
>,
|
||||
EngineMessageStream<<N as NodeTypes>::Payload>,
|
||||
BasicBlockDownloader<Client, BlockTy<N>>,
|
||||
>,
|
||||
PipelineSync<N>,
|
||||
>;
|
||||
|
||||
/// The type that drives the chain forward and communicates progress.
|
||||
#[pin_project]
|
||||
#[expect(missing_debug_implementations)]
|
||||
// TODO(mattsse): remove hidden once fixed : <https://github.com/rust-lang/rust/issues/135363>
|
||||
// otherwise rustdoc fails to resolve the alias
|
||||
#[doc(hidden)]
|
||||
pub struct EngineService<N, Client>
|
||||
where
|
||||
N: ProviderNodeTypes,
|
||||
Client: BlockClient<Block = BlockTy<N>> + 'static,
|
||||
{
|
||||
orchestrator: EngineServiceType<N, Client>,
|
||||
}
|
||||
|
||||
impl<N, Client> EngineService<N, Client>
|
||||
where
|
||||
N: ProviderNodeTypes,
|
||||
Client: BlockClient<Block = BlockTy<N>> + 'static,
|
||||
{
|
||||
/// Constructor for `EngineService`.
|
||||
#[expect(clippy::too_many_arguments)]
|
||||
pub fn new<V, C>(
|
||||
consensus: Arc<dyn FullConsensus<N::Primitives>>,
|
||||
chain_spec: Arc<N::ChainSpec>,
|
||||
client: Client,
|
||||
incoming_requests: EngineMessageStream<N::Payload>,
|
||||
pipeline: Pipeline<N>,
|
||||
pipeline_task_spawner: Box<dyn TaskSpawner>,
|
||||
provider: ProviderFactory<N>,
|
||||
blockchain_db: BlockchainProvider<N>,
|
||||
pruner: PrunerWithFactory<ProviderFactory<N>>,
|
||||
payload_builder: PayloadBuilderHandle<N::Payload>,
|
||||
payload_validator: V,
|
||||
tree_config: TreeConfig,
|
||||
sync_metrics_tx: MetricEventsSender,
|
||||
evm_config: C,
|
||||
changeset_cache: ChangesetCache,
|
||||
) -> Self
|
||||
where
|
||||
V: EngineValidator<N::Payload>,
|
||||
C: ConfigureEvm<Primitives = N::Primitives> + 'static,
|
||||
{
|
||||
let engine_kind =
|
||||
if chain_spec.is_optimism() { EngineApiKind::OpStack } else { EngineApiKind::Ethereum };
|
||||
|
||||
let downloader = BasicBlockDownloader::new(client, consensus.clone());
|
||||
let use_hashed_state = provider.cached_storage_settings().use_hashed_state();
|
||||
|
||||
let persistence_handle =
|
||||
PersistenceHandle::<N::Primitives>::spawn_service(provider, pruner, sync_metrics_tx);
|
||||
|
||||
let canonical_in_memory_state = blockchain_db.canonical_in_memory_state();
|
||||
|
||||
let (to_tree_tx, from_tree) = EngineApiTreeHandler::spawn_new(
|
||||
blockchain_db,
|
||||
consensus,
|
||||
payload_validator,
|
||||
persistence_handle,
|
||||
payload_builder,
|
||||
canonical_in_memory_state,
|
||||
tree_config,
|
||||
engine_kind,
|
||||
evm_config,
|
||||
changeset_cache,
|
||||
use_hashed_state,
|
||||
);
|
||||
|
||||
let engine_handler = EngineApiRequestHandler::new(to_tree_tx, from_tree);
|
||||
let handler = EngineHandler::new(engine_handler, downloader, incoming_requests);
|
||||
|
||||
let backfill_sync = PipelineSync::new(pipeline, pipeline_task_spawner);
|
||||
|
||||
Self { orchestrator: ChainOrchestrator::new(handler, backfill_sync) }
|
||||
}
|
||||
|
||||
/// Returns a mutable reference to the orchestrator.
|
||||
pub fn orchestrator_mut(&mut self) -> &mut EngineServiceType<N, Client> {
|
||||
&mut self.orchestrator
|
||||
}
|
||||
}
|
||||
|
||||
impl<N, Client> Stream for EngineService<N, Client>
|
||||
where
|
||||
N: ProviderNodeTypes,
|
||||
Client: BlockClient<Block = BlockTy<N>> + 'static,
|
||||
{
|
||||
type Item = ChainEvent<ConsensusEngineEvent<N::Primitives>>;
|
||||
|
||||
fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
|
||||
let mut orchestrator = self.project().orchestrator;
|
||||
StreamExt::poll_next_unpin(&mut orchestrator, cx)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use reth_chainspec::{ChainSpecBuilder, MAINNET};
|
||||
use reth_engine_primitives::{BeaconEngineMessage, NoopInvalidBlockHook};
|
||||
use reth_engine_tree::{test_utils::TestPipelineBuilder, tree::BasicEngineValidator};
|
||||
use reth_ethereum_consensus::EthBeaconConsensus;
|
||||
use reth_ethereum_engine_primitives::EthEngineTypes;
|
||||
use reth_evm_ethereum::EthEvmConfig;
|
||||
use reth_exex_types::FinishedExExHeight;
|
||||
use reth_network_p2p::test_utils::TestFullBlockClient;
|
||||
use reth_node_ethereum::EthereumEngineValidator;
|
||||
use reth_primitives_traits::SealedHeader;
|
||||
use reth_provider::{
|
||||
providers::BlockchainProvider, test_utils::create_test_provider_factory_with_chain_spec,
|
||||
};
|
||||
use reth_prune::Pruner;
|
||||
use reth_tasks::TokioTaskExecutor;
|
||||
use reth_trie_db::ChangesetCache;
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::{mpsc::unbounded_channel, watch};
|
||||
use tokio_stream::wrappers::UnboundedReceiverStream;
|
||||
|
||||
#[test]
|
||||
fn eth_chain_orchestrator_build() {
|
||||
let chain_spec = Arc::new(
|
||||
ChainSpecBuilder::default()
|
||||
.chain(MAINNET.chain)
|
||||
.genesis(MAINNET.genesis.clone())
|
||||
.paris_activated()
|
||||
.build(),
|
||||
);
|
||||
let consensus = Arc::new(EthBeaconConsensus::new(chain_spec.clone()));
|
||||
|
||||
let client = TestFullBlockClient::default();
|
||||
|
||||
let (_tx, rx) = unbounded_channel::<BeaconEngineMessage<EthEngineTypes>>();
|
||||
let incoming_requests = UnboundedReceiverStream::new(rx);
|
||||
|
||||
let pipeline = TestPipelineBuilder::new().build(chain_spec.clone());
|
||||
let pipeline_task_spawner = Box::<TokioTaskExecutor>::default();
|
||||
let provider_factory = create_test_provider_factory_with_chain_spec(chain_spec.clone());
|
||||
|
||||
let blockchain_db =
|
||||
BlockchainProvider::with_latest(provider_factory.clone(), SealedHeader::default())
|
||||
.unwrap();
|
||||
let engine_payload_validator = EthereumEngineValidator::new(chain_spec.clone());
|
||||
let (_tx, rx) = watch::channel(FinishedExExHeight::NoExExs);
|
||||
let pruner = Pruner::new_with_factory(provider_factory.clone(), vec![], 0, 0, None, rx);
|
||||
let evm_config = EthEvmConfig::new(chain_spec.clone());
|
||||
|
||||
let changeset_cache = ChangesetCache::new();
|
||||
|
||||
let engine_validator = BasicEngineValidator::new(
|
||||
blockchain_db.clone(),
|
||||
consensus.clone(),
|
||||
evm_config.clone(),
|
||||
engine_payload_validator,
|
||||
TreeConfig::default(),
|
||||
Box::new(NoopInvalidBlockHook::default()),
|
||||
changeset_cache.clone(),
|
||||
reth_tasks::Runtime::test(),
|
||||
);
|
||||
|
||||
let (sync_metrics_tx, _sync_metrics_rx) = unbounded_channel();
|
||||
let (tx, _rx) = unbounded_channel();
|
||||
let _eth_service = EngineService::new(
|
||||
consensus,
|
||||
chain_spec,
|
||||
client,
|
||||
Box::pin(incoming_requests),
|
||||
pipeline,
|
||||
pipeline_task_spawner,
|
||||
provider_factory,
|
||||
blockchain_db,
|
||||
pruner,
|
||||
PayloadBuilderHandle::new(tx),
|
||||
engine_validator,
|
||||
TreeConfig::default(),
|
||||
sync_metrics_tx,
|
||||
evm_config,
|
||||
changeset_cache,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -29,7 +29,7 @@ reth-provider.workspace = true
|
||||
reth-prune.workspace = true
|
||||
reth-revm = { workspace = true, features = ["optional-balance-check"] }
|
||||
reth-stages-api.workspace = true
|
||||
reth-tasks.workspace = true
|
||||
reth-tasks = { workspace = true, features = ["rayon"] }
|
||||
reth-trie-parallel.workspace = true
|
||||
reth-trie-sparse = { workspace = true, features = ["std", "metrics"] }
|
||||
reth-trie.workspace = true
|
||||
@@ -73,7 +73,6 @@ reth-prune-types = { workspace = true, optional = true }
|
||||
reth-stages = { workspace = true, optional = true }
|
||||
reth-static-file = { workspace = true, optional = true }
|
||||
reth-tracing = { workspace = true, optional = true }
|
||||
reth-mdbx-viz = { workspace = true, optional = true }
|
||||
|
||||
[dev-dependencies]
|
||||
# reth
|
||||
@@ -151,7 +150,6 @@ rocksdb = [
|
||||
"reth-e2e-test-utils/rocksdb",
|
||||
]
|
||||
edge = ["rocksdb"]
|
||||
pageviz = ["dep:reth-mdbx-viz"]
|
||||
|
||||
[[test]]
|
||||
name = "e2e_testsuite"
|
||||
|
||||
110
crates/engine/tree/src/launch.rs
Normal file
110
crates/engine/tree/src/launch.rs
Normal file
@@ -0,0 +1,110 @@
|
||||
//! Engine orchestrator launch helper.
|
||||
//!
|
||||
//! Provides [`build_engine_orchestrator`](crate::launch::build_engine_orchestrator) which wires
|
||||
//! together all engine components and returns a
|
||||
//! [`ChainOrchestrator`](crate::chain::ChainOrchestrator) ready to be polled as a `Stream`.
|
||||
|
||||
use crate::{
|
||||
backfill::PipelineSync,
|
||||
chain::ChainOrchestrator,
|
||||
download::BasicBlockDownloader,
|
||||
engine::{EngineApiKind, EngineApiRequest, EngineApiRequestHandler, EngineHandler},
|
||||
persistence::PersistenceHandle,
|
||||
tree::{EngineApiTreeHandler, EngineValidator, TreeConfig},
|
||||
};
|
||||
use futures::Stream;
|
||||
use reth_consensus::FullConsensus;
|
||||
use reth_engine_primitives::BeaconEngineMessage;
|
||||
use reth_evm::ConfigureEvm;
|
||||
use reth_network_p2p::BlockClient;
|
||||
use reth_payload_builder::PayloadBuilderHandle;
|
||||
use reth_primitives_traits::NodePrimitives;
|
||||
use reth_provider::{
|
||||
providers::{BlockchainProvider, ProviderNodeTypes},
|
||||
ProviderFactory, StorageSettingsCache,
|
||||
};
|
||||
use reth_prune::PrunerWithFactory;
|
||||
use reth_stages_api::{MetricEventsSender, Pipeline};
|
||||
use reth_tasks::TaskSpawner;
|
||||
use reth_trie_db::ChangesetCache;
|
||||
use std::sync::Arc;
|
||||
|
||||
/// Builds the engine [`ChainOrchestrator`] that drives the chain forward.
|
||||
///
|
||||
/// This spawns and wires together the following components:
|
||||
///
|
||||
/// - **[`BasicBlockDownloader`]** — downloads blocks on demand from the network during live sync.
|
||||
/// - **[`PersistenceHandle`]** — spawns the persistence service on a background thread for writing
|
||||
/// blocks and performing pruning outside the critical consensus path.
|
||||
/// - **[`EngineApiTreeHandler`]** — spawns the tree handler that processes engine API requests
|
||||
/// (`newPayload`, `forkchoiceUpdated`) and maintains the in-memory chain state.
|
||||
/// - **[`EngineApiRequestHandler`]** + **[`EngineHandler`]** — glue that routes incoming CL
|
||||
/// messages to the tree handler and manages download requests.
|
||||
/// - **[`PipelineSync`]** — wraps the staged sync [`Pipeline`] for backfill sync when the node
|
||||
/// needs to catch up over large block ranges.
|
||||
///
|
||||
/// The returned orchestrator implements [`Stream`] and yields
|
||||
/// [`ChainEvent`]s.
|
||||
///
|
||||
/// [`ChainEvent`]: crate::chain::ChainEvent
|
||||
#[expect(clippy::too_many_arguments, clippy::type_complexity)]
|
||||
pub fn build_engine_orchestrator<N, Client, S, V, C>(
|
||||
engine_kind: EngineApiKind,
|
||||
consensus: Arc<dyn FullConsensus<N::Primitives>>,
|
||||
client: Client,
|
||||
incoming_requests: S,
|
||||
pipeline: Pipeline<N>,
|
||||
pipeline_task_spawner: Box<dyn TaskSpawner>,
|
||||
provider: ProviderFactory<N>,
|
||||
blockchain_db: BlockchainProvider<N>,
|
||||
pruner: PrunerWithFactory<ProviderFactory<N>>,
|
||||
payload_builder: PayloadBuilderHandle<N::Payload>,
|
||||
payload_validator: V,
|
||||
tree_config: TreeConfig,
|
||||
sync_metrics_tx: MetricEventsSender,
|
||||
evm_config: C,
|
||||
changeset_cache: ChangesetCache,
|
||||
) -> ChainOrchestrator<
|
||||
EngineHandler<
|
||||
EngineApiRequestHandler<EngineApiRequest<N::Payload, N::Primitives>, N::Primitives>,
|
||||
S,
|
||||
BasicBlockDownloader<Client, <N::Primitives as NodePrimitives>::Block>,
|
||||
>,
|
||||
PipelineSync<N>,
|
||||
>
|
||||
where
|
||||
N: ProviderNodeTypes,
|
||||
Client: BlockClient<Block = <N::Primitives as NodePrimitives>::Block> + 'static,
|
||||
S: Stream<Item = BeaconEngineMessage<N::Payload>> + Send + Sync + Unpin + 'static,
|
||||
V: EngineValidator<N::Payload>,
|
||||
C: ConfigureEvm<Primitives = N::Primitives> + 'static,
|
||||
{
|
||||
let downloader = BasicBlockDownloader::new(client, consensus.clone());
|
||||
let use_hashed_state = provider.cached_storage_settings().use_hashed_state();
|
||||
|
||||
let persistence_handle =
|
||||
PersistenceHandle::<N::Primitives>::spawn_service(provider, pruner, sync_metrics_tx);
|
||||
|
||||
let canonical_in_memory_state = blockchain_db.canonical_in_memory_state();
|
||||
|
||||
let (to_tree_tx, from_tree) = EngineApiTreeHandler::spawn_new(
|
||||
blockchain_db,
|
||||
consensus,
|
||||
payload_validator,
|
||||
persistence_handle,
|
||||
payload_builder,
|
||||
canonical_in_memory_state,
|
||||
tree_config,
|
||||
engine_kind,
|
||||
evm_config,
|
||||
changeset_cache,
|
||||
use_hashed_state,
|
||||
);
|
||||
|
||||
let engine_handler = EngineApiRequestHandler::new(to_tree_tx, from_tree);
|
||||
let handler = EngineHandler::new(engine_handler, downloader, incoming_requests);
|
||||
|
||||
let backfill_sync = PipelineSync::new(pipeline, pipeline_task_spawner);
|
||||
|
||||
ChainOrchestrator::new(handler, backfill_sync)
|
||||
}
|
||||
@@ -100,6 +100,8 @@ pub mod chain;
|
||||
pub mod download;
|
||||
/// Engine Api chain handler support.
|
||||
pub mod engine;
|
||||
/// Engine orchestrator launch helper.
|
||||
pub mod launch;
|
||||
/// Metrics support.
|
||||
pub mod metrics;
|
||||
/// The background writer service, coordinating write operations on static files and the database.
|
||||
|
||||
@@ -119,7 +119,7 @@ where
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[instrument(level = "debug", target = "engine::persistence", skip_all, fields(new_tip_num))]
|
||||
#[instrument(level = "debug", target = "engine::persistence", skip_all, fields(%new_tip_num))]
|
||||
fn on_remove_blocks_above(
|
||||
&self,
|
||||
new_tip_num: u64,
|
||||
|
||||
@@ -2602,7 +2602,7 @@ where
|
||||
/// Returns `InsertPayloadOk::Inserted(BlockStatus::Valid)` on successful execution,
|
||||
/// `InsertPayloadOk::AlreadySeen` if the block already exists, or
|
||||
/// `InsertPayloadOk::Inserted(BlockStatus::Disconnected)` if parent state is missing.
|
||||
#[instrument(level = "debug", target = "engine::tree", skip_all, fields(block_id))]
|
||||
#[instrument(level = "debug", target = "engine::tree", skip_all, fields(?block_id))]
|
||||
fn insert_block_or_payload<Input, Err>(
|
||||
&mut self,
|
||||
block_id: BlockWithParent,
|
||||
@@ -2677,23 +2677,8 @@ where
|
||||
|
||||
let start = Instant::now();
|
||||
|
||||
#[cfg(feature = "pageviz")]
|
||||
reth_mdbx_viz::pageviz_emit_block_marker(block_num_hash.number, true, 0, 0, 0);
|
||||
|
||||
#[cfg(feature = "pageviz")]
|
||||
let pageviz_start = std::time::Instant::now();
|
||||
|
||||
let executed = execute(&mut self.payload_validator, input, ctx)?;
|
||||
|
||||
#[cfg(feature = "pageviz")]
|
||||
reth_mdbx_viz::pageviz_emit_block_marker(
|
||||
block_num_hash.number,
|
||||
false,
|
||||
executed.recovered_block().senders().len() as u16,
|
||||
pageviz_start.elapsed().as_millis() as u32,
|
||||
executed.recovered_block().gas_used(),
|
||||
);
|
||||
|
||||
// if the parent is the canonical head, we can insert the block as the pending block
|
||||
if self.state.tree_state.canonical_block_hash() == executed.recovered_block().parent_hash()
|
||||
{
|
||||
|
||||
@@ -33,7 +33,7 @@ use reth_provider::{
|
||||
StateProviderFactory, StateReader,
|
||||
};
|
||||
use reth_revm::{db::BundleState, state::EvmState};
|
||||
use reth_tasks::Runtime;
|
||||
use reth_tasks::{ForEachOrdered, Runtime};
|
||||
use reth_trie::{hashed_cursor::HashedCursorFactory, trie_cursor::TrieCursorFactory};
|
||||
use reth_trie_parallel::{
|
||||
proof_task::{ProofTaskCtx, ProofWorkerHandle},
|
||||
@@ -43,7 +43,6 @@ use reth_trie_sparse::{
|
||||
ParallelSparseTrie, ParallelismThresholds, RevealableSparseTrie, SparseStateTrie,
|
||||
};
|
||||
use std::{
|
||||
collections::BTreeMap,
|
||||
ops::Not,
|
||||
sync::{
|
||||
atomic::AtomicBool,
|
||||
@@ -97,6 +96,7 @@ pub const SPARSE_TRIE_MAX_VALUES_SHRINK_CAPACITY: usize = 1_000_000;
|
||||
/// Blocks with fewer transactions than this skip prewarming, since the fixed overhead of spawning
|
||||
/// prewarm workers exceeds the execution time saved.
|
||||
pub const SMALL_BLOCK_TX_THRESHOLD: usize = 5;
|
||||
|
||||
/// Type alias for [`PayloadHandle`] returned by payload processor spawn methods.
|
||||
type IteratorPayloadHandle<Evm, I, N> = PayloadHandle<
|
||||
WithTxEnv<TxEnvFor<Evm>, <I as ExecutableTxIterator<Evm>>::Recovered>,
|
||||
@@ -248,42 +248,23 @@ where
|
||||
let (to_sparse_trie, sparse_trie_rx) = channel();
|
||||
let (to_multi_proof, from_multi_proof) = crossbeam_channel::unbounded();
|
||||
|
||||
// Extract V2 proofs flag early so we can pass it to prewarm
|
||||
let v2_proofs_enabled = !config.disable_proof_v2();
|
||||
|
||||
// Capture parent_state_root before env is moved into spawn_caching_with
|
||||
let parent_state_root = env.parent_state_root;
|
||||
let transaction_count = env.transaction_count;
|
||||
let prewarm_handle = self.spawn_caching_with(
|
||||
env,
|
||||
prewarm_rx,
|
||||
provider_builder.clone(),
|
||||
Some(to_multi_proof.clone()),
|
||||
bal,
|
||||
v2_proofs_enabled,
|
||||
);
|
||||
|
||||
// Handle BAL-based optimization if available
|
||||
let prewarm_handle = if let Some(bal) = bal {
|
||||
// When BAL is present, use BAL prewarming and send BAL to multiproof
|
||||
debug!(target: "engine::tree::payload_processor", "BAL present, using BAL prewarming");
|
||||
|
||||
// The prewarm task converts the BAL to HashedPostState and sends it on
|
||||
// to_multi_proof after slot prefetching completes.
|
||||
self.spawn_caching_with(
|
||||
env,
|
||||
prewarm_rx,
|
||||
provider_builder.clone(),
|
||||
Some(to_multi_proof.clone()),
|
||||
Some(bal),
|
||||
v2_proofs_enabled,
|
||||
)
|
||||
} else {
|
||||
// Normal path: spawn with transaction prewarming
|
||||
self.spawn_caching_with(
|
||||
env,
|
||||
prewarm_rx,
|
||||
provider_builder.clone(),
|
||||
Some(to_multi_proof.clone()),
|
||||
None,
|
||||
v2_proofs_enabled,
|
||||
)
|
||||
};
|
||||
|
||||
// Create and spawn the storage proof task
|
||||
// Create and spawn the storage proof task.
|
||||
let task_ctx = ProofTaskCtx::new(multiproof_provider_factory);
|
||||
let proof_handle = ProofWorkerHandle::new(&self.executor, task_ctx, v2_proofs_enabled);
|
||||
let halve_workers = transaction_count <= Self::SMALL_BLOCK_PROOF_WORKER_TX_THRESHOLD;
|
||||
let proof_handle =
|
||||
ProofWorkerHandle::new(&self.executor, task_ctx, halve_workers, v2_proofs_enabled);
|
||||
|
||||
if config.disable_trie_cache() {
|
||||
let multi_proof_task = MultiProofTask::new(
|
||||
@@ -363,6 +344,10 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
/// Transaction count threshold below which proof workers are halved, since fewer transactions
|
||||
/// produce fewer state changes and most workers would be idle overhead.
|
||||
const SMALL_BLOCK_PROOF_WORKER_TX_THRESHOLD: usize = 30;
|
||||
|
||||
/// Transaction count threshold below which sequential signature recovery is used.
|
||||
///
|
||||
/// For blocks with fewer than this many transactions, the rayon parallel iterator overhead
|
||||
@@ -374,8 +359,11 @@ where
|
||||
/// Spawns a task advancing transaction env iterator and streaming updates through a channel.
|
||||
///
|
||||
/// For blocks with fewer than [`Self::SMALL_BLOCK_TX_THRESHOLD`] transactions, uses
|
||||
/// sequential iteration to avoid rayon overhead.
|
||||
/// sequential iteration to avoid rayon overhead. For larger blocks, uses rayon parallel
|
||||
/// iteration with [`ForEachOrdered`] to recover signatures in parallel while streaming
|
||||
/// results to execution in the original transaction order.
|
||||
#[expect(clippy::type_complexity)]
|
||||
#[instrument(level = "debug", target = "engine::tree::payload_processor", skip_all)]
|
||||
fn spawn_tx_iterator<I: ExecutableTxIterator<Evm>>(
|
||||
&self,
|
||||
transactions: I,
|
||||
@@ -384,9 +372,8 @@ where
|
||||
mpsc::Receiver<WithTxEnv<TxEnvFor<Evm>, I::Recovered>>,
|
||||
mpsc::Receiver<Result<WithTxEnv<TxEnvFor<Evm>, I::Recovered>, I::Error>>,
|
||||
) {
|
||||
let (ooo_tx, ooo_rx) = mpsc::channel();
|
||||
let (prewarm_tx, prewarm_rx) = mpsc::channel();
|
||||
let (execute_tx, execute_rx) = mpsc::channel();
|
||||
let (prewarm_tx, prewarm_rx) = mpsc::sync_channel(transaction_count);
|
||||
let (execute_tx, execute_rx) = mpsc::sync_channel(transaction_count);
|
||||
|
||||
if transaction_count == 0 {
|
||||
// Empty block — nothing to do.
|
||||
@@ -400,7 +387,7 @@ where
|
||||
);
|
||||
self.executor.spawn_blocking(move || {
|
||||
let (transactions, convert) = transactions.into_parts();
|
||||
for (idx, tx) in transactions.into_iter().enumerate() {
|
||||
for tx in transactions {
|
||||
let tx = convert.convert(tx);
|
||||
let tx = tx.map(|tx| {
|
||||
let (tx_env, tx) = tx.into_parts();
|
||||
@@ -409,57 +396,42 @@ where
|
||||
if let Ok(tx) = &tx {
|
||||
let _ = prewarm_tx.send(tx.clone());
|
||||
}
|
||||
let _ = ooo_tx.send((idx, tx));
|
||||
let _ = execute_tx.send(tx);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// Parallel path — spawn on rayon for parallel signature recovery.
|
||||
// Parallel path — recover signatures in parallel on rayon, stream results
|
||||
// to execution in order via `for_each_ordered`.
|
||||
rayon::spawn(move || {
|
||||
let (transactions, convert) = transactions.into_parts();
|
||||
transactions.into_par_iter().enumerate().for_each_with(
|
||||
ooo_tx,
|
||||
|ooo_tx, (idx, tx)| {
|
||||
transactions
|
||||
.into_par_iter()
|
||||
.map(|tx| {
|
||||
let tx = convert.convert(tx);
|
||||
let tx = tx.map(|tx| {
|
||||
tx.map(|tx| {
|
||||
let (tx_env, tx) = tx.into_parts();
|
||||
WithTxEnv { tx_env, tx: Arc::new(tx) }
|
||||
});
|
||||
// Only send Ok(_) variants to prewarming task.
|
||||
if let Ok(tx) = &tx {
|
||||
let tx = WithTxEnv { tx_env, tx: Arc::new(tx) };
|
||||
// Send to prewarming out of order — order doesn't matter there.
|
||||
let _ = prewarm_tx.send(tx.clone());
|
||||
}
|
||||
let _ = ooo_tx.send((idx, tx));
|
||||
},
|
||||
);
|
||||
tx
|
||||
})
|
||||
})
|
||||
.for_each_ordered(|tx| {
|
||||
let _ = execute_tx.send(tx);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Spawn a task that processes out-of-order transactions from the task above and sends them
|
||||
// to the execution task in order.
|
||||
self.executor.spawn_blocking(move || {
|
||||
let mut next_for_execution = 0;
|
||||
let mut queue = BTreeMap::new();
|
||||
while let Ok((idx, tx)) = ooo_rx.recv() {
|
||||
if next_for_execution == idx {
|
||||
let _ = execute_tx.send(tx);
|
||||
next_for_execution += 1;
|
||||
|
||||
while let Some(entry) = queue.first_entry() &&
|
||||
*entry.key() == next_for_execution
|
||||
{
|
||||
let _ = execute_tx.send(entry.remove());
|
||||
next_for_execution += 1;
|
||||
}
|
||||
} else {
|
||||
queue.insert(idx, tx);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
(prewarm_rx, execute_rx)
|
||||
}
|
||||
|
||||
/// Spawn prewarming optionally wired to the multiproof task for target updates.
|
||||
#[instrument(
|
||||
level = "debug",
|
||||
target = "engine::tree::payload_processor",
|
||||
skip_all,
|
||||
fields(bal=%bal.is_some(), %v2_proofs_enabled)
|
||||
)]
|
||||
fn spawn_caching_with<P>(
|
||||
&self,
|
||||
env: ExecutionEnv<Evm>,
|
||||
@@ -498,7 +470,6 @@ where
|
||||
self.prewarm_max_concurrency,
|
||||
);
|
||||
|
||||
// spawn pre-warm task
|
||||
{
|
||||
let to_prewarm_task = to_prewarm_task.clone();
|
||||
self.executor.spawn_blocking(move || {
|
||||
|
||||
@@ -771,6 +771,11 @@ impl MultiProofTask {
|
||||
fn on_prefetch_proof(&mut self, mut targets: VersionedMultiProofTargets) -> u64 {
|
||||
// Remove already fetched proof targets to avoid redundant work.
|
||||
targets.retain_difference(&self.fetched_proof_targets);
|
||||
|
||||
if targets.is_empty() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
extend_multiproof_targets(&mut self.fetched_proof_targets, &targets);
|
||||
|
||||
// For Legacy multiproofs, make sure all target accounts have an `AddedRemovedKeySet` in the
|
||||
@@ -889,6 +894,10 @@ impl MultiProofTask {
|
||||
state_updates += 1;
|
||||
}
|
||||
|
||||
if not_fetched_state_update.is_empty() {
|
||||
return state_updates;
|
||||
}
|
||||
|
||||
// Clone+Arc MultiAddedRemovedKeys for sharing with the dispatched multiproof tasks
|
||||
let multi_added_removed_keys = Arc::new(MultiAddedRemovedKeys {
|
||||
account: self.multi_added_removed_keys.account.clone(),
|
||||
@@ -1573,7 +1582,7 @@ mod tests {
|
||||
let changeset_cache = ChangesetCache::new();
|
||||
let overlay_factory = OverlayStateProviderFactory::new(factory, changeset_cache);
|
||||
let task_ctx = ProofTaskCtx::new(overlay_factory);
|
||||
let proof_handle = ProofWorkerHandle::new(runtime, task_ctx, false);
|
||||
let proof_handle = ProofWorkerHandle::new(runtime, task_ctx, false, false);
|
||||
let (to_sparse_trie, _receiver) = std::sync::mpsc::channel();
|
||||
let (tx, rx) = crossbeam_channel::unbounded();
|
||||
|
||||
@@ -2056,7 +2065,7 @@ mod tests {
|
||||
panic!("Expected PrefetchProofs message");
|
||||
};
|
||||
|
||||
assert_eq!(proofs_requested, 1);
|
||||
assert!(proofs_requested >= 1);
|
||||
}
|
||||
|
||||
/// Verifies that different message types arriving mid-batch are not lost and preserve order.
|
||||
|
||||
@@ -42,7 +42,7 @@ use std::{
|
||||
ops::Range,
|
||||
sync::{
|
||||
atomic::{AtomicBool, Ordering},
|
||||
mpsc::{self, channel, Receiver, Sender},
|
||||
mpsc::{self, channel, Receiver, Sender, SyncSender},
|
||||
Arc,
|
||||
},
|
||||
time::Instant,
|
||||
@@ -154,8 +154,6 @@ where
|
||||
self.executor.spawn_blocking(move || {
|
||||
let _enter = debug_span!(target: "engine::tree::payload_processor::prewarm", parent: span, "spawn_all").entered();
|
||||
|
||||
let (done_tx, done_rx) = mpsc::channel();
|
||||
|
||||
// When transaction_count is 0, it means the count is unknown. In this case, spawn
|
||||
// max workers to handle potentially many transactions in parallel rather
|
||||
// than bottlenecking on a single worker.
|
||||
@@ -166,6 +164,8 @@ where
|
||||
transaction_count.min(max_concurrency)
|
||||
};
|
||||
|
||||
let (done_tx, done_rx) = mpsc::sync_channel(workers_needed);
|
||||
|
||||
// Spawn workers
|
||||
let tx_sender = ctx.clone().spawn_workers(workers_needed, &executor, to_multi_proof.clone(), done_tx.clone());
|
||||
|
||||
@@ -236,36 +236,33 @@ where
|
||||
|
||||
if let Some(saved_cache) = saved_cache {
|
||||
debug!(target: "engine::caching", parent_hash=?hash, "Updating execution cache");
|
||||
// Perform all cache operations atomically under the lock
|
||||
|
||||
// Detach the published cache so readers see None during the update.
|
||||
// This is necessary because ExecutionCache is Arc-shared: mutating
|
||||
// it via insert_state would be visible through the old SavedCache.
|
||||
execution_cache.update_with_guard(|cached| {
|
||||
// consumes the `SavedCache` held by the prewarming task, which releases its usage
|
||||
// guard
|
||||
let (caches, cache_metrics, disable_cache_metrics) = saved_cache.split();
|
||||
let new_cache = SavedCache::new(hash, caches, cache_metrics)
|
||||
.with_disable_cache_metrics(disable_cache_metrics);
|
||||
cached.take();
|
||||
});
|
||||
|
||||
// Insert state into cache while holding the lock
|
||||
// Access the BundleState through the shared ExecutionOutcome
|
||||
if new_cache.cache().insert_state(&execution_outcome.state).is_err() {
|
||||
// Clear the cache on error to prevent having a polluted cache
|
||||
*cached = None;
|
||||
debug!(target: "engine::caching", "cleared execution cache on update error");
|
||||
return;
|
||||
}
|
||||
let (caches, cache_metrics, disable_cache_metrics) = saved_cache.split();
|
||||
let new_cache = SavedCache::new(hash, caches, cache_metrics)
|
||||
.with_disable_cache_metrics(disable_cache_metrics);
|
||||
|
||||
if new_cache.cache().insert_state(&execution_outcome.state).is_err() {
|
||||
debug!(target: "engine::caching", "cleared execution cache on update error");
|
||||
} else {
|
||||
new_cache.update_metrics();
|
||||
|
||||
if valid_block_rx.recv().is_ok() {
|
||||
// Replace the shared cache with the new one; the previous cache (if any) is
|
||||
// dropped.
|
||||
*cached = Some(new_cache);
|
||||
} else {
|
||||
// Block was invalid; caches were already mutated by insert_state above,
|
||||
// so we must clear to prevent using polluted state
|
||||
*cached = None;
|
||||
debug!(target: "engine::caching", "cleared execution cache on invalid block");
|
||||
}
|
||||
});
|
||||
let valid = valid_block_rx.recv().is_ok();
|
||||
|
||||
execution_cache.update_with_guard(|cached| {
|
||||
if valid {
|
||||
*cached = Some(new_cache);
|
||||
} else {
|
||||
debug!(target: "engine::caching", "cleared execution cache on invalid block");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
let elapsed = start.elapsed();
|
||||
debug!(target: "engine::caching", parent_hash=?hash, elapsed=?elapsed, "Updated execution cache");
|
||||
@@ -312,11 +309,11 @@ where
|
||||
return;
|
||||
}
|
||||
|
||||
let (done_tx, done_rx) = mpsc::channel();
|
||||
|
||||
// Calculate number of workers needed (at most max_concurrency)
|
||||
let workers_needed = total_slots.min(self.max_concurrency);
|
||||
|
||||
let (done_tx, done_rx) = mpsc::sync_channel(workers_needed);
|
||||
|
||||
// Calculate slots per worker
|
||||
let slots_per_worker = total_slots / workers_needed;
|
||||
let remainder = total_slots % workers_needed;
|
||||
@@ -585,7 +582,7 @@ where
|
||||
self,
|
||||
txs: CrossbeamReceiver<IndexedTransaction<Tx>>,
|
||||
to_multi_proof: Option<CrossbeamSender<MultiProofMessage>>,
|
||||
done_tx: Sender<()>,
|
||||
done_tx: SyncSender<()>,
|
||||
) where
|
||||
Tx: ExecutableTxFor<Evm>,
|
||||
{
|
||||
@@ -660,7 +657,7 @@ where
|
||||
workers_needed: usize,
|
||||
task_executor: &Runtime,
|
||||
to_multi_proof: Option<CrossbeamSender<MultiProofMessage>>,
|
||||
done_tx: Sender<()>,
|
||||
done_tx: SyncSender<()>,
|
||||
) -> CrossbeamSender<IndexedTransaction<Tx>>
|
||||
where
|
||||
Tx: ExecutableTxFor<Evm> + Send + 'static,
|
||||
@@ -698,7 +695,7 @@ where
|
||||
executor: &Runtime,
|
||||
bal: Arc<BlockAccessList>,
|
||||
range: Range<usize>,
|
||||
done_tx: Sender<()>,
|
||||
done_tx: SyncSender<()>,
|
||||
) {
|
||||
let ctx = self.clone();
|
||||
let span = debug_span!(
|
||||
@@ -724,7 +721,7 @@ where
|
||||
self,
|
||||
bal: Arc<BlockAccessList>,
|
||||
range: Range<usize>,
|
||||
done_tx: Sender<()>,
|
||||
done_tx: SyncSender<()>,
|
||||
) {
|
||||
let Self { saved_cache, provider, metrics, .. } = self;
|
||||
|
||||
|
||||
@@ -593,18 +593,24 @@ where
|
||||
self.process_leaf_updates(true)?;
|
||||
|
||||
for (address, mut new) in self.new_storage_updates.drain() {
|
||||
let updates = self.storage_updates.entry(address).or_default();
|
||||
for (slot, new) in new.drain() {
|
||||
match updates.entry(slot) {
|
||||
Entry::Occupied(mut entry) => {
|
||||
// Only overwrite existing entries with new values
|
||||
if new.is_changed() {
|
||||
entry.insert(new);
|
||||
match self.storage_updates.entry(address) {
|
||||
Entry::Vacant(entry) => {
|
||||
entry.insert(new); // insert the whole map at once, no per-slot loop
|
||||
}
|
||||
Entry::Occupied(mut entry) => {
|
||||
let updates = entry.get_mut();
|
||||
for (slot, new) in new.drain() {
|
||||
match updates.entry(slot) {
|
||||
Entry::Occupied(mut slot_entry) => {
|
||||
if new.is_changed() {
|
||||
slot_entry.insert(new);
|
||||
}
|
||||
}
|
||||
Entry::Vacant(slot_entry) => {
|
||||
slot_entry.insert(new);
|
||||
}
|
||||
}
|
||||
}
|
||||
Entry::Vacant(entry) => {
|
||||
entry.insert(new);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,11 +10,11 @@ use crate::tree::{
|
||||
EngineApiMetrics, EngineApiTreeState, ExecutionEnv, PayloadHandle, StateProviderBuilder,
|
||||
StateProviderDatabase, TreeConfig,
|
||||
};
|
||||
use alloy_consensus::transaction::{Either, TxHashRef};
|
||||
use alloy_consensus::{proofs::calculate_receipt_root, transaction::Either, TxHashRef, TxReceipt};
|
||||
use alloy_eip7928::BlockAccessList;
|
||||
use alloy_eips::{eip1898::BlockWithParent, eip4895::Withdrawal, NumHash};
|
||||
use alloy_evm::Evm;
|
||||
use alloy_primitives::B256;
|
||||
use alloy_primitives::{Bloom, B256};
|
||||
|
||||
use crate::tree::payload_processor::receipt_root_task::{IndexedReceipt, ReceiptRootTaskHandle};
|
||||
use reth_chain_state::{CanonicalInMemoryState, DeferredTrieData, ExecutedBlock, LazyOverlay};
|
||||
@@ -53,6 +53,16 @@ use std::{
|
||||
};
|
||||
use tracing::{debug, debug_span, error, info, instrument, trace, warn};
|
||||
|
||||
/// Blocks with at most this many transactions compute the receipt root inline to avoid
|
||||
/// background task overhead.
|
||||
const SMALL_BLOCK_RECEIPT_ROOT_TX_THRESHOLD: usize = 50;
|
||||
const SMALL_BLOCK_STATE_ROOT_TX_THRESHOLD: usize = 50;
|
||||
|
||||
enum ReceiptRootResult {
|
||||
Precomputed(ReceiptRootBloom),
|
||||
Pending(tokio::sync::oneshot::Receiver<ReceiptRootBloom>),
|
||||
}
|
||||
|
||||
/// Context providing access to tree state during validation.
|
||||
///
|
||||
/// This context is provided to the [`EngineValidator`] and includes the state of the tree's
|
||||
@@ -290,7 +300,7 @@ where
|
||||
// Validate block consensus rules which includes header validation
|
||||
if let Err(consensus_err) = self.validate_block_inner(&block) {
|
||||
// Header validation error takes precedence over execution error
|
||||
return Err(InsertBlockError::new(block, consensus_err.into()).into())
|
||||
return Err(InsertBlockError::new(block, consensus_err.into()).into());
|
||||
}
|
||||
|
||||
// Also validate against the parent
|
||||
@@ -298,7 +308,7 @@ where
|
||||
self.consensus.validate_header_against_parent(block.sealed_header(), parent_block)
|
||||
{
|
||||
// Parent validation error takes precedence over execution error
|
||||
return Err(InsertBlockError::new(block, consensus_err.into()).into())
|
||||
return Err(InsertBlockError::new(block, consensus_err.into()).into());
|
||||
}
|
||||
|
||||
// No header validation errors, return the original execution error
|
||||
@@ -338,7 +348,7 @@ where
|
||||
Ok(val) => val,
|
||||
Err(e) => {
|
||||
let block = self.convert_to_block(input)?;
|
||||
return Err(InsertBlockError::new(block, e.into()).into())
|
||||
return Err(InsertBlockError::new(block, e.into()).into());
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -371,7 +381,7 @@ where
|
||||
self.convert_to_block(input)?,
|
||||
ProviderError::HeaderNotFound(parent_hash.into()).into(),
|
||||
)
|
||||
.into())
|
||||
.into());
|
||||
};
|
||||
let mut state_provider = ensure_ok!(provider_builder.build());
|
||||
drop(_enter);
|
||||
@@ -384,7 +394,7 @@ where
|
||||
self.convert_to_block(input)?,
|
||||
ProviderError::HeaderNotFound(parent_hash.into()).into(),
|
||||
)
|
||||
.into())
|
||||
.into());
|
||||
};
|
||||
|
||||
let evm_env = debug_span!(target: "engine::tree::payload_validator", "evm env")
|
||||
@@ -401,7 +411,7 @@ where
|
||||
};
|
||||
|
||||
// Plan the strategy used for state root computation.
|
||||
let strategy = self.plan_state_root_computation();
|
||||
let strategy = self.plan_state_root_computation(input.transaction_count());
|
||||
|
||||
debug!(
|
||||
target: "engine::tree::payload_validator",
|
||||
@@ -455,7 +465,7 @@ where
|
||||
// Execute the block and handle any execution errors.
|
||||
// The receipt root task is spawned before execution and receives receipts incrementally
|
||||
// as transactions complete, allowing parallel computation during execution.
|
||||
let (output, senders, receipt_root_rx) =
|
||||
let (output, senders, receipt_root_result) =
|
||||
match self.execute_block(state_provider, env, &input, &mut handle) {
|
||||
Ok(output) => output,
|
||||
Err(err) => return self.handle_execution_error(input, err, &parent_block),
|
||||
@@ -476,15 +486,18 @@ where
|
||||
let block = self.convert_to_block(input)?.with_senders(senders);
|
||||
|
||||
// Wait for the receipt root computation to complete.
|
||||
let receipt_root_bloom = receipt_root_rx
|
||||
.blocking_recv()
|
||||
.inspect_err(|_| {
|
||||
tracing::error!(
|
||||
target: "engine::tree::payload_validator",
|
||||
"Receipt root task dropped sender without result, receipt root calculation likely aborted"
|
||||
);
|
||||
})
|
||||
.ok();
|
||||
let receipt_root_bloom = match receipt_root_result {
|
||||
ReceiptRootResult::Precomputed(receipt_root_bloom) => Some(receipt_root_bloom),
|
||||
ReceiptRootResult::Pending(receipt_root_rx) => receipt_root_rx
|
||||
.blocking_recv()
|
||||
.inspect_err(|_| {
|
||||
tracing::error!(
|
||||
target: "engine::tree::payload_validator",
|
||||
"Receipt root task dropped sender without result, receipt root calculation likely aborted"
|
||||
);
|
||||
})
|
||||
.ok(),
|
||||
};
|
||||
|
||||
let hashed_state = ensure_ok_post_block!(
|
||||
self.validate_post_execution(
|
||||
@@ -617,7 +630,7 @@ where
|
||||
)
|
||||
.into(),
|
||||
)
|
||||
.into())
|
||||
.into());
|
||||
}
|
||||
|
||||
if let Some(valid_block_tx) = valid_block_tx {
|
||||
@@ -656,12 +669,12 @@ where
|
||||
fn validate_block_inner(&self, block: &SealedBlock<N::Block>) -> Result<(), ConsensusError> {
|
||||
if let Err(e) = self.consensus.validate_header(block.sealed_header()) {
|
||||
error!(target: "engine::tree::payload_validator", ?block, "Failed to validate header {}: {e}", block.hash());
|
||||
return Err(e)
|
||||
return Err(e);
|
||||
}
|
||||
|
||||
if let Err(e) = self.consensus.validate_block_pre_execution(block) {
|
||||
error!(target: "engine::tree::payload_validator", ?block, "Failed to validate block {}: {e}", block.hash());
|
||||
return Err(e)
|
||||
return Err(e);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
@@ -671,7 +684,7 @@ where
|
||||
///
|
||||
/// This method orchestrates block execution:
|
||||
/// 1. Sets up the EVM with state database and precompile caching
|
||||
/// 2. Spawns a background task for incremental receipt root computation
|
||||
/// 2. Spawns a background task for incremental receipt root computation (if needed)
|
||||
/// 3. Executes transactions with metrics collection via state hooks
|
||||
/// 4. Merges state transitions and records execution metrics
|
||||
#[instrument(level = "debug", target = "engine::tree::payload_validator", skip_all)]
|
||||
@@ -683,11 +696,7 @@ where
|
||||
input: &BlockOrPayload<T>,
|
||||
handle: &mut PayloadHandle<impl ExecutableTxFor<Evm>, Err, N::Receipt>,
|
||||
) -> Result<
|
||||
(
|
||||
BlockExecutionOutput<N::Receipt>,
|
||||
Vec<Address>,
|
||||
tokio::sync::oneshot::Receiver<(B256, alloy_primitives::Bloom)>,
|
||||
),
|
||||
(BlockExecutionOutput<N::Receipt>, Vec<Address>, ReceiptRootResult),
|
||||
InsertBlockErrorKind,
|
||||
>
|
||||
where
|
||||
@@ -731,10 +740,16 @@ where
|
||||
// Spawn background task to compute receipt root and logs bloom incrementally.
|
||||
// Unbounded channel is used since tx count bounds capacity anyway (max ~30k txs per block).
|
||||
let receipts_len = input.transaction_count();
|
||||
let (receipt_tx, receipt_rx) = crossbeam_channel::unbounded();
|
||||
let (result_tx, result_rx) = tokio::sync::oneshot::channel();
|
||||
let task_handle = ReceiptRootTaskHandle::new(receipt_rx, result_tx);
|
||||
self.payload_processor.executor().spawn_blocking(move || task_handle.run(receipts_len));
|
||||
let compute_receipt_root_inline = receipts_len <= SMALL_BLOCK_RECEIPT_ROOT_TX_THRESHOLD;
|
||||
let (receipt_tx, result_rx) = if compute_receipt_root_inline {
|
||||
(None, None)
|
||||
} else {
|
||||
let (receipt_tx, receipt_rx) = crossbeam_channel::unbounded();
|
||||
let (result_tx, result_rx) = tokio::sync::oneshot::channel();
|
||||
let task_handle = ReceiptRootTaskHandle::new(receipt_rx, result_tx);
|
||||
self.payload_processor.executor().spawn_blocking(move || task_handle.run(receipts_len));
|
||||
(Some(receipt_tx), Some(result_rx))
|
||||
};
|
||||
|
||||
let transaction_count = input.transaction_count();
|
||||
let executor = executor.with_state_hook(Some(Box::new(handle.state_hook())));
|
||||
@@ -746,7 +761,7 @@ where
|
||||
executor,
|
||||
transaction_count,
|
||||
handle.iter_transactions(),
|
||||
&receipt_tx,
|
||||
receipt_tx.as_ref(),
|
||||
)?;
|
||||
drop(receipt_tx);
|
||||
|
||||
@@ -763,11 +778,21 @@ where
|
||||
|
||||
let output = BlockExecutionOutput { result, state: db.take_bundle() };
|
||||
|
||||
let receipt_root_result = if compute_receipt_root_inline {
|
||||
ReceiptRootResult::Precomputed(Self::compute_receipt_root_bloom(
|
||||
&output.result.receipts,
|
||||
))
|
||||
} else {
|
||||
ReceiptRootResult::Pending(
|
||||
result_rx.expect("receipt root receiver missing when task spawned"),
|
||||
)
|
||||
};
|
||||
|
||||
let execution_duration = execution_start.elapsed();
|
||||
self.metrics.record_block_execution(&output, execution_duration);
|
||||
|
||||
debug!(target: "engine::tree::payload_validator", elapsed = ?execution_duration, "Executed block");
|
||||
Ok((output, senders, result_rx))
|
||||
Ok((output, senders, receipt_root_result))
|
||||
}
|
||||
|
||||
/// Executes transactions and collects senders, streaming receipts to a background task.
|
||||
@@ -784,7 +809,7 @@ where
|
||||
mut executor: E,
|
||||
transaction_count: usize,
|
||||
transactions: impl Iterator<Item = Result<Tx, Err>>,
|
||||
receipt_tx: &crossbeam_channel::Sender<IndexedReceipt<N::Receipt>>,
|
||||
receipt_tx: Option<&crossbeam_channel::Sender<IndexedReceipt<N::Receipt>>>,
|
||||
) -> Result<(E, Vec<Address>), BlockExecutionError>
|
||||
where
|
||||
E: BlockExecutor<Receipt = N::Receipt>,
|
||||
@@ -831,13 +856,15 @@ where
|
||||
executor.execute_transaction(tx)?;
|
||||
self.metrics.record_transaction_execution(tx_start.elapsed());
|
||||
|
||||
let current_len = executor.receipts().len();
|
||||
if current_len > last_sent_len {
|
||||
last_sent_len = current_len;
|
||||
// Send the latest receipt to the background task for incremental root computation.
|
||||
if let Some(receipt) = executor.receipts().last() {
|
||||
let tx_index = current_len - 1;
|
||||
let _ = receipt_tx.send(IndexedReceipt::new(tx_index, receipt.clone()));
|
||||
if let Some(receipt_tx) = receipt_tx {
|
||||
let current_len = executor.receipts().len();
|
||||
if current_len > last_sent_len {
|
||||
last_sent_len = current_len;
|
||||
// Send the latest receipt to the background task for incremental root computation.
|
||||
if let Some(receipt) = executor.receipts().last() {
|
||||
let tx_index = current_len - 1;
|
||||
let _ = receipt_tx.send(IndexedReceipt::new(tx_index, receipt.clone()));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1073,7 +1100,7 @@ where
|
||||
trace!(target: "engine::tree::payload_validator", block=?block.num_hash(), "Validating block consensus");
|
||||
// validate block consensus rules
|
||||
if let Err(e) = self.validate_block_inner(block) {
|
||||
return Err(e.into())
|
||||
return Err(e.into());
|
||||
}
|
||||
|
||||
// now validate against the parent
|
||||
@@ -1082,7 +1109,7 @@ where
|
||||
self.consensus.validate_header_against_parent(block.sealed_header(), parent_block)
|
||||
{
|
||||
warn!(target: "engine::tree::payload_validator", ?block, "Failed to validate header {} against parent: {e}", block.hash());
|
||||
return Err(e.into())
|
||||
return Err(e.into());
|
||||
}
|
||||
drop(_enter);
|
||||
|
||||
@@ -1095,7 +1122,7 @@ where
|
||||
{
|
||||
// call post-block hook
|
||||
self.on_invalid_block(parent_block, block, output, None, ctx.state_mut());
|
||||
return Err(err.into())
|
||||
return Err(err.into());
|
||||
}
|
||||
drop(_enter);
|
||||
|
||||
@@ -1110,7 +1137,7 @@ where
|
||||
{
|
||||
// call post-block hook
|
||||
self.on_invalid_block(parent_block, block, output, None, ctx.state_mut());
|
||||
return Err(err.into())
|
||||
return Err(err.into());
|
||||
}
|
||||
|
||||
// record post-execution validation duration
|
||||
@@ -1122,6 +1149,16 @@ where
|
||||
Ok(hashed_state)
|
||||
}
|
||||
|
||||
fn compute_receipt_root_bloom(receipts: &[N::Receipt]) -> ReceiptRootBloom {
|
||||
let receipts_with_bloom =
|
||||
receipts.iter().map(TxReceipt::with_bloom_ref).collect::<Vec<_>>();
|
||||
let receipts_root = calculate_receipt_root(&receipts_with_bloom);
|
||||
let logs_bloom = receipts_with_bloom
|
||||
.iter()
|
||||
.fold(Bloom::ZERO, |bloom, receipt| bloom | receipt.bloom_ref());
|
||||
(receipts_root, logs_bloom)
|
||||
}
|
||||
|
||||
/// Spawns a payload processor task based on the state root strategy.
|
||||
///
|
||||
/// This method determines how to execute the block and compute its state root based on
|
||||
@@ -1142,7 +1179,7 @@ where
|
||||
level = "debug",
|
||||
target = "engine::tree::payload_validator",
|
||||
skip_all,
|
||||
fields(strategy)
|
||||
fields(?strategy)
|
||||
)]
|
||||
fn spawn_payload_processor<T: ExecutableTxIterator<Evm>>(
|
||||
&mut self,
|
||||
@@ -1218,7 +1255,7 @@ where
|
||||
self.provider.clone(),
|
||||
historical,
|
||||
Some(blocks),
|
||||
)))
|
||||
)));
|
||||
}
|
||||
|
||||
// Check if the block is persisted
|
||||
@@ -1226,7 +1263,7 @@ where
|
||||
debug!(target: "engine::tree::payload_validator", %hash, number = %header.number(), "found canonical state for block in database, creating provider builder");
|
||||
// For persisted blocks, we create a builder that will fetch state directly from the
|
||||
// database
|
||||
return Ok(Some(StateProviderBuilder::new(self.provider.clone(), hash, None)))
|
||||
return Ok(Some(StateProviderBuilder::new(self.provider.clone(), hash, None)));
|
||||
}
|
||||
|
||||
debug!(target: "engine::tree::payload_validator", %hash, "no canonical state found for block");
|
||||
@@ -1237,7 +1274,12 @@ where
|
||||
///
|
||||
/// Note: Use state root task only if prefix sets are empty, otherwise proof generation is
|
||||
/// too expensive because it requires walking all paths in every proof.
|
||||
const fn plan_state_root_computation(&self) -> StateRootStrategy {
|
||||
fn plan_state_root_computation(&self, transaction_count: usize) -> StateRootStrategy {
|
||||
// Small blocks are faster without spawning parallel state root tasks.
|
||||
if transaction_count > 0 && transaction_count <= SMALL_BLOCK_STATE_ROOT_TX_THRESHOLD {
|
||||
return StateRootStrategy::Synchronous;
|
||||
}
|
||||
|
||||
if self.config.state_root_fallback() {
|
||||
StateRootStrategy::Synchronous
|
||||
} else if self.config.use_state_root_task() {
|
||||
@@ -1258,7 +1300,7 @@ where
|
||||
) {
|
||||
if state.invalid_headers.get(&block.hash()).is_some() {
|
||||
// we already marked this block as invalid
|
||||
return
|
||||
return;
|
||||
}
|
||||
self.invalid_block_hook.on_invalid_block(parent_header, block, output, trie_updates);
|
||||
}
|
||||
|
||||
@@ -425,17 +425,9 @@ impl TotalDifficulty {
|
||||
|
||||
/// Convert to an [`Entry`]
|
||||
pub fn to_entry(&self) -> Entry {
|
||||
let mut data = [0u8; 32];
|
||||
|
||||
let be_bytes = self.value.to_be_bytes_vec();
|
||||
|
||||
if be_bytes.len() <= 32 {
|
||||
data[32 - be_bytes.len()..].copy_from_slice(&be_bytes);
|
||||
} else {
|
||||
data.copy_from_slice(&be_bytes[be_bytes.len() - 32..]);
|
||||
}
|
||||
|
||||
Entry::new(TOTAL_DIFFICULTY, data.to_vec())
|
||||
// era1 spec: `total-difficulty = { type: 0x0600, data: SSZ uint256 }` (little-endian)
|
||||
let data = self.value.to_le_bytes::<32>().to_vec();
|
||||
Entry::new(TOTAL_DIFFICULTY, data)
|
||||
}
|
||||
|
||||
/// Create from an [`Entry`]
|
||||
@@ -454,8 +446,8 @@ impl TotalDifficulty {
|
||||
)));
|
||||
}
|
||||
|
||||
// Convert 32-byte array to U256
|
||||
let value = U256::from_be_slice(&entry.data);
|
||||
// era1 spec: `total-difficulty = { type: 0x0600, data: SSZ uint256 }` (little-endian)
|
||||
let value = U256::from_le_slice(&entry.data);
|
||||
|
||||
Ok(Self { value })
|
||||
}
|
||||
@@ -608,6 +600,19 @@ mod tests {
|
||||
assert_eq!(recovered.value, value);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_total_difficulty_ssz_le_encoding() {
|
||||
// Verify that total-difficulty is encoded as SSZ uint256 (little-endian).
|
||||
// See https://github.com/eth-clients/e2store-format-specs/blob/main/formats/era1.md
|
||||
let value = U256::from(1u64);
|
||||
let td = TotalDifficulty::new(value);
|
||||
let entry = td.to_entry();
|
||||
|
||||
// Little-endian: least significant byte first [1, 0, 0, ..., 0]
|
||||
assert_eq!(entry.data[0], 1, "First byte must be 1 (little-endian)");
|
||||
assert_eq!(entry.data[31], 0, "Last byte must be 0 (little-endian)");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_compression_roundtrip() {
|
||||
let rlp_data = vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use alloy_primitives::{Address, B256, U256};
|
||||
use reth_primitives_traits::{Account, Bytecode};
|
||||
use revm::database::BundleState;
|
||||
use revm::database::{states::BundleState, BundleAccount};
|
||||
|
||||
pub use alloy_evm::block::BlockExecutionResult;
|
||||
|
||||
@@ -37,6 +37,11 @@ impl<T> BlockExecutionOutput<T> {
|
||||
self.state.account(address).map(|a| a.info.as_ref().map(Into::into))
|
||||
}
|
||||
|
||||
/// Returns the state [`BundleAccount`] for the given address.
|
||||
pub fn account_state(&self, address: &Address) -> Option<&BundleAccount> {
|
||||
self.state.account(address)
|
||||
}
|
||||
|
||||
/// Get storage if value is known.
|
||||
///
|
||||
/// This means that depending on status we can potentially return `U256::ZERO`.
|
||||
|
||||
@@ -10,6 +10,7 @@ use reth_provider::{BlockReader, Chain, HeaderProvider, StateProviderFactory};
|
||||
use reth_stages_api::ExecutionStageThresholds;
|
||||
use reth_tracing::tracing::debug;
|
||||
use std::{
|
||||
collections::VecDeque,
|
||||
fmt::Debug,
|
||||
pin::Pin,
|
||||
sync::Arc,
|
||||
@@ -286,6 +287,9 @@ where
|
||||
backfill_job: Option<StreamBackfillJob<E, P, Chain<E::Primitives>>>,
|
||||
/// Custom thresholds for the backfill job, if set.
|
||||
backfill_thresholds: Option<ExecutionStageThresholds>,
|
||||
/// Notifications that arrived during backfill and need to be delivered after it completes.
|
||||
/// These are notifications for blocks beyond the backfill range that we must not drop.
|
||||
pending_notifications: VecDeque<ExExNotification<E::Primitives>>,
|
||||
}
|
||||
|
||||
impl<P, E> ExExNotificationsWithHead<P, E>
|
||||
@@ -312,6 +316,7 @@ where
|
||||
pending_check_backfill: true,
|
||||
backfill_job: None,
|
||||
backfill_thresholds: None,
|
||||
pending_notifications: VecDeque::new(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -448,6 +453,34 @@ where
|
||||
// 3. If backfill is in progress yield new notifications
|
||||
if let Some(backfill_job) = &mut this.backfill_job {
|
||||
debug!(target: "exex::notifications", "Polling backfill job");
|
||||
|
||||
// Drain the notification channel to prevent backpressure from stalling the
|
||||
// ExExManager. During backfill, the ExEx is not consuming from the channel,
|
||||
// so the capacity-1 channel fills up, which blocks the manager's PollSender,
|
||||
// which fills the manager's 1024-entry buffer, which blocks all upstream
|
||||
// senders. Notifications for blocks covered by the backfill range are
|
||||
// discarded (they'll be re-delivered by the backfill job), while
|
||||
// notifications beyond the backfill range are buffered for delivery after the
|
||||
// backfill completes.
|
||||
while let Poll::Ready(Some(notification)) = this.notifications.poll_recv(cx) {
|
||||
// Always buffer revert-containing notifications (ChainReverted,
|
||||
// ChainReorged) because the backfill job only re-delivers
|
||||
// ChainCommitted from the database. Discarding a reorg here would
|
||||
// leave the ExEx unaware of the fork switch.
|
||||
if notification.reverted_chain().is_some() {
|
||||
this.pending_notifications.push_back(notification);
|
||||
continue;
|
||||
}
|
||||
if let Some(committed) = notification.committed_chain() &&
|
||||
committed.tip().number() <= this.initial_local_head.number
|
||||
{
|
||||
// Covered by backfill range, safe to discard
|
||||
continue;
|
||||
}
|
||||
// Beyond the backfill range — buffer for delivery after backfill
|
||||
this.pending_notifications.push_back(notification);
|
||||
}
|
||||
|
||||
if let Some(chain) = ready!(backfill_job.poll_next_unpin(cx)).transpose()? {
|
||||
debug!(target: "exex::notifications", range = ?chain.range(), "Backfill job returned a chain");
|
||||
return Poll::Ready(Some(Ok(ExExNotification::ChainCommitted {
|
||||
@@ -459,13 +492,18 @@ where
|
||||
this.backfill_job = None;
|
||||
}
|
||||
|
||||
// 4. Otherwise advance the regular event stream
|
||||
// 4. Deliver any notifications that were buffered during backfill
|
||||
if let Some(notification) = this.pending_notifications.pop_front() {
|
||||
return Poll::Ready(Some(Ok(notification)))
|
||||
}
|
||||
|
||||
// 5. Otherwise advance the regular event stream
|
||||
loop {
|
||||
let Some(notification) = ready!(this.notifications.poll_recv(cx)) else {
|
||||
return Poll::Ready(None)
|
||||
};
|
||||
|
||||
// 5. In case the exex is ahead of the new tip, we must skip it
|
||||
// 6. In case the exex is ahead of the new tip, we must skip it
|
||||
if let Some(committed) = notification.committed_chain() {
|
||||
// inclusive check because we should start with `exex.head + 1`
|
||||
if this.initial_exex_head.block.number >= committed.tip().number() {
|
||||
@@ -789,4 +827,135 @@ mod tests {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Regression test for <https://github.com/paradigmxyz/reth/issues/19665>.
|
||||
///
|
||||
/// During backfill, `poll_next` must drain the notification channel so that
|
||||
/// the upstream `ExExManager` is never blocked by a full channel. Without
|
||||
/// the drain loop the capacity-1 channel stays full for the entire backfill
|
||||
/// duration, which stalls the manager's `PollSender` and eventually blocks
|
||||
/// all upstream senders once the 1024-entry buffer fills up.
|
||||
///
|
||||
/// The key assertion is the `try_send` after the first `poll_next`: it
|
||||
/// proves the channel was drained during the backfill poll. Without the
|
||||
/// fix this `try_send` fails because the notification is still sitting in
|
||||
/// the channel.
|
||||
#[tokio::test]
|
||||
async fn exex_notifications_backfill_drains_channel() -> eyre::Result<()> {
|
||||
let mut rng = generators::rng();
|
||||
|
||||
let temp_dir = tempfile::tempdir().unwrap();
|
||||
let wal = Wal::new(temp_dir.path()).unwrap();
|
||||
|
||||
let provider_factory = create_test_provider_factory();
|
||||
let genesis_hash = init_genesis(&provider_factory)?;
|
||||
let genesis_block = provider_factory
|
||||
.block(genesis_hash.into())?
|
||||
.ok_or_else(|| eyre::eyre!("genesis block not found"))?;
|
||||
|
||||
let provider = BlockchainProvider::new(provider_factory.clone())?;
|
||||
|
||||
// Insert block 1 into the DB so there's something to backfill
|
||||
let node_head_block = random_block(
|
||||
&mut rng,
|
||||
genesis_block.number + 1,
|
||||
BlockParams { parent: Some(genesis_hash), tx_count: Some(0), ..Default::default() },
|
||||
)
|
||||
.try_recover()?;
|
||||
let node_head = node_head_block.num_hash();
|
||||
let provider_rw = provider_factory.provider_rw()?;
|
||||
provider_rw.insert_block(&node_head_block)?;
|
||||
provider_rw.commit()?;
|
||||
|
||||
// ExEx head is at genesis — backfill will run for block 1
|
||||
let exex_head =
|
||||
ExExHead { block: BlockNumHash { number: genesis_block.number, hash: genesis_hash } };
|
||||
|
||||
// Notification for a block AFTER the backfill range (block 2).
|
||||
let post_backfill_notification = ExExNotification::ChainCommitted {
|
||||
new: Arc::new(Chain::new(
|
||||
vec![random_block(
|
||||
&mut rng,
|
||||
node_head.number + 1,
|
||||
BlockParams { parent: Some(node_head.hash), ..Default::default() },
|
||||
)
|
||||
.try_recover()?],
|
||||
Default::default(),
|
||||
BTreeMap::new(),
|
||||
)),
|
||||
};
|
||||
|
||||
// Another notification (block 3) used to probe channel capacity.
|
||||
let probe_notification = ExExNotification::ChainCommitted {
|
||||
new: Arc::new(Chain::new(
|
||||
vec![random_block(
|
||||
&mut rng,
|
||||
node_head.number + 2,
|
||||
BlockParams { parent: None, ..Default::default() },
|
||||
)
|
||||
.try_recover()?],
|
||||
Default::default(),
|
||||
BTreeMap::new(),
|
||||
)),
|
||||
};
|
||||
|
||||
let (notifications_tx, notifications_rx) = mpsc::channel(1);
|
||||
|
||||
// Fill the capacity-1 channel.
|
||||
notifications_tx.send(post_backfill_notification.clone()).await?;
|
||||
|
||||
// Confirm the channel is full — this is the precondition that causes the
|
||||
// stall in production: the ExExManager's PollSender would block here.
|
||||
assert!(
|
||||
notifications_tx.try_send(probe_notification.clone()).is_err(),
|
||||
"channel should be full before backfill poll"
|
||||
);
|
||||
|
||||
let mut notifications = ExExNotificationsWithoutHead::new(
|
||||
node_head,
|
||||
provider,
|
||||
EthEvmConfig::mainnet(),
|
||||
notifications_rx,
|
||||
wal.handle(),
|
||||
)
|
||||
.with_head(exex_head);
|
||||
|
||||
// Poll once — this returns the backfill result for block 1. Crucially,
|
||||
// the drain loop in poll_next runs in this same call, consuming the
|
||||
// notification from the channel and buffering it.
|
||||
let backfill_result = notifications.next().await.transpose()?;
|
||||
assert_eq!(
|
||||
backfill_result,
|
||||
Some(ExExNotification::ChainCommitted {
|
||||
new: Arc::new(
|
||||
BackfillJobFactory::new(
|
||||
notifications.evm_config.clone(),
|
||||
notifications.provider.clone()
|
||||
)
|
||||
.backfill(1..=1)
|
||||
.next()
|
||||
.ok_or_eyre("failed to backfill")??
|
||||
)
|
||||
})
|
||||
);
|
||||
|
||||
// KEY ASSERTION: the channel was drained during the backfill poll above.
|
||||
// Without the drain loop this try_send fails because the original
|
||||
// notification is still occupying the capacity-1 channel.
|
||||
assert!(
|
||||
notifications_tx.try_send(probe_notification.clone()).is_ok(),
|
||||
"channel should have been drained during backfill poll"
|
||||
);
|
||||
|
||||
// The first buffered notification (block 2) was drained from the channel
|
||||
// during backfill and is delivered now.
|
||||
let buffered = notifications.next().await.transpose()?;
|
||||
assert_eq!(buffered, Some(post_backfill_notification));
|
||||
|
||||
// The probe notification (block 3) that we just sent is delivered next.
|
||||
let probe = notifications.next().await.transpose()?;
|
||||
assert_eq!(probe, Some(probe_notification));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
27
crates/net/eth-wire-types/src/block_access_lists.rs
Normal file
27
crates/net/eth-wire-types/src/block_access_lists.rs
Normal file
@@ -0,0 +1,27 @@
|
||||
//! Implements the `GetBlockAccessLists` and `BlockAccessLists` message types.
|
||||
|
||||
use alloc::vec::Vec;
|
||||
use alloy_primitives::{Bytes, B256};
|
||||
use alloy_rlp::{RlpDecodableWrapper, RlpEncodableWrapper};
|
||||
use reth_codecs_derive::add_arbitrary_tests;
|
||||
|
||||
/// A request for block access lists from the given block hashes.
|
||||
#[derive(Clone, Debug, PartialEq, Eq, RlpEncodableWrapper, RlpDecodableWrapper, Default)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
#[cfg_attr(any(test, feature = "arbitrary"), derive(arbitrary::Arbitrary))]
|
||||
#[add_arbitrary_tests(rlp)]
|
||||
pub struct GetBlockAccessLists(
|
||||
/// The block hashes to request block access lists for.
|
||||
pub Vec<B256>,
|
||||
);
|
||||
|
||||
/// Response for [`GetBlockAccessLists`] containing one BAL per requested block hash.
|
||||
#[derive(Clone, Debug, PartialEq, Eq, RlpEncodableWrapper, RlpDecodableWrapper, Default)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
#[cfg_attr(any(test, feature = "arbitrary"), derive(arbitrary::Arbitrary))]
|
||||
#[add_arbitrary_tests(rlp)]
|
||||
pub struct BlockAccessLists(
|
||||
/// The requested block access lists as opaque bytes. Unavailable entries are represented by
|
||||
/// empty byte slices.
|
||||
pub Vec<Bytes>,
|
||||
);
|
||||
@@ -169,7 +169,10 @@ impl NewPooledTransactionHashes {
|
||||
matches!(version, EthVersion::Eth67 | EthVersion::Eth66)
|
||||
}
|
||||
Self::Eth68(_) => {
|
||||
matches!(version, EthVersion::Eth68 | EthVersion::Eth69 | EthVersion::Eth70)
|
||||
matches!(
|
||||
version,
|
||||
EthVersion::Eth68 | EthVersion::Eth69 | EthVersion::Eth70 | EthVersion::Eth71
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -110,6 +110,11 @@ impl Capability {
|
||||
Self::eth(EthVersion::Eth70)
|
||||
}
|
||||
|
||||
/// Returns the [`EthVersion::Eth71`] capability.
|
||||
pub const fn eth_71() -> Self {
|
||||
Self::eth(EthVersion::Eth71)
|
||||
}
|
||||
|
||||
/// Whether this is eth v66 protocol.
|
||||
#[inline]
|
||||
pub fn is_eth_v66(&self) -> bool {
|
||||
@@ -140,6 +145,12 @@ impl Capability {
|
||||
self.name == "eth" && self.version == 70
|
||||
}
|
||||
|
||||
/// Whether this is eth v71.
|
||||
#[inline]
|
||||
pub fn is_eth_v71(&self) -> bool {
|
||||
self.name == "eth" && self.version == 71
|
||||
}
|
||||
|
||||
/// Whether this is any eth version.
|
||||
#[inline]
|
||||
pub fn is_eth(&self) -> bool {
|
||||
@@ -147,7 +158,8 @@ impl Capability {
|
||||
self.is_eth_v67() ||
|
||||
self.is_eth_v68() ||
|
||||
self.is_eth_v69() ||
|
||||
self.is_eth_v70()
|
||||
self.is_eth_v70() ||
|
||||
self.is_eth_v71()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -167,7 +179,7 @@ impl From<EthVersion> for Capability {
|
||||
#[cfg(any(test, feature = "arbitrary"))]
|
||||
impl<'a> arbitrary::Arbitrary<'a> for Capability {
|
||||
fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result<Self> {
|
||||
let version = u.int_in_range(66..=70)?; // Valid eth protocol versions are 66-70
|
||||
let version = u.int_in_range(66..=71)?; // Valid eth protocol versions are 66-71
|
||||
// Only generate valid eth protocol name for now since it's the only supported protocol
|
||||
Ok(Self::new_static("eth", version))
|
||||
}
|
||||
@@ -183,6 +195,7 @@ pub struct Capabilities {
|
||||
eth_68: bool,
|
||||
eth_69: bool,
|
||||
eth_70: bool,
|
||||
eth_71: bool,
|
||||
}
|
||||
|
||||
impl Capabilities {
|
||||
@@ -194,6 +207,7 @@ impl Capabilities {
|
||||
eth_68: value.iter().any(Capability::is_eth_v68),
|
||||
eth_69: value.iter().any(Capability::is_eth_v69),
|
||||
eth_70: value.iter().any(Capability::is_eth_v70),
|
||||
eth_71: value.iter().any(Capability::is_eth_v71),
|
||||
inner: value,
|
||||
}
|
||||
}
|
||||
@@ -212,7 +226,7 @@ impl Capabilities {
|
||||
/// Whether the peer supports `eth` sub-protocol.
|
||||
#[inline]
|
||||
pub const fn supports_eth(&self) -> bool {
|
||||
self.eth_70 || self.eth_69 || self.eth_68 || self.eth_67 || self.eth_66
|
||||
self.eth_71 || self.eth_70 || self.eth_69 || self.eth_68 || self.eth_67 || self.eth_66
|
||||
}
|
||||
|
||||
/// Whether this peer supports eth v66 protocol.
|
||||
@@ -244,6 +258,12 @@ impl Capabilities {
|
||||
pub const fn supports_eth_v70(&self) -> bool {
|
||||
self.eth_70
|
||||
}
|
||||
|
||||
/// Whether this peer supports eth v71 protocol.
|
||||
#[inline]
|
||||
pub const fn supports_eth_v71(&self) -> bool {
|
||||
self.eth_71
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Vec<Capability>> for Capabilities {
|
||||
@@ -268,6 +288,7 @@ impl Decodable for Capabilities {
|
||||
eth_68: inner.iter().any(Capability::is_eth_v68),
|
||||
eth_69: inner.iter().any(Capability::is_eth_v69),
|
||||
eth_70: inner.iter().any(Capability::is_eth_v70),
|
||||
eth_71: inner.iter().any(Capability::is_eth_v71),
|
||||
inner,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -38,6 +38,9 @@ pub use state::*;
|
||||
pub mod receipts;
|
||||
pub use receipts::*;
|
||||
|
||||
pub mod block_access_lists;
|
||||
pub use block_access_lists::*;
|
||||
|
||||
pub mod disconnect_reason;
|
||||
pub use disconnect_reason::*;
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
//! Implements Ethereum wire protocol for versions 66 through 70.
|
||||
//! Implements Ethereum wire protocol for versions 66 through 71.
|
||||
//! Defines structs/enums for messages, request-response pairs, and broadcasts.
|
||||
//! Handles compatibility with [`EthVersion`].
|
||||
//!
|
||||
@@ -7,10 +7,10 @@
|
||||
//! Reference: [Ethereum Wire Protocol](https://github.com/ethereum/devp2p/blob/master/caps/eth.md).
|
||||
|
||||
use super::{
|
||||
broadcast::NewBlockHashes, BlockBodies, BlockHeaders, GetBlockBodies, GetBlockHeaders,
|
||||
GetNodeData, GetPooledTransactions, GetReceipts, GetReceipts70, NewPooledTransactionHashes66,
|
||||
NewPooledTransactionHashes68, NodeData, PooledTransactions, Receipts, Status, StatusEth69,
|
||||
Transactions,
|
||||
broadcast::NewBlockHashes, BlockAccessLists, BlockBodies, BlockHeaders, GetBlockAccessLists,
|
||||
GetBlockBodies, GetBlockHeaders, GetNodeData, GetPooledTransactions, GetReceipts,
|
||||
GetReceipts70, NewPooledTransactionHashes66, NewPooledTransactionHashes68, NodeData,
|
||||
PooledTransactions, Receipts, Status, StatusEth69, Transactions,
|
||||
};
|
||||
use crate::{
|
||||
status::StatusMessage, BlockRangeUpdate, EthNetworkPrimitives, EthVersion, NetworkPrimitives,
|
||||
@@ -168,6 +168,18 @@ impl<N: NetworkPrimitives> ProtocolMessage<N> {
|
||||
}
|
||||
EthMessage::BlockRangeUpdate(BlockRangeUpdate::decode(buf)?)
|
||||
}
|
||||
EthMessageID::GetBlockAccessLists => {
|
||||
if version < EthVersion::Eth71 {
|
||||
return Err(MessageError::Invalid(version, EthMessageID::GetBlockAccessLists))
|
||||
}
|
||||
EthMessage::GetBlockAccessLists(RequestPair::decode(buf)?)
|
||||
}
|
||||
EthMessageID::BlockAccessLists => {
|
||||
if version < EthVersion::Eth71 {
|
||||
return Err(MessageError::Invalid(version, EthMessageID::BlockAccessLists))
|
||||
}
|
||||
EthMessage::BlockAccessLists(RequestPair::decode(buf)?)
|
||||
}
|
||||
EthMessageID::Other(_) => {
|
||||
let raw_payload = Bytes::copy_from_slice(buf);
|
||||
buf.advance(raw_payload.len());
|
||||
@@ -250,6 +262,8 @@ impl<N: NetworkPrimitives> From<EthBroadcastMessage<N>> for ProtocolBroadcastMes
|
||||
///
|
||||
/// The `eth/70` (EIP-7975) keeps the eth/69 status format and introduces partial receipts.
|
||||
/// requests/responses.
|
||||
///
|
||||
/// The `eth/71` draft extends eth/70 with block access list request/response messages.
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
pub enum EthMessage<N: NetworkPrimitives = EthNetworkPrimitives> {
|
||||
@@ -310,6 +324,8 @@ pub enum EthMessage<N: NetworkPrimitives = EthNetworkPrimitives> {
|
||||
/// `GetReceipts` in EIP-7975 inlines the request id. The type still wraps
|
||||
/// a [`RequestPair`], but with a custom inline encoding.
|
||||
GetReceipts70(RequestPair<GetReceipts70>),
|
||||
/// Represents a `GetBlockAccessLists` request-response pair for eth/71.
|
||||
GetBlockAccessLists(RequestPair<GetBlockAccessLists>),
|
||||
/// Represents a Receipts request-response pair.
|
||||
#[cfg_attr(
|
||||
feature = "serde",
|
||||
@@ -332,6 +348,8 @@ pub enum EthMessage<N: NetworkPrimitives = EthNetworkPrimitives> {
|
||||
/// request id. The type still wraps a [`RequestPair`], but with a custom
|
||||
/// inline encoding.
|
||||
Receipts70(RequestPair<Receipts70<N::Receipt>>),
|
||||
/// Represents a `BlockAccessLists` request-response pair for eth/71.
|
||||
BlockAccessLists(RequestPair<BlockAccessLists>),
|
||||
/// Represents a `BlockRangeUpdate` message broadcast to the network.
|
||||
#[cfg_attr(
|
||||
feature = "serde",
|
||||
@@ -364,6 +382,8 @@ impl<N: NetworkPrimitives> EthMessage<N> {
|
||||
Self::GetReceipts(_) | Self::GetReceipts70(_) => EthMessageID::GetReceipts,
|
||||
Self::Receipts(_) | Self::Receipts69(_) | Self::Receipts70(_) => EthMessageID::Receipts,
|
||||
Self::BlockRangeUpdate(_) => EthMessageID::BlockRangeUpdate,
|
||||
Self::GetBlockAccessLists(_) => EthMessageID::GetBlockAccessLists,
|
||||
Self::BlockAccessLists(_) => EthMessageID::BlockAccessLists,
|
||||
Self::Other(msg) => EthMessageID::Other(msg.id as u8),
|
||||
}
|
||||
}
|
||||
@@ -376,6 +396,7 @@ impl<N: NetworkPrimitives> EthMessage<N> {
|
||||
Self::GetBlockHeaders(_) |
|
||||
Self::GetReceipts(_) |
|
||||
Self::GetReceipts70(_) |
|
||||
Self::GetBlockAccessLists(_) |
|
||||
Self::GetPooledTransactions(_) |
|
||||
Self::GetNodeData(_)
|
||||
)
|
||||
@@ -389,6 +410,7 @@ impl<N: NetworkPrimitives> EthMessage<N> {
|
||||
Self::Receipts(_) |
|
||||
Self::Receipts69(_) |
|
||||
Self::Receipts70(_) |
|
||||
Self::BlockAccessLists(_) |
|
||||
Self::BlockHeaders(_) |
|
||||
Self::BlockBodies(_) |
|
||||
Self::NodeData(_)
|
||||
@@ -443,9 +465,11 @@ impl<N: NetworkPrimitives> Encodable for EthMessage<N> {
|
||||
Self::NodeData(data) => data.encode(out),
|
||||
Self::GetReceipts(request) => request.encode(out),
|
||||
Self::GetReceipts70(request) => request.encode(out),
|
||||
Self::GetBlockAccessLists(request) => request.encode(out),
|
||||
Self::Receipts(receipts) => receipts.encode(out),
|
||||
Self::Receipts69(receipt69) => receipt69.encode(out),
|
||||
Self::Receipts70(receipt70) => receipt70.encode(out),
|
||||
Self::BlockAccessLists(block_access_lists) => block_access_lists.encode(out),
|
||||
Self::BlockRangeUpdate(block_range_update) => block_range_update.encode(out),
|
||||
Self::Other(unknown) => out.put_slice(&unknown.payload),
|
||||
}
|
||||
@@ -468,9 +492,11 @@ impl<N: NetworkPrimitives> Encodable for EthMessage<N> {
|
||||
Self::NodeData(data) => data.length(),
|
||||
Self::GetReceipts(request) => request.length(),
|
||||
Self::GetReceipts70(request) => request.length(),
|
||||
Self::GetBlockAccessLists(request) => request.length(),
|
||||
Self::Receipts(receipts) => receipts.length(),
|
||||
Self::Receipts69(receipt69) => receipt69.length(),
|
||||
Self::Receipts70(receipt70) => receipt70.length(),
|
||||
Self::BlockAccessLists(block_access_lists) => block_access_lists.length(),
|
||||
Self::BlockRangeUpdate(block_range_update) => block_range_update.length(),
|
||||
Self::Other(unknown) => unknown.length(),
|
||||
}
|
||||
@@ -559,6 +585,14 @@ pub enum EthMessageID {
|
||||
///
|
||||
/// Introduced in Eth69
|
||||
BlockRangeUpdate = 0x11,
|
||||
/// Requests block access lists.
|
||||
///
|
||||
/// Introduced in Eth71
|
||||
GetBlockAccessLists = 0x12,
|
||||
/// Represents block access lists.
|
||||
///
|
||||
/// Introduced in Eth71
|
||||
BlockAccessLists = 0x13,
|
||||
/// Represents unknown message types.
|
||||
Other(u8),
|
||||
}
|
||||
@@ -583,13 +617,17 @@ impl EthMessageID {
|
||||
Self::GetReceipts => 0x0f,
|
||||
Self::Receipts => 0x10,
|
||||
Self::BlockRangeUpdate => 0x11,
|
||||
Self::GetBlockAccessLists => 0x12,
|
||||
Self::BlockAccessLists => 0x13,
|
||||
Self::Other(value) => *value, // Return the stored `u8`
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the max value for the given version.
|
||||
pub const fn max(version: EthVersion) -> u8 {
|
||||
if version as u8 >= EthVersion::Eth69 as u8 {
|
||||
if version.is_eth71() {
|
||||
Self::BlockAccessLists.to_u8()
|
||||
} else if version.is_eth69_or_newer() {
|
||||
Self::BlockRangeUpdate.to_u8()
|
||||
} else {
|
||||
Self::Receipts.to_u8()
|
||||
@@ -634,6 +672,8 @@ impl Decodable for EthMessageID {
|
||||
0x0f => Self::GetReceipts,
|
||||
0x10 => Self::Receipts,
|
||||
0x11 => Self::BlockRangeUpdate,
|
||||
0x12 => Self::GetBlockAccessLists,
|
||||
0x13 => Self::BlockAccessLists,
|
||||
unknown => Self::Other(*unknown),
|
||||
};
|
||||
buf.advance(1);
|
||||
@@ -662,6 +702,8 @@ impl TryFrom<usize> for EthMessageID {
|
||||
0x0f => Ok(Self::GetReceipts),
|
||||
0x10 => Ok(Self::Receipts),
|
||||
0x11 => Ok(Self::BlockRangeUpdate),
|
||||
0x12 => Ok(Self::GetBlockAccessLists),
|
||||
0x13 => Ok(Self::BlockAccessLists),
|
||||
_ => Err("Invalid message ID"),
|
||||
}
|
||||
}
|
||||
@@ -742,8 +784,9 @@ where
|
||||
mod tests {
|
||||
use super::MessageError;
|
||||
use crate::{
|
||||
message::RequestPair, EthMessage, EthMessageID, EthNetworkPrimitives, EthVersion,
|
||||
GetNodeData, NodeData, ProtocolMessage, RawCapabilityMessage,
|
||||
message::RequestPair, BlockAccessLists, EthMessage, EthMessageID, EthNetworkPrimitives,
|
||||
EthVersion, GetBlockAccessLists, GetNodeData, NodeData, ProtocolMessage,
|
||||
RawCapabilityMessage,
|
||||
};
|
||||
use alloy_primitives::hex;
|
||||
use alloy_rlp::{Decodable, Encodable, Error};
|
||||
@@ -784,6 +827,60 @@ mod tests {
|
||||
assert!(matches!(msg, Err(MessageError::Invalid(..))));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_bal_message_version_gating() {
|
||||
let get_block_access_lists =
|
||||
EthMessage::<EthNetworkPrimitives>::GetBlockAccessLists(RequestPair {
|
||||
request_id: 1337,
|
||||
message: GetBlockAccessLists(vec![]),
|
||||
});
|
||||
let buf = encode(ProtocolMessage {
|
||||
message_type: EthMessageID::GetBlockAccessLists,
|
||||
message: get_block_access_lists,
|
||||
});
|
||||
let msg = ProtocolMessage::<EthNetworkPrimitives>::decode_message(
|
||||
EthVersion::Eth70,
|
||||
&mut &buf[..],
|
||||
);
|
||||
assert!(matches!(
|
||||
msg,
|
||||
Err(MessageError::Invalid(EthVersion::Eth70, EthMessageID::GetBlockAccessLists))
|
||||
));
|
||||
|
||||
let block_access_lists =
|
||||
EthMessage::<EthNetworkPrimitives>::BlockAccessLists(RequestPair {
|
||||
request_id: 1337,
|
||||
message: BlockAccessLists(vec![]),
|
||||
});
|
||||
let buf = encode(ProtocolMessage {
|
||||
message_type: EthMessageID::BlockAccessLists,
|
||||
message: block_access_lists,
|
||||
});
|
||||
let msg = ProtocolMessage::<EthNetworkPrimitives>::decode_message(
|
||||
EthVersion::Eth70,
|
||||
&mut &buf[..],
|
||||
);
|
||||
assert!(matches!(
|
||||
msg,
|
||||
Err(MessageError::Invalid(EthVersion::Eth70, EthMessageID::BlockAccessLists))
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_bal_message_eth71_roundtrip() {
|
||||
let msg = ProtocolMessage::from(EthMessage::<EthNetworkPrimitives>::GetBlockAccessLists(
|
||||
RequestPair { request_id: 42, message: GetBlockAccessLists(vec![]) },
|
||||
));
|
||||
let encoded = encode(msg.clone());
|
||||
let decoded = ProtocolMessage::<EthNetworkPrimitives>::decode_message(
|
||||
EthVersion::Eth71,
|
||||
&mut &encoded[..],
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(decoded, msg);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn request_pair_encode() {
|
||||
let request_pair = RequestPair { request_id: 1337, message: vec![5u8] };
|
||||
|
||||
@@ -29,6 +29,8 @@ pub enum EthVersion {
|
||||
Eth69 = 69,
|
||||
/// The `eth` protocol version 70.
|
||||
Eth70 = 70,
|
||||
/// The `eth` protocol version 71.
|
||||
Eth71 = 71,
|
||||
}
|
||||
|
||||
impl EthVersion {
|
||||
@@ -62,9 +64,19 @@ impl EthVersion {
|
||||
pub const fn is_eth70(&self) -> bool {
|
||||
matches!(self, Self::Eth70)
|
||||
}
|
||||
|
||||
/// Returns true if the version is eth/71
|
||||
pub const fn is_eth71(&self) -> bool {
|
||||
matches!(self, Self::Eth71)
|
||||
}
|
||||
|
||||
/// Returns true if the version is eth/69 or newer.
|
||||
pub const fn is_eth69_or_newer(&self) -> bool {
|
||||
matches!(self, Self::Eth69 | Self::Eth70 | Self::Eth71)
|
||||
}
|
||||
}
|
||||
|
||||
/// RLP encodes `EthVersion` as a single byte (66-69).
|
||||
/// RLP encodes `EthVersion` as a single byte (66-71).
|
||||
impl Encodable for EthVersion {
|
||||
fn encode(&self, out: &mut dyn BufMut) {
|
||||
(*self as u8).encode(out)
|
||||
@@ -76,7 +88,7 @@ impl Encodable for EthVersion {
|
||||
}
|
||||
|
||||
/// RLP decodes a single byte into `EthVersion`.
|
||||
/// Returns error if byte is not a valid version (66-69).
|
||||
/// Returns error if byte is not a valid version (66-71).
|
||||
impl Decodable for EthVersion {
|
||||
fn decode(buf: &mut &[u8]) -> alloy_rlp::Result<Self> {
|
||||
let version = u8::decode(buf)?;
|
||||
@@ -104,6 +116,7 @@ impl TryFrom<&str> for EthVersion {
|
||||
"68" => Ok(Self::Eth68),
|
||||
"69" => Ok(Self::Eth69),
|
||||
"70" => Ok(Self::Eth70),
|
||||
"71" => Ok(Self::Eth71),
|
||||
_ => Err(ParseVersionError(s.to_string())),
|
||||
}
|
||||
}
|
||||
@@ -129,6 +142,7 @@ impl TryFrom<u8> for EthVersion {
|
||||
68 => Ok(Self::Eth68),
|
||||
69 => Ok(Self::Eth69),
|
||||
70 => Ok(Self::Eth70),
|
||||
71 => Ok(Self::Eth71),
|
||||
_ => Err(ParseVersionError(u.to_string())),
|
||||
}
|
||||
}
|
||||
@@ -159,6 +173,7 @@ impl From<EthVersion> for &'static str {
|
||||
EthVersion::Eth68 => "68",
|
||||
EthVersion::Eth69 => "69",
|
||||
EthVersion::Eth70 => "70",
|
||||
EthVersion::Eth71 => "71",
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -216,6 +231,7 @@ mod tests {
|
||||
assert_eq!(EthVersion::Eth68, EthVersion::try_from("68").unwrap());
|
||||
assert_eq!(EthVersion::Eth69, EthVersion::try_from("69").unwrap());
|
||||
assert_eq!(EthVersion::Eth70, EthVersion::try_from("70").unwrap());
|
||||
assert_eq!(EthVersion::Eth71, EthVersion::try_from("71").unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -225,6 +241,7 @@ mod tests {
|
||||
assert_eq!(EthVersion::Eth68, "68".parse().unwrap());
|
||||
assert_eq!(EthVersion::Eth69, "69".parse().unwrap());
|
||||
assert_eq!(EthVersion::Eth70, "70".parse().unwrap());
|
||||
assert_eq!(EthVersion::Eth71, "71".parse().unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -235,6 +252,7 @@ mod tests {
|
||||
EthVersion::Eth68,
|
||||
EthVersion::Eth69,
|
||||
EthVersion::Eth70,
|
||||
EthVersion::Eth71,
|
||||
];
|
||||
|
||||
for version in versions {
|
||||
@@ -253,6 +271,7 @@ mod tests {
|
||||
(68_u8, Ok(EthVersion::Eth68)),
|
||||
(69_u8, Ok(EthVersion::Eth69)),
|
||||
(70_u8, Ok(EthVersion::Eth70)),
|
||||
(71_u8, Ok(EthVersion::Eth71)),
|
||||
(65_u8, Err(RlpError::Custom("invalid eth version"))),
|
||||
];
|
||||
|
||||
|
||||
@@ -294,7 +294,8 @@ mod tests {
|
||||
use alloy_primitives::B256;
|
||||
use alloy_rlp::Encodable;
|
||||
use reth_eth_wire_types::{
|
||||
message::RequestPair, GetAccountRangeMessage, GetBlockHeaders, HeadersDirection,
|
||||
message::RequestPair, GetAccountRangeMessage, GetBlockAccessLists, GetBlockHeaders,
|
||||
HeadersDirection,
|
||||
};
|
||||
|
||||
// Helper to create eth message and its bytes
|
||||
@@ -419,4 +420,40 @@ mod tests {
|
||||
let snap_boundary_result = inner.decode_message(snap_boundary_bytes);
|
||||
assert!(snap_boundary_result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_eth70_message_id_0x12_is_snap() {
|
||||
let inner = EthSnapStreamInner::<EthNetworkPrimitives>::new(EthVersion::Eth70);
|
||||
let snap_msg = SnapProtocolMessage::GetAccountRange(GetAccountRangeMessage {
|
||||
request_id: 1,
|
||||
root_hash: B256::default(),
|
||||
starting_hash: B256::default(),
|
||||
limit_hash: B256::default(),
|
||||
response_bytes: 1000,
|
||||
});
|
||||
|
||||
let encoded = inner.encode_snap_message(snap_msg);
|
||||
assert_eq!(encoded[0], EthMessageID::message_count(EthVersion::Eth70));
|
||||
|
||||
let decoded = inner.decode_message(BytesMut::from(&encoded[..])).unwrap();
|
||||
assert!(matches!(decoded, EthSnapMessage::Snap(_)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_eth71_message_id_0x12_is_eth() {
|
||||
let inner = EthSnapStreamInner::<EthNetworkPrimitives>::new(EthVersion::Eth71);
|
||||
let eth_msg = EthMessage::<EthNetworkPrimitives>::GetBlockAccessLists(RequestPair {
|
||||
request_id: 1,
|
||||
message: GetBlockAccessLists(vec![B256::ZERO]),
|
||||
});
|
||||
let protocol_msg = ProtocolMessage::from(eth_msg.clone());
|
||||
let mut buf = Vec::new();
|
||||
protocol_msg.encode(&mut buf);
|
||||
|
||||
let decoded = inner.decode_message(BytesMut::from(&buf[..])).unwrap();
|
||||
let EthSnapMessage::Eth(decoded_eth) = decoded else {
|
||||
panic!("expected eth message");
|
||||
};
|
||||
assert_eq!(decoded_eth, eth_msg);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -84,5 +84,7 @@ mod tests {
|
||||
assert_eq!(Protocol::eth(EthVersion::Eth67).messages(), 17);
|
||||
assert_eq!(Protocol::eth(EthVersion::Eth68).messages(), 17);
|
||||
assert_eq!(Protocol::eth(EthVersion::Eth69).messages(), 18);
|
||||
assert_eq!(Protocol::eth(EthVersion::Eth70).messages(), 18);
|
||||
assert_eq!(Protocol::eth(EthVersion::Eth71).messages(), 20);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
//! API related to listening for network events.
|
||||
|
||||
use reth_eth_wire_types::{
|
||||
message::RequestPair, BlockBodies, BlockHeaders, Capabilities, DisconnectReason, EthMessage,
|
||||
EthNetworkPrimitives, EthVersion, GetBlockBodies, GetBlockHeaders, GetNodeData,
|
||||
GetPooledTransactions, GetReceipts, GetReceipts70, NetworkPrimitives, NodeData,
|
||||
PooledTransactions, Receipts, Receipts69, Receipts70, UnifiedStatus,
|
||||
message::RequestPair, BlockAccessLists, BlockBodies, BlockHeaders, Capabilities,
|
||||
DisconnectReason, EthMessage, EthNetworkPrimitives, EthVersion, GetBlockAccessLists,
|
||||
GetBlockBodies, GetBlockHeaders, GetNodeData, GetPooledTransactions, GetReceipts,
|
||||
GetReceipts70, NetworkPrimitives, NodeData, PooledTransactions, Receipts, Receipts69,
|
||||
Receipts70, UnifiedStatus,
|
||||
};
|
||||
use reth_ethereum_forks::ForkId;
|
||||
use reth_network_p2p::error::{RequestError, RequestResult};
|
||||
@@ -252,6 +253,15 @@ pub enum PeerRequest<N: NetworkPrimitives = EthNetworkPrimitives> {
|
||||
/// The channel to send the response for receipts.
|
||||
response: oneshot::Sender<RequestResult<Receipts70<N::Receipt>>>,
|
||||
},
|
||||
/// Requests block access lists from the peer.
|
||||
///
|
||||
/// The response should be sent through the channel.
|
||||
GetBlockAccessLists {
|
||||
/// The request for block access lists.
|
||||
request: GetBlockAccessLists,
|
||||
/// The channel to send the response for block access lists.
|
||||
response: oneshot::Sender<RequestResult<BlockAccessLists>>,
|
||||
},
|
||||
}
|
||||
|
||||
// === impl PeerRequest ===
|
||||
@@ -272,9 +282,19 @@ impl<N: NetworkPrimitives> PeerRequest<N> {
|
||||
Self::GetReceipts { response, .. } => response.send(Err(err)).ok(),
|
||||
Self::GetReceipts69 { response, .. } => response.send(Err(err)).ok(),
|
||||
Self::GetReceipts70 { response, .. } => response.send(Err(err)).ok(),
|
||||
Self::GetBlockAccessLists { response, .. } => response.send(Err(err)).ok(),
|
||||
};
|
||||
}
|
||||
|
||||
/// Returns true if this request is supported for the negotiated eth protocol version.
|
||||
#[inline]
|
||||
pub fn is_supported_by_eth_version(&self, version: EthVersion) -> bool {
|
||||
match self {
|
||||
Self::GetBlockAccessLists { .. } => version >= EthVersion::Eth71,
|
||||
_ => true,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the [`EthMessage`] for this type
|
||||
pub fn create_request_message(&self, request_id: u64) -> EthMessage<N> {
|
||||
match self {
|
||||
@@ -299,6 +319,12 @@ impl<N: NetworkPrimitives> PeerRequest<N> {
|
||||
Self::GetReceipts70 { request, .. } => {
|
||||
EthMessage::GetReceipts70(RequestPair { request_id, message: request.clone() })
|
||||
}
|
||||
Self::GetBlockAccessLists { request, .. } => {
|
||||
EthMessage::GetBlockAccessLists(RequestPair {
|
||||
request_id,
|
||||
message: request.clone(),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -349,3 +375,18 @@ impl<R> fmt::Debug for PeerRequestSender<R> {
|
||||
f.debug_struct("PeerRequestSender").field("peer_id", &self.peer_id).finish_non_exhaustive()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_get_block_access_lists_version_support() {
|
||||
let (tx, _rx) = oneshot::channel();
|
||||
let req: PeerRequest<EthNetworkPrimitives> =
|
||||
PeerRequest::GetBlockAccessLists { request: GetBlockAccessLists(vec![]), response: tx };
|
||||
|
||||
assert!(!req.is_supported_by_eth_version(EthVersion::Eth70));
|
||||
assert!(req.is_supported_by_eth_version(EthVersion::Eth71));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,12 +6,13 @@ use crate::{
|
||||
};
|
||||
use alloy_consensus::{BlockHeader, ReceiptWithBloom};
|
||||
use alloy_eips::BlockHashOrNumber;
|
||||
use alloy_primitives::Bytes;
|
||||
use alloy_rlp::Encodable;
|
||||
use futures::StreamExt;
|
||||
use reth_eth_wire::{
|
||||
BlockBodies, BlockHeaders, EthNetworkPrimitives, GetBlockBodies, GetBlockHeaders, GetNodeData,
|
||||
GetReceipts, GetReceipts70, HeadersDirection, NetworkPrimitives, NodeData, Receipts,
|
||||
Receipts69, Receipts70,
|
||||
BlockAccessLists, BlockBodies, BlockHeaders, EthNetworkPrimitives, GetBlockAccessLists,
|
||||
GetBlockBodies, GetBlockHeaders, GetNodeData, GetReceipts, GetReceipts70, HeadersDirection,
|
||||
NetworkPrimitives, NodeData, Receipts, Receipts69, Receipts70,
|
||||
};
|
||||
use reth_network_api::test_utils::PeersHandle;
|
||||
use reth_network_p2p::error::RequestResult;
|
||||
@@ -281,6 +282,19 @@ where
|
||||
let _ = response.send(Ok(Receipts70 { last_block_incomplete, receipts }));
|
||||
}
|
||||
|
||||
/// Handles [`GetBlockAccessLists`] queries.
|
||||
///
|
||||
/// For now this returns one empty BAL per requested hash.
|
||||
fn on_block_access_lists_request(
|
||||
&self,
|
||||
_peer_id: PeerId,
|
||||
request: GetBlockAccessLists,
|
||||
response: oneshot::Sender<RequestResult<BlockAccessLists>>,
|
||||
) {
|
||||
let access_lists = request.0.into_iter().map(|_| Bytes::new()).collect();
|
||||
let _ = response.send(Ok(BlockAccessLists(access_lists)));
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn get_receipts_response<T, F>(&self, request: GetReceipts, transform_fn: F) -> Vec<Vec<T>>
|
||||
where
|
||||
@@ -352,6 +366,9 @@ where
|
||||
IncomingEthRequest::GetReceipts70 { peer_id, request, response } => {
|
||||
this.on_receipts70_request(peer_id, request, response)
|
||||
}
|
||||
IncomingEthRequest::GetBlockAccessLists { peer_id, request, response } => {
|
||||
this.on_block_access_lists_request(peer_id, request, response)
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
@@ -437,4 +454,15 @@ pub enum IncomingEthRequest<N: NetworkPrimitives = EthNetworkPrimitives> {
|
||||
/// The channel sender for the response containing Receipts70.
|
||||
response: oneshot::Sender<RequestResult<Receipts70<N::Receipt>>>,
|
||||
},
|
||||
/// Request Block Access Lists from the peer.
|
||||
///
|
||||
/// The response should be sent through the channel.
|
||||
GetBlockAccessLists {
|
||||
/// The ID of the peer to request block access lists from.
|
||||
peer_id: PeerId,
|
||||
/// The requested block hashes.
|
||||
request: GetBlockAccessLists,
|
||||
/// The channel sender for the response containing block access lists.
|
||||
response: oneshot::Sender<RequestResult<BlockAccessLists>>,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -551,6 +551,13 @@ impl<N: NetworkPrimitives> NetworkManager<N> {
|
||||
response,
|
||||
})
|
||||
}
|
||||
PeerRequest::GetBlockAccessLists { request, response } => {
|
||||
self.delegate_eth_request(IncomingEthRequest::GetBlockAccessLists {
|
||||
peer_id,
|
||||
request,
|
||||
response,
|
||||
})
|
||||
}
|
||||
PeerRequest::GetPooledTransactions { request, response } => {
|
||||
self.notify_tx_manager(NetworkTransactionEvent::GetPooledTransactions {
|
||||
peer_id,
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
//! An `RLPx` stream is multiplexed via the prepended message-id of a framed message.
|
||||
//! Capabilities are exchanged via the `RLPx` `Hello` message as pairs of `(id, version)`, <https://github.com/ethereum/devp2p/blob/master/rlpx.md#capability-messaging>
|
||||
|
||||
use crate::types::{Receipts69, Receipts70};
|
||||
use crate::types::{BlockAccessLists, Receipts69, Receipts70};
|
||||
use alloy_consensus::{BlockHeader, ReceiptWithBloom};
|
||||
use alloy_primitives::{Bytes, B256};
|
||||
use futures::FutureExt;
|
||||
@@ -121,6 +121,11 @@ pub enum PeerResponse<N: NetworkPrimitives = EthNetworkPrimitives> {
|
||||
/// The receiver channel for the response to a receipts request.
|
||||
response: oneshot::Receiver<RequestResult<Receipts70<N::Receipt>>>,
|
||||
},
|
||||
/// Represents a response to a request for block access lists.
|
||||
BlockAccessLists {
|
||||
/// The receiver channel for the response to a block access lists request.
|
||||
response: oneshot::Receiver<RequestResult<BlockAccessLists>>,
|
||||
},
|
||||
}
|
||||
|
||||
// === impl PeerResponse ===
|
||||
@@ -160,6 +165,10 @@ impl<N: NetworkPrimitives> PeerResponse<N> {
|
||||
Ok(res) => PeerResponseResult::Receipts70(res),
|
||||
Err(err) => PeerResponseResult::Receipts70(Err(err.into())),
|
||||
},
|
||||
Self::BlockAccessLists { response } => match ready!(response.poll_unpin(cx)) {
|
||||
Ok(res) => PeerResponseResult::BlockAccessLists(res),
|
||||
Err(err) => PeerResponseResult::BlockAccessLists(Err(err.into())),
|
||||
},
|
||||
};
|
||||
Poll::Ready(res)
|
||||
}
|
||||
@@ -182,6 +191,8 @@ pub enum PeerResponseResult<N: NetworkPrimitives = EthNetworkPrimitives> {
|
||||
Receipts69(RequestResult<Vec<Vec<N::Receipt>>>),
|
||||
/// Represents a result containing receipts or an error for eth/70.
|
||||
Receipts70(RequestResult<Receipts70<N::Receipt>>),
|
||||
/// Represents a result containing block access lists or an error.
|
||||
BlockAccessLists(RequestResult<BlockAccessLists>),
|
||||
}
|
||||
|
||||
// === impl PeerResponseResult ===
|
||||
@@ -226,6 +237,13 @@ impl<N: NetworkPrimitives> PeerResponseResult<N> {
|
||||
}
|
||||
Err(err) => Err(err),
|
||||
},
|
||||
Self::BlockAccessLists(resp) => match resp {
|
||||
Ok(res) => {
|
||||
let request = RequestPair { request_id: id, message: res };
|
||||
Ok(EthMessage::BlockAccessLists(request))
|
||||
}
|
||||
Err(err) => Err(err),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -239,6 +257,7 @@ impl<N: NetworkPrimitives> PeerResponseResult<N> {
|
||||
Self::Receipts(res) => res.as_ref().err(),
|
||||
Self::Receipts69(res) => res.as_ref().err(),
|
||||
Self::Receipts70(res) => res.as_ref().err(),
|
||||
Self::BlockAccessLists(res) => res.as_ref().err(),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -282,6 +282,12 @@ impl<N: NetworkPrimitives> ActiveSession<N> {
|
||||
EthMessage::Receipts70(resp) => {
|
||||
on_response!(resp, GetReceipts70)
|
||||
}
|
||||
EthMessage::GetBlockAccessLists(req) => {
|
||||
on_request!(req, BlockAccessLists, GetBlockAccessLists)
|
||||
}
|
||||
EthMessage::BlockAccessLists(resp) => {
|
||||
on_response!(resp, GetBlockAccessLists)
|
||||
}
|
||||
EthMessage::BlockRangeUpdate(msg) => {
|
||||
// Validate that earliest <= latest according to the spec
|
||||
if msg.earliest > msg.latest {
|
||||
@@ -316,9 +322,22 @@ impl<N: NetworkPrimitives> ActiveSession<N> {
|
||||
|
||||
/// Handle an internal peer request that will be sent to the remote.
|
||||
fn on_internal_peer_request(&mut self, request: PeerRequest<N>, deadline: Instant) {
|
||||
let version = self.conn.version();
|
||||
if !Self::is_request_supported_for_version(&request, version) {
|
||||
debug!(
|
||||
target: "net",
|
||||
?request,
|
||||
peer_id=?self.remote_peer_id,
|
||||
?version,
|
||||
"Request not supported for negotiated eth version",
|
||||
);
|
||||
request.send_err_response(RequestError::UnsupportedCapability);
|
||||
return;
|
||||
}
|
||||
|
||||
let request_id = self.next_id();
|
||||
trace!(?request, peer_id=?self.remote_peer_id, ?request_id, "sending request to peer");
|
||||
let msg = request.create_request_message(request_id).map_versioned(self.conn.version());
|
||||
let msg = request.create_request_message(request_id).map_versioned(version);
|
||||
|
||||
self.queued_outgoing.push_back(msg.into());
|
||||
let req = InflightRequest {
|
||||
@@ -329,6 +348,11 @@ impl<N: NetworkPrimitives> ActiveSession<N> {
|
||||
self.inflight_requests.insert(request_id, req);
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn is_request_supported_for_version(request: &PeerRequest<N>, version: EthVersion) -> bool {
|
||||
request.is_supported_by_eth_version(version)
|
||||
}
|
||||
|
||||
/// Handle a message received from the internal network
|
||||
fn on_internal_peer_message(&mut self, msg: PeerMessage<N>) {
|
||||
match msg {
|
||||
@@ -938,9 +962,9 @@ mod tests {
|
||||
use reth_chainspec::MAINNET;
|
||||
use reth_ecies::stream::ECIESStream;
|
||||
use reth_eth_wire::{
|
||||
handshake::EthHandshake, EthNetworkPrimitives, EthStream, GetBlockBodies,
|
||||
HelloMessageWithProtocols, P2PStream, StatusBuilder, UnauthedEthStream, UnauthedP2PStream,
|
||||
UnifiedStatus,
|
||||
handshake::EthHandshake, EthNetworkPrimitives, EthStream, GetBlockAccessLists,
|
||||
GetBlockBodies, HelloMessageWithProtocols, P2PStream, StatusBuilder, UnauthedEthStream,
|
||||
UnauthedP2PStream, UnifiedStatus,
|
||||
};
|
||||
use reth_ethereum_forks::EthereumHardfork;
|
||||
use reth_network_peers::pk2id;
|
||||
@@ -1240,6 +1264,22 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_reject_bal_request_for_eth70() {
|
||||
let (tx, _rx) = oneshot::channel();
|
||||
let request: PeerRequest<EthNetworkPrimitives> =
|
||||
PeerRequest::GetBlockAccessLists { request: GetBlockAccessLists(vec![]), response: tx };
|
||||
|
||||
assert!(!ActiveSession::<EthNetworkPrimitives>::is_request_supported_for_version(
|
||||
&request,
|
||||
EthVersion::Eth70
|
||||
));
|
||||
assert!(ActiveSession::<EthNetworkPrimitives>::is_request_supported_for_version(
|
||||
&request,
|
||||
EthVersion::Eth71
|
||||
));
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn test_keep_alive() {
|
||||
let mut builder = SessionBuilder::default();
|
||||
|
||||
@@ -908,7 +908,7 @@ pub(crate) async fn start_pending_incoming_session<N: NetworkPrimitives>(
|
||||
}
|
||||
|
||||
/// Starts the authentication process for a connection initiated by a remote peer.
|
||||
#[instrument(level = "trace", target = "net::network", skip_all, fields(%remote_addr, peer_id))]
|
||||
#[instrument(level = "trace", target = "net::network", skip_all, fields(%remote_addr, peer_id = ?remote_peer_id))]
|
||||
#[expect(clippy::too_many_arguments)]
|
||||
async fn start_pending_outbound_session<N: NetworkPrimitives>(
|
||||
handshake: Arc<dyn EthRlpxHandshake>,
|
||||
|
||||
@@ -1949,7 +1949,7 @@ impl PooledTransactionsHashesBuilder {
|
||||
fn new(version: EthVersion) -> Self {
|
||||
match version {
|
||||
EthVersion::Eth66 | EthVersion::Eth67 => Self::Eth66(Default::default()),
|
||||
EthVersion::Eth68 | EthVersion::Eth69 | EthVersion::Eth70 => {
|
||||
EthVersion::Eth68 | EthVersion::Eth69 | EthVersion::Eth70 | EthVersion::Eth71 => {
|
||||
Self::Eth68(Default::default())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,7 +24,6 @@ reth-db-common.workspace = true
|
||||
reth-downloaders.workspace = true
|
||||
reth-engine-local.workspace = true
|
||||
reth-engine-primitives.workspace = true
|
||||
reth-engine-service.workspace = true
|
||||
reth-engine-tree.workspace = true
|
||||
reth-engine-util.workspace = true
|
||||
reth-evm.workspace = true
|
||||
@@ -129,4 +128,3 @@ op = [
|
||||
"reth-evm/op",
|
||||
"reth-primitives-traits/op",
|
||||
]
|
||||
pageviz = ["reth-engine-tree/pageviz"]
|
||||
|
||||
@@ -11,10 +11,10 @@ use crate::{
|
||||
use alloy_consensus::BlockHeader;
|
||||
use futures::{stream_select, FutureExt, StreamExt};
|
||||
use reth_chainspec::{EthChainSpec, EthereumHardforks};
|
||||
use reth_engine_service::service::{ChainEvent, EngineService};
|
||||
use reth_engine_tree::{
|
||||
chain::FromOrchestrator,
|
||||
engine::{EngineApiRequest, EngineRequestHandler},
|
||||
chain::{ChainEvent, FromOrchestrator},
|
||||
engine::{EngineApiKind, EngineApiRequest, EngineRequestHandler},
|
||||
launch::build_engine_orchestrator,
|
||||
tree::TreeConfig,
|
||||
};
|
||||
use reth_engine_util::EngineMessageStreamExt;
|
||||
@@ -219,9 +219,15 @@ impl EngineNodeLauncher {
|
||||
// during this run.
|
||||
.maybe_store_messages(node_config.debug.engine_api_store.clone());
|
||||
|
||||
let mut engine_service = EngineService::new(
|
||||
let engine_kind = if ctx.chain_spec().is_optimism() {
|
||||
EngineApiKind::OpStack
|
||||
} else {
|
||||
EngineApiKind::Ethereum
|
||||
};
|
||||
|
||||
let mut orchestrator = build_engine_orchestrator(
|
||||
engine_kind,
|
||||
consensus.clone(),
|
||||
ctx.chain_spec(),
|
||||
network_client.clone(),
|
||||
Box::pin(consensus_engine_stream),
|
||||
pipeline,
|
||||
@@ -290,7 +296,7 @@ impl EngineNodeLauncher {
|
||||
if let Some(initial_target) = initial_target {
|
||||
debug!(target: "reth::cli", %initial_target, "start backfill sync");
|
||||
// network_handle's sync state is already initialized at Syncing
|
||||
engine_service.orchestrator_mut().start_backfill_sync(initial_target);
|
||||
orchestrator.start_backfill_sync(initial_target);
|
||||
} else if startup_sync_state_idle {
|
||||
network_handle.update_sync_state(SyncState::Idle);
|
||||
}
|
||||
@@ -303,7 +309,7 @@ impl EngineNodeLauncher {
|
||||
// the CL
|
||||
loop {
|
||||
tokio::select! {
|
||||
event = engine_service.next() => {
|
||||
event = orchestrator.next() => {
|
||||
let Some(event) = event else { break };
|
||||
debug!(target: "reth::cli", "Event: {event}");
|
||||
match event {
|
||||
@@ -353,13 +359,13 @@ impl EngineNodeLauncher {
|
||||
payload = built_payloads.select_next_some() => {
|
||||
if let Some(executed_block) = payload.executed_block() {
|
||||
debug!(target: "reth::cli", block=?executed_block.recovered_block.num_hash(), "inserting built payload");
|
||||
engine_service.orchestrator_mut().handler_mut().handler_mut().on_event(EngineApiRequest::InsertExecutedBlock(executed_block.into_executed_payload()).into());
|
||||
orchestrator.handler_mut().handler_mut().on_event(EngineApiRequest::InsertExecutedBlock(executed_block.into_executed_payload()).into());
|
||||
}
|
||||
}
|
||||
shutdown_req = &mut shutdown_rx => {
|
||||
if let Ok(req) = shutdown_req {
|
||||
debug!(target: "reth::cli", "received engine shutdown request");
|
||||
engine_service.orchestrator_mut().handler_mut().handler_mut().on_event(
|
||||
orchestrator.handler_mut().handler_mut().on_event(
|
||||
FromOrchestrator::Terminate { tx: req.done_tx }.into()
|
||||
);
|
||||
}
|
||||
|
||||
@@ -354,7 +354,7 @@ pub(crate) fn parse_receipts_log_filter(
|
||||
) -> Result<ReceiptsLogPruneConfig, ReceiptsLogError> {
|
||||
let mut config = BTreeMap::new();
|
||||
// Split out each of the filters.
|
||||
let filters = value.split(',');
|
||||
let filters = value.split(',').map(str::trim);
|
||||
for filter in filters {
|
||||
let parts: Vec<&str> = filter.split(':').collect();
|
||||
if parts.len() < 2 {
|
||||
@@ -450,6 +450,23 @@ mod tests {
|
||||
assert_eq!(config.0.get(&addr3), Some(&PruneMode::Before(5000000)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_receipts_log_filter_with_spaces() {
|
||||
// Verify that spaces after commas are handled correctly
|
||||
let filters = "0x0000000000000000000000000000000000000001:full, 0x0000000000000000000000000000000000000002:distance:1000";
|
||||
|
||||
let result = parse_receipts_log_filter(filters);
|
||||
assert!(result.is_ok());
|
||||
let config = result.unwrap();
|
||||
assert_eq!(config.0.len(), 2);
|
||||
|
||||
let addr1: Address = "0x0000000000000000000000000000000000000001".parse().unwrap();
|
||||
let addr2: Address = "0x0000000000000000000000000000000000000002".parse().unwrap();
|
||||
|
||||
assert_eq!(config.0.get(&addr1), Some(&PruneMode::Full));
|
||||
assert_eq!(config.0.get(&addr2), Some(&PruneMode::Distance(1000)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_receipts_log_filter_invalid_filter_format() {
|
||||
let result = parse_receipts_log_filter("invalid_format");
|
||||
|
||||
@@ -20,6 +20,7 @@ use reth_primitives_traits::TxTy;
|
||||
use reth_rpc_convert::RpcTxReq;
|
||||
use reth_rpc_eth_types::FillTransaction;
|
||||
use reth_rpc_server_types::{result::internal_rpc_err, ToRpcResult};
|
||||
use std::collections::HashMap;
|
||||
use tracing::trace;
|
||||
|
||||
/// Helper trait, unifies functionality that must be supported to implement all RPC methods for
|
||||
@@ -201,6 +202,14 @@ pub trait EthApi<
|
||||
block_number: Option<BlockId>,
|
||||
) -> RpcResult<B256>;
|
||||
|
||||
/// Returns values from multiple storage positions across multiple addresses.
|
||||
#[method(name = "getStorageValues")]
|
||||
async fn storage_values(
|
||||
&self,
|
||||
requests: HashMap<Address, Vec<JsonStorageKey>>,
|
||||
block_number: Option<BlockId>,
|
||||
) -> RpcResult<HashMap<Address, Vec<B256>>>;
|
||||
|
||||
/// Returns the number of transactions sent from an address at given block number.
|
||||
#[method(name = "getTransactionCount")]
|
||||
async fn transaction_count(
|
||||
@@ -651,6 +660,16 @@ where
|
||||
Ok(EthState::storage_at(self, address, index, block_number).await?)
|
||||
}
|
||||
|
||||
/// Handler for: `eth_getStorageValues`
|
||||
async fn storage_values(
|
||||
&self,
|
||||
requests: HashMap<Address, Vec<JsonStorageKey>>,
|
||||
block_number: Option<BlockId>,
|
||||
) -> RpcResult<HashMap<Address, Vec<B256>>> {
|
||||
trace!(target: "rpc::eth", ?block_number, "Serving eth_getStorageValues");
|
||||
Ok(EthState::storage_values(self, requests, block_number).await?)
|
||||
}
|
||||
|
||||
/// Handler for: `eth_getTransactionCount`
|
||||
async fn transaction_count(
|
||||
&self,
|
||||
|
||||
@@ -16,11 +16,13 @@ use reth_rpc_convert::RpcConvert;
|
||||
use reth_rpc_eth_types::{
|
||||
error::FromEvmError, EthApiError, PendingBlockEnv, RpcInvalidTransactionError,
|
||||
};
|
||||
use reth_rpc_server_types::constants::DEFAULT_MAX_STORAGE_VALUES_SLOTS;
|
||||
use reth_storage_api::{
|
||||
BlockIdReader, BlockNumReader, BlockReaderIdExt, StateProvider, StateProviderBox,
|
||||
StateProviderFactory,
|
||||
};
|
||||
use reth_transaction_pool::TransactionPool;
|
||||
use std::collections::HashMap;
|
||||
|
||||
/// Helper methods for `eth_` methods relating to state (accounts).
|
||||
pub trait EthState: LoadState + SpawnBlocking {
|
||||
@@ -83,6 +85,47 @@ pub trait EthState: LoadState + SpawnBlocking {
|
||||
})
|
||||
}
|
||||
|
||||
/// Returns values from multiple storage positions across multiple addresses.
|
||||
///
|
||||
/// Enforces a cap on total slot count (sum of all slot arrays) and returns an error if
|
||||
/// exceeded.
|
||||
fn storage_values(
|
||||
&self,
|
||||
requests: HashMap<Address, Vec<JsonStorageKey>>,
|
||||
block_id: Option<BlockId>,
|
||||
) -> impl Future<Output = Result<HashMap<Address, Vec<B256>>, Self::Error>> + Send {
|
||||
async move {
|
||||
let total_slots: usize = requests.values().map(|slots| slots.len()).sum();
|
||||
if total_slots > DEFAULT_MAX_STORAGE_VALUES_SLOTS {
|
||||
return Err(Self::Error::from_eth_err(EthApiError::InvalidParams(
|
||||
format!(
|
||||
"total slot count {total_slots} exceeds limit {DEFAULT_MAX_STORAGE_VALUES_SLOTS}",
|
||||
),
|
||||
)));
|
||||
}
|
||||
|
||||
self.spawn_blocking_io_fut(move |this| async move {
|
||||
let state = this.state_at_block_id_or_latest(block_id).await?;
|
||||
|
||||
let mut result = HashMap::with_capacity(requests.len());
|
||||
for (address, slots) in requests {
|
||||
let mut values = Vec::with_capacity(slots.len());
|
||||
for slot in &slots {
|
||||
let value = state
|
||||
.storage(address, slot.as_b256())
|
||||
.map_err(Self::Error::from_eth_err)?
|
||||
.unwrap_or_default();
|
||||
values.push(B256::new(value.to_be_bytes()));
|
||||
}
|
||||
result.insert(address, values);
|
||||
}
|
||||
|
||||
Ok(result)
|
||||
})
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns values stored of given account, with Merkle-proof, at given blocknumber.
|
||||
fn get_proof(
|
||||
&self,
|
||||
|
||||
@@ -55,6 +55,9 @@ pub const DEFAULT_ENGINE_API_IPC_ENDPOINT: &str = "/tmp/reth_engine_api.ipc";
|
||||
/// The default limit for blocks count in `eth_simulateV1`.
|
||||
pub const DEFAULT_MAX_SIMULATE_BLOCKS: u64 = 256;
|
||||
|
||||
/// The default maximum number of total storage slots for `eth_getStorageValues`.
|
||||
pub const DEFAULT_MAX_STORAGE_VALUES_SLOTS: usize = 1024;
|
||||
|
||||
/// The default eth historical proof window.
|
||||
pub const DEFAULT_ETH_PROOF_WINDOW: u64 = 0;
|
||||
|
||||
|
||||
@@ -183,10 +183,9 @@ where
|
||||
// channels used to return result of account hashing
|
||||
for chunk in &accounts_cursor.walk(None)?.chunks(WORKER_CHUNK_SIZE) {
|
||||
// An _unordered_ channel to receive results from a rayon job
|
||||
let (tx, rx) = mpsc::channel();
|
||||
channels.push(rx);
|
||||
|
||||
let chunk = chunk.collect::<Result<Vec<_>, _>>()?;
|
||||
let (tx, rx) = mpsc::sync_channel(chunk.len());
|
||||
channels.push(rx);
|
||||
// Spawn the hashing task onto the global rayon pool
|
||||
rayon::spawn(move || {
|
||||
for (address, account) in chunk {
|
||||
|
||||
@@ -110,10 +110,9 @@ where
|
||||
|
||||
for chunk in &storage_cursor.walk(None)?.chunks(WORKER_CHUNK_SIZE) {
|
||||
// An _unordered_ channel to receive results from a rayon job
|
||||
let (tx, rx) = mpsc::channel();
|
||||
channels.push(rx);
|
||||
|
||||
let chunk = chunk.collect::<Result<Vec<_>, _>>()?;
|
||||
let (tx, rx) = mpsc::sync_channel(chunk.len());
|
||||
channels.push(rx);
|
||||
// Spawn the hashing task onto the global rayon pool
|
||||
rayon::spawn(move || {
|
||||
// Cache hashed address since PlainStorageState is sorted by address
|
||||
|
||||
@@ -34,7 +34,7 @@ const BATCH_SIZE: usize = 100_000;
|
||||
const WORKER_CHUNK_SIZE: usize = 100;
|
||||
|
||||
/// Type alias for a sender that transmits the result of sender recovery.
|
||||
type RecoveryResultSender = mpsc::Sender<Result<(u64, Address), Box<SenderRecoveryStageError>>>;
|
||||
type RecoveryResultSender = mpsc::SyncSender<Result<(u64, Address), Box<SenderRecoveryStageError>>>;
|
||||
|
||||
/// The sender recovery stage iterates over existing transactions,
|
||||
/// recovers the transaction signer and stores them
|
||||
@@ -245,7 +245,7 @@ where
|
||||
.step_by(WORKER_CHUNK_SIZE)
|
||||
.map(|start| {
|
||||
let range = start..std::cmp::min(start + WORKER_CHUNK_SIZE as u64, tx_range.end);
|
||||
let (tx, rx) = mpsc::channel();
|
||||
let (tx, rx) = mpsc::sync_channel((range.end - range.start) as usize);
|
||||
// Range and channel sender will be sent to rayon worker
|
||||
((range, tx), rx)
|
||||
})
|
||||
|
||||
@@ -27,8 +27,6 @@ alloy-primitives.workspace = true
|
||||
# mdbx
|
||||
reth-libmdbx = { workspace = true, optional = true, features = ["return-borrowed", "read-tx-timeouts"] }
|
||||
eyre = { workspace = true, optional = true }
|
||||
reth-mdbx-viz = { workspace = true, optional = true }
|
||||
tokio = { workspace = true, optional = true, features = ["full"] }
|
||||
|
||||
# metrics
|
||||
reth-metrics = { workspace = true, optional = true }
|
||||
@@ -99,7 +97,6 @@ op = [
|
||||
"reth-db-api/op",
|
||||
"reth-primitives-traits/op",
|
||||
]
|
||||
pageviz = ["reth-libmdbx/pageviz", "dep:reth-mdbx-viz", "dep:tokio", "mdbx"]
|
||||
disable-lock = []
|
||||
|
||||
[[bench]]
|
||||
|
||||
@@ -522,81 +522,6 @@ impl DatabaseEnv {
|
||||
self
|
||||
}
|
||||
|
||||
/// Starts the real-time page access visualization server.
|
||||
///
|
||||
/// This enables the C-level page access instrumentation hooks,
|
||||
/// starts a background drainer thread that coalesces events,
|
||||
/// and launches an HTTP+WebSocket server on the given port.
|
||||
///
|
||||
/// The server serves an interactive HTML visualization at `http://localhost:{port}/`
|
||||
/// showing page accesses in real-time with fading highlights:
|
||||
/// - Blue = read, Red = write, Yellow = free
|
||||
#[cfg(feature = "pageviz")]
|
||||
pub fn start_pageviz(&self, port: u16, db_path: std::path::PathBuf) {
|
||||
use reth_libmdbx::pageviz::PageVizDrainer;
|
||||
|
||||
let max_dbi = self.dbis.values().copied().max().unwrap_or(0) as usize;
|
||||
let mut dbi_names = vec![String::new(); max_dbi + 1];
|
||||
dbi_names[0] = "FREE_DBI".to_string();
|
||||
if dbi_names.len() > 1 {
|
||||
dbi_names[1] = "MAIN_DBI".to_string();
|
||||
}
|
||||
for (name, &dbi) in self.dbis.iter() {
|
||||
dbi_names[dbi as usize] = name.to_string();
|
||||
}
|
||||
|
||||
let mut name_to_dbi = std::collections::HashMap::new();
|
||||
for (&name, &dbi) in self.dbis.iter() {
|
||||
name_to_dbi.insert(name, dbi as u8);
|
||||
}
|
||||
|
||||
let mdbx_dat = db_path.join("mdbx.dat");
|
||||
let walk = match reth_mdbx_viz::walker::walk_mdbx(&mdbx_dat, &name_to_dbi) {
|
||||
Ok(w) => w,
|
||||
Err(e) => {
|
||||
reth_tracing::tracing::error!("pageviz: failed to walk B-tree: {e}");
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
let page_count = walk.page_count;
|
||||
let page_size = walk.page_size;
|
||||
|
||||
let mut drainer = PageVizDrainer::new();
|
||||
drainer.enable();
|
||||
let rx = drainer.start(60);
|
||||
|
||||
let config = reth_mdbx_viz::VizConfig {
|
||||
port,
|
||||
page_count: page_count as u64,
|
||||
page_size: page_size as u32,
|
||||
dbi_names,
|
||||
owner_map: walk.owner_map,
|
||||
tree_info: walk.tree_info,
|
||||
db_path: mdbx_dat,
|
||||
};
|
||||
|
||||
// Spawn the viz server on a background tokio runtime
|
||||
std::thread::Builder::new()
|
||||
.name("mdbx-viz-server".into())
|
||||
.spawn(move || {
|
||||
let rt = tokio::runtime::Builder::new_current_thread()
|
||||
.enable_all()
|
||||
.build()
|
||||
.expect("failed to build tokio runtime for pageviz");
|
||||
rt.block_on(async {
|
||||
if let Err(e) = reth_mdbx_viz::start_viz_server(config, rx).await {
|
||||
reth_tracing::tracing::error!("pageviz server error: {e}");
|
||||
}
|
||||
});
|
||||
// Keep drainer alive while server runs
|
||||
drop(drainer);
|
||||
})
|
||||
.expect("failed to spawn pageviz server thread");
|
||||
|
||||
reth_tracing::tracing::info!("pageviz server started on port {port}");
|
||||
}
|
||||
|
||||
/// Creates all the tables defined in [`Tables`], if necessary.
|
||||
///
|
||||
/// This keeps tracks of the created table handles and stores them for better efficiency.
|
||||
|
||||
@@ -45,22 +45,11 @@ pub fn init_db_for<P: AsRef<Path>, TS: TableSet>(
|
||||
path: P,
|
||||
args: DatabaseArguments,
|
||||
) -> eyre::Result<DatabaseEnv> {
|
||||
let db_path = path.as_ref().to_path_buf();
|
||||
let client_version = args.client_version().clone();
|
||||
let mut db = create_db(path, args)?;
|
||||
db.create_and_track_tables_for::<TS>()?;
|
||||
db.record_client_version(client_version)?;
|
||||
drop_orphan_tables(&db);
|
||||
|
||||
#[cfg(feature = "pageviz")]
|
||||
{
|
||||
let port = std::env::var("RETH_PAGEVIZ_PORT")
|
||||
.ok()
|
||||
.and_then(|s| s.parse().ok())
|
||||
.unwrap_or(3141u16);
|
||||
db.start_pageviz(port, db_path);
|
||||
}
|
||||
|
||||
Ok(db)
|
||||
}
|
||||
|
||||
|
||||
@@ -28,7 +28,6 @@ dashmap = { workspace = true, features = ["inline"], optional = true }
|
||||
default = []
|
||||
return-borrowed = []
|
||||
read-tx-timeouts = ["dep:dashmap"]
|
||||
pageviz = ["reth-mdbx-sys/pageviz"]
|
||||
|
||||
[dev-dependencies]
|
||||
criterion.workspace = true
|
||||
|
||||
@@ -11,7 +11,3 @@ repository.workspace = true
|
||||
[build-dependencies]
|
||||
cc.workspace = true
|
||||
bindgen = { workspace = true, features = ["runtime"] }
|
||||
|
||||
[features]
|
||||
default = []
|
||||
pageviz = []
|
||||
|
||||
@@ -30,11 +30,6 @@ fn main() {
|
||||
#[cfg(not(debug_assertions))]
|
||||
cc.define("MDBX_DEBUG", "0").define("NDEBUG", None);
|
||||
|
||||
// Enable page visualization instrumentation
|
||||
if std::env::var("CARGO_FEATURE_PAGEVIZ").is_ok() {
|
||||
cc.define("MDBX_PAGEVIZ", "1");
|
||||
}
|
||||
|
||||
// Propagate `-C target-cpu=native`
|
||||
let rustflags = env::var("CARGO_ENCODED_RUSTFLAGS").unwrap();
|
||||
if rustflags.contains("target-cpu=native") &&
|
||||
@@ -43,11 +38,7 @@ fn main() {
|
||||
cc.flag("-march=native");
|
||||
}
|
||||
|
||||
cc.file(mdbx.join("mdbx.c"));
|
||||
if std::env::var("CARGO_FEATURE_PAGEVIZ").is_ok() {
|
||||
cc.file(mdbx.join("mdbx_pageviz.c"));
|
||||
}
|
||||
cc.compile("libmdbx.a");
|
||||
cc.file(mdbx.join("mdbx.c")).compile("libmdbx.a");
|
||||
}
|
||||
|
||||
fn generate_bindings(mdbx: &Path, out_file: &Path) {
|
||||
|
||||
@@ -13,7 +13,6 @@
|
||||
#include MDBX_CONFIG_H
|
||||
#endif
|
||||
|
||||
|
||||
/* Undefine the NDEBUG if debugging is enforced by MDBX_DEBUG */
|
||||
#if (defined(MDBX_DEBUG) && MDBX_DEBUG > 0) || (defined(MDBX_FORCE_ASSERTIONS) && MDBX_FORCE_ASSERTIONS)
|
||||
#undef NDEBUG
|
||||
@@ -47,7 +46,6 @@
|
||||
#define _DARWIN_C_SOURCE
|
||||
#endif /* _DARWIN_C_SOURCE */
|
||||
|
||||
#include "mdbx_pageviz.h"
|
||||
#if (defined(__MINGW__) || defined(__MINGW32__) || defined(__MINGW64__)) && !defined(__USE_MINGW_ANSI_STDIO)
|
||||
#define __USE_MINGW_ANSI_STDIO 1
|
||||
#endif /* MinGW */
|
||||
@@ -6195,7 +6193,6 @@ MDBX_INTERNAL int page_touch_unmodifable(MDBX_txn *txn, MDBX_cursor *mc, const p
|
||||
static inline int page_touch(MDBX_cursor *mc) {
|
||||
page_t *const mp = mc->pg[mc->top];
|
||||
MDBX_txn *txn = mc->txn;
|
||||
MDBX_PAGEVIZ_WRITE(mc, mp->pgno);
|
||||
|
||||
tASSERT(txn, mc->txn->flags & MDBX_TXN_DIRTY);
|
||||
tASSERT(txn, F_ISSET(*cursor_dbi_state(mc), DBI_LINDO | DBI_VALID | DBI_DIRTY));
|
||||
@@ -19929,9 +19926,6 @@ __cold int dxb_resize(MDBX_env *const env, const pgno_t used_pgno, const pgno_t
|
||||
eASSERT(env, env->dxb_mmap.limit >= env->dxb_mmap.current);
|
||||
|
||||
if (rc == MDBX_SUCCESS) {
|
||||
#if defined(MDBX_PAGEVIZ) && MDBX_PAGEVIZ
|
||||
mdbx_pageviz_set_mapping(env->dxb_mmap.base, env->dxb_mmap.current, env->ps);
|
||||
#endif
|
||||
eASSERT(env, limit_bytes == env->dxb_mmap.limit);
|
||||
eASSERT(env, size_bytes <= env->dxb_mmap.filesize);
|
||||
if (mode == explicit_resize)
|
||||
@@ -20345,10 +20339,6 @@ __cold int dxb_setup(MDBX_env *env, const int lck_rc, const mdbx_mode_t mode_bit
|
||||
if (unlikely(err != MDBX_SUCCESS))
|
||||
return err;
|
||||
|
||||
#if defined(MDBX_PAGEVIZ) && MDBX_PAGEVIZ
|
||||
mdbx_pageviz_set_mapping(env->dxb_mmap.base, env->dxb_mmap.current, env->ps);
|
||||
#endif
|
||||
|
||||
#if defined(MADV_DONTDUMP)
|
||||
err =
|
||||
madvise(env->dxb_mmap.base, env->dxb_mmap.limit, MADV_DONTDUMP) ? ignore_enosys_and_eagain(errno) : MDBX_SUCCESS;
|
||||
@@ -23100,7 +23090,6 @@ static int gcu_loose(MDBX_txn *txn, gcu_t *ctx) {
|
||||
return err;
|
||||
for (page_t *lp = txn->tw.loose_pages; lp; lp = page_next(lp)) {
|
||||
pnl_append_prereserved(txn->tw.retired_pages, lp->pgno);
|
||||
mdbx_pageviz_emit(MDBX_PAGEVIZ_OP_FREE, 0, lp->pgno);
|
||||
MDBX_ASAN_UNPOISON_MEMORY_REGION(&page_next(lp), sizeof(page_t *));
|
||||
VALGRIND_MAKE_MEM_DEFINED(&page_next(lp), sizeof(page_t *));
|
||||
}
|
||||
@@ -23115,7 +23104,6 @@ static int gcu_loose(MDBX_txn *txn, gcu_t *ctx) {
|
||||
for (page_t *lp = txn->tw.loose_pages; lp; lp = page_next(lp)) {
|
||||
tASSERT(txn, lp->flags == P_LOOSE);
|
||||
loose[++count] = lp->pgno;
|
||||
mdbx_pageviz_emit(MDBX_PAGEVIZ_OP_FREE, 0, lp->pgno);
|
||||
MDBX_ASAN_UNPOISON_MEMORY_REGION(&page_next(lp), sizeof(page_t *));
|
||||
VALGRIND_MAKE_MEM_DEFINED(&page_next(lp), sizeof(page_t *));
|
||||
}
|
||||
@@ -31638,7 +31626,6 @@ static __always_inline pgr_t page_get_inline(const uint16_t ILL, const MDBX_curs
|
||||
if (unlikely(r.err != MDBX_SUCCESS))
|
||||
goto bailout;
|
||||
#endif /* MDBX_DISABLE_VALIDATION */
|
||||
MDBX_PAGEVIZ_READ(mc, pgno);
|
||||
return r;
|
||||
}
|
||||
|
||||
@@ -31855,7 +31842,6 @@ pgr_t page_new(MDBX_cursor *mc, const unsigned flags) {
|
||||
return ret;
|
||||
|
||||
DEBUG("db %zu allocated new page %" PRIaPGNO, cursor_dbi(mc), ret.page->pgno);
|
||||
MDBX_PAGEVIZ_WRITE(mc, ret.page->pgno);
|
||||
ret.page->flags = (uint16_t)flags;
|
||||
cASSERT(mc, *cursor_dbi_state(mc) & DBI_DIRTY);
|
||||
cASSERT(mc, mc->txn->flags & MDBX_TXN_DIRTY);
|
||||
@@ -31884,7 +31870,6 @@ pgr_t page_new_large(MDBX_cursor *mc, const size_t npages) {
|
||||
return ret;
|
||||
|
||||
DEBUG("dbi %zu allocated new large-page %" PRIaPGNO ", num %zu", cursor_dbi(mc), ret.page->pgno, npages);
|
||||
MDBX_PAGEVIZ_WRITE(mc, ret.page->pgno);
|
||||
ret.page->flags = P_LARGE;
|
||||
cASSERT(mc, *cursor_dbi_state(mc) & DBI_DIRTY);
|
||||
cASSERT(mc, mc->txn->flags & MDBX_TXN_DIRTY);
|
||||
@@ -32030,7 +32015,6 @@ __hot int page_touch_unmodifable(MDBX_txn *txn, MDBX_cursor *mc, const page_t *c
|
||||
DEBUG("touched db %d page %" PRIaPGNO " -> %" PRIaPGNO, cursor_dbi_dbg(mc), mp->pgno, pgno);
|
||||
tASSERT(txn, mp->pgno != pgno);
|
||||
pnl_append_prereserved(txn->tw.retired_pages, mp->pgno);
|
||||
MDBX_PAGEVIZ_FREE(mc, mp->pgno);
|
||||
/* Update the parent page, if any, to point to the new page */
|
||||
if (likely(mc->top)) {
|
||||
page_t *parent = mc->pg[mc->top - 1];
|
||||
@@ -32227,7 +32211,6 @@ int page_retire_ex(MDBX_cursor *mc, const pgno_t pgno, page_t *mp /* maybe null
|
||||
unsigned pageflags /* maybe unknown/zero */) {
|
||||
int rc;
|
||||
MDBX_txn *const txn = mc->txn;
|
||||
MDBX_PAGEVIZ_FREE(mc, pgno);
|
||||
tASSERT(txn, !mp || (mp->pgno == pgno && mp->flags == pageflags));
|
||||
|
||||
/* During deleting entire subtrees, it is reasonable and possible to avoid
|
||||
|
||||
@@ -1,119 +0,0 @@
|
||||
#include "mdbx_pageviz.h"
|
||||
|
||||
#if defined(MDBX_PAGEVIZ) && MDBX_PAGEVIZ
|
||||
|
||||
/* ── Global state ─────────────────────────────────────────────────────── */
|
||||
|
||||
mdbx_pageviz_state_t *mdbx_pageviz_global = NULL;
|
||||
|
||||
/* ── Thread-local ring index ──────────────────────────────────────────── */
|
||||
|
||||
MDBX_PAGEVIZ_TLS uint32_t mdbx_pageviz_tls_ring = MDBX_PAGEVIZ_TLS_UNREGISTERED;
|
||||
|
||||
/* ── Lifecycle ────────────────────────────────────────────────────────── */
|
||||
|
||||
mdbx_pageviz_state_t *mdbx_pageviz_create(void) {
|
||||
mdbx_pageviz_state_t *state = calloc(1, sizeof(mdbx_pageviz_state_t));
|
||||
if (state)
|
||||
mdbx_pageviz_global = state;
|
||||
return state;
|
||||
}
|
||||
|
||||
void mdbx_pageviz_destroy(mdbx_pageviz_state_t *state) {
|
||||
if (!state)
|
||||
return;
|
||||
if (mdbx_pageviz_global == state)
|
||||
mdbx_pageviz_global = NULL;
|
||||
free(state);
|
||||
}
|
||||
|
||||
/* ── Enable / Disable ─────────────────────────────────────────────────── */
|
||||
|
||||
void mdbx_pageviz_enable(mdbx_pageviz_state_t *state) {
|
||||
atomic_store_explicit(&state->enabled, 1, memory_order_release);
|
||||
}
|
||||
|
||||
void mdbx_pageviz_disable(mdbx_pageviz_state_t *state) {
|
||||
atomic_store_explicit(&state->enabled, 0, memory_order_release);
|
||||
}
|
||||
|
||||
/* ── Drain ────────────────────────────────────────────────────────────── */
|
||||
|
||||
uint32_t mdbx_pageviz_drain(mdbx_pageviz_state_t *state, uint32_t ring_idx,
|
||||
uint64_t *out_buf, uint32_t max_count) {
|
||||
if (!state || ring_idx >= MDBX_PAGEVIZ_MAX_RINGS || !out_buf || max_count == 0)
|
||||
return 0;
|
||||
|
||||
mdbx_pageviz_ring_t *ring = &state->rings[ring_idx];
|
||||
uint32_t head = atomic_load_explicit(&ring->published_head,
|
||||
memory_order_acquire);
|
||||
uint32_t tail = atomic_load_explicit(&ring->consumer_tail,
|
||||
memory_order_relaxed);
|
||||
|
||||
if (head == tail)
|
||||
return 0;
|
||||
|
||||
uint32_t avail = head - tail;
|
||||
uint32_t count = avail < max_count ? avail : max_count;
|
||||
for (uint32_t i = 0; i < count; i++) {
|
||||
uint32_t slot = (tail + i) & MDBX_PAGEVIZ_RING_MASK;
|
||||
out_buf[i] = ring->events[slot];
|
||||
}
|
||||
|
||||
atomic_store_explicit(&ring->consumer_tail, tail + count,
|
||||
memory_order_release);
|
||||
return count;
|
||||
}
|
||||
|
||||
/* ── Queries ──────────────────────────────────────────────────────────── */
|
||||
|
||||
uint32_t mdbx_pageviz_ring_count(mdbx_pageviz_state_t *state) {
|
||||
return atomic_load_explicit(&state->ring_count, memory_order_relaxed);
|
||||
}
|
||||
|
||||
uint64_t mdbx_pageviz_dropped(mdbx_pageviz_state_t *state, uint32_t ring_idx) {
|
||||
return atomic_load_explicit(&state->rings[ring_idx].dropped,
|
||||
memory_order_relaxed);
|
||||
}
|
||||
|
||||
/* ── Mapping info ────────────────────────────────────────────────────── */
|
||||
|
||||
void mdbx_pageviz_set_mapping(void *base, size_t len, uint32_t mdbx_page_size) {
|
||||
mdbx_pageviz_state_t *state = mdbx_pageviz_global;
|
||||
if (!state)
|
||||
return;
|
||||
state->mapping.mdbx_page_size = mdbx_page_size;
|
||||
state->mapping.sys_page_size = (uint32_t)sysconf(_SC_PAGESIZE);
|
||||
state->mapping.len = len;
|
||||
/* base written last so consumers see consistent len+page_size first */
|
||||
__atomic_store_n((void *volatile *)&state->mapping.base, base, __ATOMIC_RELEASE);
|
||||
}
|
||||
|
||||
int mdbx_pageviz_get_mapping(void **out_base, size_t *out_len,
|
||||
uint32_t *out_mdbx_ps, uint32_t *out_sys_ps) {
|
||||
mdbx_pageviz_state_t *state = mdbx_pageviz_global;
|
||||
if (!state)
|
||||
return 0;
|
||||
void *b = __atomic_load_n((void *volatile *)&state->mapping.base, __ATOMIC_ACQUIRE);
|
||||
if (!b)
|
||||
return 0;
|
||||
*out_base = b;
|
||||
*out_len = state->mapping.len;
|
||||
*out_mdbx_ps = state->mapping.mdbx_page_size;
|
||||
*out_sys_ps = state->mapping.sys_page_size;
|
||||
return 1;
|
||||
}
|
||||
|
||||
/* ── Block marker emit (non-inline wrapper for Rust FFI) ──────────────── */
|
||||
|
||||
void mdbx_pageviz_emit_block_marker(uint8_t op, uint32_t block_number,
|
||||
uint16_t tx_count, uint8_t duration_encoded,
|
||||
uint8_t gas_encoded) {
|
||||
mdbx_pageviz_emit(op, ((uint32_t)gas_encoded << 24) | ((uint32_t)duration_encoded << 16) | (uint32_t)tx_count, block_number);
|
||||
}
|
||||
|
||||
#else /* !MDBX_PAGEVIZ */
|
||||
|
||||
typedef int mdbx_pageviz_empty_tu_;
|
||||
|
||||
#endif /* MDBX_PAGEVIZ */
|
||||
@@ -1,195 +0,0 @@
|
||||
/* mdbx_pageviz.h — Standalone page-access visualization ring buffer for MDBX.
|
||||
*
|
||||
* Self-contained: no dependencies on MDBX internals.
|
||||
* Gate everything behind MDBX_PAGEVIZ; when not defined, all macros expand to
|
||||
* nothing (zero overhead).
|
||||
*
|
||||
* Compatible with C11 and C17. */
|
||||
|
||||
#ifndef MDBX_PAGEVIZ_H
|
||||
#define MDBX_PAGEVIZ_H
|
||||
|
||||
/* ── Disabled stub macros ─────────────────────────────────────────────── */
|
||||
|
||||
#if !defined(MDBX_PAGEVIZ) || !(MDBX_PAGEVIZ)
|
||||
|
||||
#define MDBX_PAGEVIZ_READ(mc, pgno) ((void)0)
|
||||
#define MDBX_PAGEVIZ_WRITE(mc, pgno) ((void)0)
|
||||
#define MDBX_PAGEVIZ_FREE(mc, pgno) ((void)0)
|
||||
|
||||
#else /* MDBX_PAGEVIZ enabled ───────────────────────────────────────────── */
|
||||
|
||||
#include <stdatomic.h>
|
||||
#include <stdint.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <unistd.h>
|
||||
|
||||
/* ── Tunables ─────────────────────────────────────────────────────────── */
|
||||
|
||||
#define MDBX_PAGEVIZ_RING_CAPACITY 65536u /* must be power-of-2 */
|
||||
#define MDBX_PAGEVIZ_MAX_RINGS 128u
|
||||
#define MDBX_PAGEVIZ_PUBLISH_INTERVAL 32u
|
||||
|
||||
/* ── Event encoding ───────────────────────────────────────────────────── */
|
||||
|
||||
/* Layout of a 64-bit event word:
|
||||
* bits 63..56 op (1=READ, 2=WRITE, 3=FREE)
|
||||
* bits 55..32 dbi (lower 24 bits of a uint32_t)
|
||||
* bits 31..0 pgno (uint32_t page number) */
|
||||
|
||||
#define MDBX_PAGEVIZ_OP_READ 1
|
||||
#define MDBX_PAGEVIZ_OP_WRITE 2
|
||||
#define MDBX_PAGEVIZ_OP_FREE 3
|
||||
#define MDBX_PAGEVIZ_OP_BLOCK_START 4
|
||||
#define MDBX_PAGEVIZ_OP_BLOCK_END 5
|
||||
|
||||
#define MDBX_PAGEVIZ_ENCODE(op, dbi, pgno) \
|
||||
(((uint64_t)(op) << 56) | ((uint64_t)((dbi) & 0x00FFFFFFu) << 32) | \
|
||||
(uint64_t)(uint32_t)(pgno))
|
||||
|
||||
#define MDBX_PAGEVIZ_DECODE_OP(ev) ((uint8_t)((ev) >> 56))
|
||||
#define MDBX_PAGEVIZ_DECODE_DBI(ev) ((uint32_t)(((ev) >> 32) & 0x00FFFFFFu))
|
||||
#define MDBX_PAGEVIZ_DECODE_PGNO(ev) ((uint32_t)(ev))
|
||||
|
||||
/* ── Ring-buffer mask (power-of-2 capacity) ───────────────────────────── */
|
||||
|
||||
#define MDBX_PAGEVIZ_RING_MASK (MDBX_PAGEVIZ_RING_CAPACITY - 1u)
|
||||
|
||||
/* ── Platform TLS ─────────────────────────────────────────────────────── */
|
||||
|
||||
#if defined(_MSC_VER)
|
||||
# define MDBX_PAGEVIZ_TLS __declspec(thread)
|
||||
#elif defined(__GNUC__) || defined(__clang__)
|
||||
# define MDBX_PAGEVIZ_TLS __thread
|
||||
#elif defined(__STDC_VERSION__) && __STDC_VERSION__ >= 201112L
|
||||
# define MDBX_PAGEVIZ_TLS _Thread_local
|
||||
#else
|
||||
# error "No thread-local storage support detected"
|
||||
#endif
|
||||
|
||||
/* ── Structures ───────────────────────────────────────────────────────── */
|
||||
|
||||
typedef struct mdbx_pageviz_ring {
|
||||
uint64_t events[MDBX_PAGEVIZ_RING_CAPACITY];
|
||||
_Atomic uint32_t published_head; /* consumer reads (acquire) */
|
||||
uint32_t local_head; /* producer-only, no atomic needed */
|
||||
_Atomic uint32_t consumer_tail; /* consumer updates after draining */
|
||||
_Atomic uint64_t dropped; /* unused, kept for ABI compat */
|
||||
uint32_t _pad[10]; /* pad to separate cache lines */
|
||||
} mdbx_pageviz_ring_t;
|
||||
|
||||
typedef struct mdbx_pageviz_mapping {
|
||||
volatile void *base;
|
||||
volatile size_t len;
|
||||
volatile uint32_t mdbx_page_size;
|
||||
volatile uint32_t sys_page_size;
|
||||
} mdbx_pageviz_mapping_t;
|
||||
|
||||
typedef struct mdbx_pageviz_state {
|
||||
mdbx_pageviz_ring_t rings[MDBX_PAGEVIZ_MAX_RINGS];
|
||||
_Atomic uint32_t ring_count; /* how many rings registered */
|
||||
_Atomic uint32_t enabled; /* runtime enable/disable */
|
||||
mdbx_pageviz_mapping_t mapping;
|
||||
} mdbx_pageviz_state_t;
|
||||
|
||||
/* ── Global state (defined in .c) ─────────────────────────────────────── */
|
||||
|
||||
extern mdbx_pageviz_state_t *mdbx_pageviz_global;
|
||||
|
||||
/* ── Thread-local ring index (defined in .c) ──────────────────────────── */
|
||||
|
||||
#define MDBX_PAGEVIZ_TLS_UNREGISTERED UINT32_MAX
|
||||
|
||||
extern MDBX_PAGEVIZ_TLS uint32_t mdbx_pageviz_tls_ring;
|
||||
|
||||
/* ── Inline helpers (private) ─────────────────────────────────────────── */
|
||||
|
||||
static inline uint32_t mdbx_pageviz_register_ring_(mdbx_pageviz_state_t *s) {
|
||||
uint32_t idx =
|
||||
atomic_fetch_add_explicit(&s->ring_count, 1, memory_order_relaxed);
|
||||
if (idx >= MDBX_PAGEVIZ_MAX_RINGS) {
|
||||
atomic_fetch_sub_explicit(&s->ring_count, 1, memory_order_relaxed);
|
||||
return MDBX_PAGEVIZ_TLS_UNREGISTERED;
|
||||
}
|
||||
mdbx_pageviz_ring_t *r = &s->rings[idx];
|
||||
atomic_store_explicit(&r->published_head, 0, memory_order_relaxed);
|
||||
r->local_head = 0;
|
||||
atomic_store_explicit(&r->consumer_tail, 0, memory_order_relaxed);
|
||||
atomic_store_explicit(&r->dropped, 0, memory_order_relaxed);
|
||||
return idx;
|
||||
}
|
||||
|
||||
/* ── Hot-path emit (inlined) ──────────────────────────────────────────── */
|
||||
|
||||
static inline void mdbx_pageviz_emit(uint8_t op, uint32_t dbi,
|
||||
uint32_t pgno) {
|
||||
mdbx_pageviz_state_t *state = mdbx_pageviz_global;
|
||||
if (__builtin_expect(
|
||||
state == NULL ||
|
||||
!atomic_load_explicit(&state->enabled, memory_order_relaxed),
|
||||
1))
|
||||
return;
|
||||
|
||||
uint32_t idx = mdbx_pageviz_tls_ring;
|
||||
if (__builtin_expect(idx == MDBX_PAGEVIZ_TLS_UNREGISTERED, 0)) {
|
||||
idx = mdbx_pageviz_register_ring_(state);
|
||||
if (__builtin_expect(idx == MDBX_PAGEVIZ_TLS_UNREGISTERED, 0))
|
||||
return;
|
||||
mdbx_pageviz_tls_ring = idx;
|
||||
}
|
||||
|
||||
mdbx_pageviz_ring_t *r = &state->rings[idx];
|
||||
|
||||
/* Block (spin) until the consumer drains enough space. */
|
||||
while (__builtin_expect(
|
||||
(uint32_t)(r->local_head -
|
||||
atomic_load_explicit(&r->consumer_tail, memory_order_acquire))
|
||||
>= MDBX_PAGEVIZ_RING_CAPACITY, 0)) {
|
||||
/* Flush our writes so the consumer can make progress. */
|
||||
atomic_store_explicit(&r->published_head, r->local_head,
|
||||
memory_order_release);
|
||||
#if defined(__x86_64__) || defined(_M_X64) || defined(__i386__) || defined(_M_IX86)
|
||||
__builtin_ia32_pause();
|
||||
#elif defined(__aarch64__) || defined(_M_ARM64)
|
||||
__asm__ volatile("yield");
|
||||
#endif
|
||||
}
|
||||
|
||||
r->events[r->local_head & MDBX_PAGEVIZ_RING_MASK] =
|
||||
MDBX_PAGEVIZ_ENCODE(op, dbi, pgno);
|
||||
r->local_head++;
|
||||
|
||||
if ((r->local_head & (MDBX_PAGEVIZ_PUBLISH_INTERVAL - 1u)) == 0)
|
||||
atomic_store_explicit(&r->published_head, r->local_head,
|
||||
memory_order_release);
|
||||
}
|
||||
|
||||
/* ── Macro hooks (inserted into mdbx.c) ──────────────────────────────── */
|
||||
|
||||
#define MDBX_PAGEVIZ_READ(mc, pgno) \
|
||||
mdbx_pageviz_emit(MDBX_PAGEVIZ_OP_READ, (uint32_t)cursor_dbi(mc), (pgno))
|
||||
#define MDBX_PAGEVIZ_WRITE(mc, pgno) \
|
||||
mdbx_pageviz_emit(MDBX_PAGEVIZ_OP_WRITE, (uint32_t)cursor_dbi(mc), (pgno))
|
||||
#define MDBX_PAGEVIZ_FREE(mc, pgno) \
|
||||
mdbx_pageviz_emit(MDBX_PAGEVIZ_OP_FREE, (uint32_t)cursor_dbi(mc), (pgno))
|
||||
|
||||
/* ── Public API (defined in .c) ───────────────────────────────────────── */
|
||||
|
||||
mdbx_pageviz_state_t *mdbx_pageviz_create(void);
|
||||
void mdbx_pageviz_destroy(mdbx_pageviz_state_t *state);
|
||||
void mdbx_pageviz_enable(mdbx_pageviz_state_t *state);
|
||||
void mdbx_pageviz_disable(mdbx_pageviz_state_t *state);
|
||||
uint32_t mdbx_pageviz_drain(mdbx_pageviz_state_t *state, uint32_t ring_idx,
|
||||
uint64_t *out_buf, uint32_t max_count);
|
||||
uint32_t mdbx_pageviz_ring_count(mdbx_pageviz_state_t *state);
|
||||
uint64_t mdbx_pageviz_dropped(mdbx_pageviz_state_t *state, uint32_t ring_idx);
|
||||
void mdbx_pageviz_set_mapping(void *base, size_t len, uint32_t mdbx_page_size);
|
||||
int mdbx_pageviz_get_mapping(void **out_base, size_t *out_len,
|
||||
uint32_t *out_mdbx_ps, uint32_t *out_sys_ps);
|
||||
void mdbx_pageviz_emit_block_marker(uint8_t op, uint32_t block_number,
|
||||
uint16_t tx_count, uint8_t duration_encoded,
|
||||
uint8_t gas_encoded);
|
||||
|
||||
#endif /* MDBX_PAGEVIZ */
|
||||
#endif /* MDBX_PAGEVIZ_H */
|
||||
@@ -36,9 +36,6 @@ mod flags;
|
||||
mod transaction;
|
||||
mod txn_manager;
|
||||
|
||||
#[cfg(feature = "pageviz")]
|
||||
pub mod pageviz;
|
||||
|
||||
#[cfg(test)]
|
||||
mod test_utils {
|
||||
use super::*;
|
||||
|
||||
@@ -1,226 +0,0 @@
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
sync::{
|
||||
atomic::{AtomicBool, Ordering},
|
||||
mpsc, Arc,
|
||||
},
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
#[repr(u8)]
|
||||
pub enum PageOp {
|
||||
Read = 1,
|
||||
Write = 2,
|
||||
Free = 3,
|
||||
BlockStart = 4,
|
||||
BlockEnd = 5,
|
||||
}
|
||||
|
||||
impl PageOp {
|
||||
fn from_u8(v: u8) -> Option<Self> {
|
||||
match v {
|
||||
1 => Some(Self::Read),
|
||||
2 => Some(Self::Write),
|
||||
3 => Some(Self::Free),
|
||||
4 => Some(Self::BlockStart),
|
||||
5 => Some(Self::BlockEnd),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_block_marker(self) -> bool {
|
||||
matches!(self, Self::BlockStart | Self::BlockEnd)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct PageEvent {
|
||||
pub pgno: u32,
|
||||
pub dbi: u32,
|
||||
pub op: PageOp,
|
||||
}
|
||||
|
||||
impl PageEvent {
|
||||
fn decode(raw: u64) -> Option<Self> {
|
||||
let op_byte = (raw >> 56) as u8;
|
||||
let op = PageOp::from_u8(op_byte)?;
|
||||
let dbi = ((raw >> 32) & 0x00FF_FFFF) as u32;
|
||||
let pgno = raw as u32;
|
||||
Some(Self { pgno, dbi, op })
|
||||
}
|
||||
}
|
||||
|
||||
unsafe extern "C" {
|
||||
fn mdbx_pageviz_create() -> *mut std::ffi::c_void;
|
||||
fn mdbx_pageviz_destroy(state: *mut std::ffi::c_void);
|
||||
fn mdbx_pageviz_enable(state: *mut std::ffi::c_void);
|
||||
fn mdbx_pageviz_disable(state: *mut std::ffi::c_void);
|
||||
fn mdbx_pageviz_drain(
|
||||
state: *mut std::ffi::c_void,
|
||||
ring_idx: u32,
|
||||
out_buf: *mut u64,
|
||||
max_count: u32,
|
||||
) -> u32;
|
||||
fn mdbx_pageviz_ring_count(state: *mut std::ffi::c_void) -> u32;
|
||||
fn mdbx_pageviz_dropped(state: *mut std::ffi::c_void, ring_idx: u32) -> u64;
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct PageVizStats {
|
||||
pub ring_count: u32,
|
||||
pub total_dropped: u64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
struct StatePtr(*mut std::ffi::c_void);
|
||||
|
||||
// SAFETY: The C state is thread-safe (uses atomics internally).
|
||||
unsafe impl Send for StatePtr {}
|
||||
unsafe impl Sync for StatePtr {}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct PageVizDrainer {
|
||||
state: StatePtr,
|
||||
running: Arc<AtomicBool>,
|
||||
handle: Option<std::thread::JoinHandle<()>>,
|
||||
}
|
||||
|
||||
const DRAIN_BUF_LEN: usize = 4096;
|
||||
const OVERLOAD_THRESHOLD: usize = 200_000;
|
||||
const READ_SAMPLE_RATE: usize = 8;
|
||||
|
||||
impl PageVizDrainer {
|
||||
pub fn new() -> Self {
|
||||
let state = StatePtr(unsafe { mdbx_pageviz_create() });
|
||||
Self { state, running: Arc::new(AtomicBool::new(false)), handle: None }
|
||||
}
|
||||
|
||||
pub fn enable(&self) {
|
||||
unsafe { mdbx_pageviz_enable(self.state.0) }
|
||||
}
|
||||
|
||||
pub fn disable(&self) {
|
||||
unsafe { mdbx_pageviz_disable(self.state.0) }
|
||||
}
|
||||
|
||||
pub fn start(&mut self, tick_hz: u32) -> mpsc::Receiver<Vec<PageEvent>> {
|
||||
let (tx, rx) = mpsc::channel();
|
||||
let running = self.running.clone();
|
||||
running.store(true, Ordering::SeqCst);
|
||||
|
||||
let state_addr = self.state.0 as usize;
|
||||
let tick_interval = Duration::from_micros(1_000_000 / u64::from(tick_hz));
|
||||
|
||||
let handle = std::thread::Builder::new()
|
||||
.name("mdbx-pageviz".into())
|
||||
.spawn(move || {
|
||||
let state = state_addr as *mut std::ffi::c_void;
|
||||
let mut buf = [0u64; DRAIN_BUF_LEN];
|
||||
let mut coalesced: HashMap<u32, PageEvent> = HashMap::new();
|
||||
let mut markers: Vec<PageEvent> = Vec::new();
|
||||
let mut read_counter: usize = 0;
|
||||
|
||||
while running.load(Ordering::Relaxed) {
|
||||
let tick_start = Instant::now();
|
||||
|
||||
let ring_count = unsafe { mdbx_pageviz_ring_count(state) };
|
||||
|
||||
for ring_idx in 0..ring_count {
|
||||
let dropped = unsafe { mdbx_pageviz_dropped(state, ring_idx) };
|
||||
if dropped > 0 {
|
||||
tracing::warn!(
|
||||
target: "libmdbx::pageviz",
|
||||
ring_idx,
|
||||
dropped,
|
||||
"pageviz ring dropped events"
|
||||
);
|
||||
}
|
||||
|
||||
loop {
|
||||
let count = unsafe {
|
||||
mdbx_pageviz_drain(
|
||||
state,
|
||||
ring_idx,
|
||||
buf.as_mut_ptr(),
|
||||
DRAIN_BUF_LEN as u32,
|
||||
)
|
||||
};
|
||||
if count == 0 {
|
||||
break;
|
||||
}
|
||||
for &raw in &buf[..count as usize] {
|
||||
if let Some(evt) = PageEvent::decode(raw) {
|
||||
if evt.op.is_block_marker() {
|
||||
markers.push(evt);
|
||||
} else {
|
||||
coalesced.insert(evt.pgno, evt);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !coalesced.is_empty() || !markers.is_empty() {
|
||||
let mut events: Vec<PageEvent> = if coalesced.len() > OVERLOAD_THRESHOLD {
|
||||
coalesced
|
||||
.drain()
|
||||
.filter(|(_, evt)| {
|
||||
if evt.op == PageOp::Read {
|
||||
read_counter += 1;
|
||||
read_counter % READ_SAMPLE_RATE == 0
|
||||
} else {
|
||||
true
|
||||
}
|
||||
})
|
||||
.map(|(_, evt)| evt)
|
||||
.collect()
|
||||
} else {
|
||||
coalesced.drain().map(|(_, evt)| evt).collect()
|
||||
};
|
||||
|
||||
events.extend(markers.drain(..));
|
||||
|
||||
if tx.send(events).is_err() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
let elapsed = tick_start.elapsed();
|
||||
if let Some(remaining) = tick_interval.checked_sub(elapsed) {
|
||||
std::thread::sleep(remaining);
|
||||
}
|
||||
}
|
||||
})
|
||||
.expect("failed to spawn mdbx-pageviz thread");
|
||||
|
||||
self.handle = Some(handle);
|
||||
rx
|
||||
}
|
||||
|
||||
pub fn stop(&mut self) {
|
||||
self.running.store(false, Ordering::SeqCst);
|
||||
if let Some(handle) = self.handle.take() {
|
||||
let _ = handle.join();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn stats(&self) -> PageVizStats {
|
||||
let ring_count = unsafe { mdbx_pageviz_ring_count(self.state.0) };
|
||||
let mut total_dropped = 0u64;
|
||||
for ring_idx in 0..ring_count {
|
||||
total_dropped += unsafe { mdbx_pageviz_dropped(self.state.0, ring_idx) };
|
||||
}
|
||||
PageVizStats { ring_count, total_dropped }
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for PageVizDrainer {
|
||||
fn drop(&mut self) {
|
||||
self.stop();
|
||||
unsafe {
|
||||
mdbx_pageviz_disable(self.state.0);
|
||||
mdbx_pageviz_destroy(self.state.0);
|
||||
}
|
||||
}
|
||||
}
|
||||
1381
crates/storage/mdbx-viz/Cargo.lock
generated
1381
crates/storage/mdbx-viz/Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -1,23 +0,0 @@
|
||||
[package]
|
||||
name = "reth-mdbx-viz"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
rust-version.workspace = true
|
||||
license = "Apache-2.0"
|
||||
homepage.workspace = true
|
||||
repository.workspace = true
|
||||
description = "Real-time MDBX page access visualization server"
|
||||
|
||||
[dependencies]
|
||||
reth-libmdbx = { workspace = true, features = ["pageviz"] }
|
||||
reth-mdbx-sys = { workspace = true, features = ["pageviz"] }
|
||||
axum = { version = "0.7", features = ["ws"] }
|
||||
tokio = { workspace = true, features = ["full"] }
|
||||
serde = { workspace = true, features = ["derive"] }
|
||||
serde_json.workspace = true
|
||||
clap = { version = "4", features = ["derive"] }
|
||||
tracing.workspace = true
|
||||
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
||||
rayon.workspace = true
|
||||
eyre.workspace = true
|
||||
libc = "0.2"
|
||||
@@ -1,952 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>MDBX Live Viz</title>
|
||||
<style>
|
||||
*{margin:0;padding:0;box-sizing:border-box}
|
||||
html,body{background:#0d1117;color:#c9d1d9;font-family:'SF Mono','Fira Code','Cascadia Code',monospace;height:100%;overflow:hidden}
|
||||
body{display:flex;flex-direction:row;height:100%}
|
||||
#main-panel{flex:1;display:flex;flex-direction:column;padding:16px;overflow:hidden;min-width:0}
|
||||
#side-panel{flex:0 0 540px;border-left:1px solid #30363d;overflow-y:auto;padding:16px;background:#161b22}
|
||||
h1{color:#f0f6fc;font-size:18px;margin-bottom:2px}
|
||||
.subtitle{color:#8b949e;font-size:12px;margin-bottom:10px}
|
||||
.sub-row{display:flex;align-items:baseline;gap:12px;margin-bottom:6px;flex-wrap:wrap}
|
||||
.sub-row .subtitle{margin-bottom:0;flex-shrink:0}
|
||||
.sub-row .viz-controls{margin-bottom:0;margin-left:auto}
|
||||
.controls{margin-bottom:6px;display:flex;gap:8px;align-items:center;flex-wrap:wrap}
|
||||
.controls button{background:#21262d;border:1px solid #30363d;color:#c9d1d9;padding:3px 10px;border-radius:4px;cursor:pointer;font-family:inherit;font-size:12px}
|
||||
.controls button:hover{background:#30363d}
|
||||
.zoom-info{font-size:11px;color:#8b949e}
|
||||
.status-bar{display:flex;gap:12px;align-items:center;margin-bottom:6px;font-size:11px}
|
||||
.dot{display:inline-block;width:8px;height:8px;border-radius:50%;margin-right:4px;vertical-align:middle}
|
||||
.dot.connected{background:#4ade80}.dot.disconnected{background:#f87171}.dot.connecting{background:#facc15}
|
||||
.stat-label{color:#8b949e}.stat-value{color:#c9d1d9;font-weight:600}
|
||||
#canvas-wrap{border:1px solid #30363d;border-radius:6px;overflow:hidden;background:#000;position:relative;cursor:crosshair;flex:1;min-height:0}
|
||||
canvas{display:block;image-rendering:pixelated;width:100%;height:100%}
|
||||
#tooltip{position:fixed;background:#1c2128;border:1px solid #30363d;border-radius:6px;padding:8px 12px;font-size:12px;pointer-events:none;display:none;z-index:100;box-shadow:0 4px 12px rgba(0,0,0,.4);max-width:360px}
|
||||
#tooltip .tt-row{display:flex;align-items:center;gap:6px}
|
||||
#tooltip .tt-swatch{width:10px;height:10px;border-radius:2px;flex-shrink:0;border:1px solid rgba(255,255,255,0.2)}
|
||||
#tooltip .tt-name{color:#f0f6fc;font-weight:600}
|
||||
#tooltip .tt-detail{color:#8b949e;margin-top:2px}
|
||||
#side-panel h2{color:#f0f6fc;font-size:14px;margin-bottom:8px}
|
||||
.viz-controls{margin-bottom:12px;font-size:11px}
|
||||
.viz-controls label{display:inline-flex;align-items:center;gap:4px;margin-right:10px;cursor:pointer;color:#8b949e}
|
||||
.viz-controls label:hover{color:#c9d1d9}
|
||||
.viz-controls input[type=checkbox]{accent-color:#58a6ff}
|
||||
.viz-controls input[type=range]{width:100px;accent-color:#58a6ff}
|
||||
.fade-val{color:#c9d1d9;min-width:35px;display:inline-block}
|
||||
table{width:100%;border-collapse:collapse;font-size:11px;background:#0d1117;border:1px solid #30363d;border-radius:6px;overflow:hidden;table-layout:fixed}
|
||||
td,th{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
||||
th{text-align:left;padding:6px 8px;background:#1c2128;color:#8b949e;font-weight:500;border-bottom:1px solid #30363d;font-size:10px;text-transform:uppercase;position:sticky;top:0}
|
||||
td{padding:5px 8px;border-bottom:1px solid #21262d}
|
||||
tr.tbl-row{cursor:pointer;transition:background 0.1s}
|
||||
tr.tbl-row:hover td{background:#1c2128}
|
||||
tr.tbl-row.active td{background:#1c2128;outline:1px solid #58a6ff}
|
||||
.pct-bar{display:inline-block;height:6px;border-radius:3px;margin-right:6px;vertical-align:middle}
|
||||
.swatch-cell{width:12px;height:12px;border-radius:2px;flex-shrink:0}
|
||||
#loading{display:flex;flex-direction:column;align-items:center;justify-content:center;height:100%;color:#8b949e;font-size:14px;gap:12px}
|
||||
#load-bar-wrap{width:280px;height:6px;background:#21262d;border-radius:3px;overflow:hidden;display:none}
|
||||
#load-bar{height:100%;width:0;background:#58a6ff;border-radius:3px;transition:width 0.15s}
|
||||
#flame-wrap{border:1px solid #30363d;border-radius:0 0 6px 6px;margin-top:-1px;background:#0d1117}
|
||||
#flame-wrap canvas{display:block}
|
||||
.spark-row{display:flex;align-items:center;gap:6px;margin-bottom:3px;font-size:10px}
|
||||
.spark-label{width:100px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:#8b949e}
|
||||
.spark-cv{border:1px solid #21262d;border-radius:2px;background:#0d1117}
|
||||
.spark-val{color:#c9d1d9;min-width:30px;text-align:right}
|
||||
.tree-card{background:#1c2128;border:1px solid #30363d;border-radius:4px;padding:6px 8px;margin-bottom:4px;font-size:10px;cursor:pointer}
|
||||
.tree-card:hover{border-color:#58a6ff}
|
||||
.tree-card .tree-name{color:#f0f6fc;font-weight:600;font-size:11px}
|
||||
.tree-card .tree-stat{color:#8b949e;margin-top:2px}
|
||||
.tree-card .tree-bar{display:flex;gap:1px;margin-top:3px;height:6px;border-radius:2px;overflow:hidden}
|
||||
.tree-card .tree-seg{height:100%}
|
||||
.cache-stat{display:flex;justify-content:space-between;align-items:center;font-size:10px;padding:2px 0}
|
||||
.cache-bar{height:4px;border-radius:2px;background:#21262d;flex:1;margin:0 6px;position:relative;overflow:hidden}
|
||||
.cache-fill{height:100%;border-radius:2px;background:#4ade80;position:absolute;left:0;top:0}
|
||||
.cache-pct{color:#4ade80;font-weight:600;min-width:36px;text-align:right}
|
||||
.cache-label{color:#8b949e;min-width:80px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
||||
.fault-cnt{color:#d946ef;font-size:10px;margin-left:4px}
|
||||
.block-card{background:#1c2128;border:1px solid #30363d;border-radius:4px;padding:6px 8px;margin-bottom:3px;font-size:10px;cursor:pointer;transition:border-color 0.15s}
|
||||
.block-card:hover{border-color:#58a6ff}
|
||||
.block-card.replaying{border-color:#f59e0b;background:#1e1b13}
|
||||
.block-card.recording{border-color:#4ade80;animation:pulse-border 1s infinite}
|
||||
.block-card .block-num{color:#f0f6fc;font-weight:600;font-size:11px}
|
||||
.block-card .block-stat{color:#8b949e;margin-top:1px}
|
||||
@keyframes pulse-border{0%,100%{border-color:#4ade80}50%{border-color:#166534}}
|
||||
#block-live{cursor:pointer;font-size:10px;margin-left:6px;user-select:none}
|
||||
.utbl-row{display:flex;align-items:center;gap:6px;padding:3px 4px;font-size:10px;cursor:pointer}
|
||||
.utbl-row:hover{background:#1c2128}
|
||||
.utbl-row.active{background:#1c2128;outline:1px solid #58a6ff}
|
||||
.utbl-sw{width:8px;height:8px;border-radius:2px;flex-shrink:0}
|
||||
.utbl-name{width:110px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:#8b949e}
|
||||
.utbl-spark{flex-shrink:0}
|
||||
.utbl-cache{flex:1;min-width:0}
|
||||
.utbl-cache-bar-wrap{width:100%;height:6px;background:#21262d;border-radius:3px;overflow:hidden}
|
||||
.utbl-cache-fill{height:100%;border-radius:3px;transition:width 0.3s}
|
||||
.utbl-cache-pct{text-align:center;font-size:9px;color:#4ade80;font-weight:600;margin-top:1px;line-height:1}
|
||||
.utbl-diff{width:52px;text-align:right;font-size:9px}
|
||||
.utbl-faults{width:36px;text-align:right;color:#d946ef;font-size:9px}
|
||||
#block-panel::-webkit-scrollbar,#side-panel::-webkit-scrollbar{width:6px}
|
||||
#block-panel::-webkit-scrollbar-track,#side-panel::-webkit-scrollbar-track{background:#161b22}
|
||||
#block-panel::-webkit-scrollbar-thumb,#side-panel::-webkit-scrollbar-thumb{background:#30363d;border-radius:3px}
|
||||
#block-panel::-webkit-scrollbar-thumb:hover,#side-panel::-webkit-scrollbar-thumb:hover{background:#484f58}
|
||||
#block-panel,#side-panel{scrollbar-width:thin;scrollbar-color:#30363d #161b22}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="main-panel">
|
||||
<h1>MDBX Live Viz</h1>
|
||||
<div class="sub-row">
|
||||
<div class="subtitle" id="subtitle">Loading...</div>
|
||||
<div class="viz-controls">
|
||||
<label><input type="checkbox" id="chk-read" checked> <span style="color:#3c82f6">■</span> Reads</label>
|
||||
<label><input type="checkbox" id="chk-write" checked> <span style="color:#ef4444">■</span> Writes</label>
|
||||
<label><input type="checkbox" id="chk-free" checked> <span style="color:#eab308">■</span> Frees</label>
|
||||
<label><input type="checkbox" id="chk-fixw"> Fixed Width</label>
|
||||
<label><input type="checkbox" id="chk-trails" checked> Trails</label>
|
||||
<label><input type="checkbox" id="chk-heatmap"> Heatmap</label>
|
||||
<label><input type="checkbox" id="chk-flame"> Timeline</label>
|
||||
<label><input type="checkbox" id="chk-cache"> Page Cache</label>
|
||||
<label>Fade: <input type="range" id="fade-slider" min="200" max="8000" value="2000" step="100"> <span class="fade-val" id="fade-val">2.0s</span></label>
|
||||
<label>Trail W: <input type="range" id="trail-width" min="0.5" max="6" value="1.0" step="0.5"> <span class="fade-val" id="trail-w-val">1.0</span></label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="status-bar">
|
||||
<span><span class="dot connecting" id="ws-dot"></span><span id="ws-status">Connecting</span></span>
|
||||
<span class="stat-label">Events/s: <span class="stat-value" id="evt-rate">0</span></span>
|
||||
<span class="stat-label">Total: <span class="stat-value" id="evt-total">0</span></span>
|
||||
<span class="stat-label">Active: <span class="stat-value" id="active-count">0</span></span>
|
||||
<span class="stat-label">FPS: <span class="stat-value" id="fps-counter">-</span></span>
|
||||
</div>
|
||||
<div class="status-bar">
|
||||
<span class="stat-label" style="color:#3c82f6">R: <span class="stat-value" id="cnt-read">0</span></span>
|
||||
<span class="stat-label" style="color:#ef4444">W: <span class="stat-value" id="cnt-write">0</span></span>
|
||||
<span class="stat-label" style="color:#eab308">F: <span class="stat-value" id="cnt-free">0</span></span>
|
||||
<span class="stat-label">Latest: <span class="stat-value" id="evt-latest">-</span></span>
|
||||
<span id="lag-warn" style="display:none;color:#f87171;font-weight:600">⚠ Lagged <span id="lag-count">0</span> msgs</span>
|
||||
</div>
|
||||
<div class="controls">
|
||||
<button id="btn-zin">Zoom +</button>
|
||||
<button id="btn-zout">Zoom −</button>
|
||||
<button id="btn-reset">Reset</button>
|
||||
<button id="btn-clear">Clear Highlights</button>
|
||||
<span class="zoom-info" id="zoom-info"></span>
|
||||
</div>
|
||||
<div id="canvas-wrap">
|
||||
<div id="loading"><span id="load-text">Loading page map…</span><div id="load-bar-wrap"><div id="load-bar"></div></div></div>
|
||||
<canvas id="map" style="display:none"></canvas>
|
||||
</div>
|
||||
<div id="flame-wrap" style="height:0;overflow:hidden;transition:height 0.2s">
|
||||
<canvas id="flame"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
<div id="side-panel">
|
||||
<h2>Blocks <span id="block-live" style="color:#4ade80" onclick="window._goLive&&window._goLive()">● LIVE</span></h2>
|
||||
<div id="block-panel" style="max-height:172px;overflow-y:auto"></div>
|
||||
<h2 style="margin-top:12px">Tables <span id="cache-total" style="color:#8b949e;font-weight:400;font-size:11px"></span> <span id="cache-ago" style="color:#8b949e;font-weight:400;font-size:10px"></span></h2>
|
||||
<div id="unified-tables"></div>
|
||||
<h2 style="margin-top:12px">Table Details</h2>
|
||||
<table><thead><tr><th></th><th>Table</th><th>Pages</th><th>Size</th><th>%</th></tr></thead><tbody id="tbl-body"></tbody></table>
|
||||
<h2 style="margin-top:12px">B-Tree Structure</h2>
|
||||
<div id="tree-info"></div>
|
||||
</div>
|
||||
<div id="tooltip">
|
||||
<div class="tt-row"><div class="tt-swatch" id="tt-swatch"></div><div class="tt-name" id="tt-name"></div></div>
|
||||
<div class="tt-detail" id="tt-detail"></div>
|
||||
</div>
|
||||
<script>
|
||||
(function(){
|
||||
"use strict";
|
||||
function hsl2r(h,s,l){h/=360;s/=100;l/=100;let r,g,b;if(!s){r=g=b=l}else{const q=l<.5?l*(1+s):l+s-l*s,p=2*l-q;const f=(p,q,t)=>{if(t<0)t+=1;if(t>1)t-=1;if(t<1/6)return p+(q-p)*6*t;if(t<.5)return q;if(t<2/3)return p+(q-p)*(2/3-t)*6;return p};r=f(p,q,h+1/3);g=f(p,q,h);b=f(p,q,h-1/3)}return[Math.round(r*255),Math.round(g*255),Math.round(b*255)]}
|
||||
const DBI_HUES=new Float32Array(256);
|
||||
for(let i=2;i<254;i++)DBI_HUES[i]=((i-2)*137.508)%360;
|
||||
const PAL=new Array(256),PAL_DESAT=new Array(256),PAL_DARK=new Array(256),PAL_DD=new Array(256);
|
||||
PAL[0]=hsl2r(220,15,82);PAL_DESAT[0]=hsl2r(220,5,72);PAL_DARK[0]=hsl2r(220,15,28);PAL_DD[0]=hsl2r(220,5,22);
|
||||
PAL[1]=hsl2r(220,30,24);PAL_DESAT[1]=hsl2r(220,8,22);PAL_DARK[1]=hsl2r(220,30,12);PAL_DD[1]=hsl2r(220,8,12);
|
||||
for(let i=2;i<254;i++){const h=DBI_HUES[i];PAL[i]=hsl2r(h,70,55);PAL_DESAT[i]=hsl2r(h,15,50);PAL_DARK[i]=hsl2r(h,70,22);PAL_DD[i]=hsl2r(h,15,20)}
|
||||
PAL[0xFE]=[20,20,23];PAL_DESAT[0xFE]=[20,20,23];PAL_DARK[0xFE]=[12,12,14];PAL_DD[0xFE]=[12,12,14];
|
||||
PAL[0xFF]=[28,28,32];PAL_DESAT[0xFF]=[28,28,32];PAL_DARK[0xFF]=[16,16,18];PAL_DD[0xFF]=[16,16,18];
|
||||
const FLASH_TINT={1:[140,180,255],2:[255,140,140],3:[255,220,100]};
|
||||
let NP=0,PS=4096,GC=0,GR=0,DBIS=[],DBI_IDX={},oMap=null,lOp=null,lTs=null,FADE=2000;
|
||||
let cntR=0,cntW=0,cntF=0;
|
||||
let sR=true,sW=true,sF=true,fW=false,showTrails=true,trailW=1.0;
|
||||
const TRAIL_CAP=4096;let trailBuf=new Array(TRAIL_CAP);let trailStart=0,trailN=0;
|
||||
const TRC={1:"60,130,246",2:"255,80,80",3:"234,179,8"};
|
||||
let activePgs=0;
|
||||
const activeSet=new Set();
|
||||
let oMapDirty=false;
|
||||
let unrefCount=0;
|
||||
let outR=0,outG=0,outB=0;
|
||||
let cacheTotalCached=0,cacheCachedPer=null,cachePrevPer=null,cachePrevTotal=0,cacheStale=true;
|
||||
let hmMode=false,hmCounts=null;
|
||||
let cacheMode=false,resMap=null,faultCounts={},faultWindow=[],faultCurrent={};
|
||||
let cacheLastUpdate=0;
|
||||
let zbCv=null,zbCx=null,zbDirty=true,zbW=0,zbH=0,zbPX=0,zbPY=0,zbSc=0,zbFlt=false;
|
||||
const BASE_LUT=new Uint32Array(256*32*2);
|
||||
function buildLUT(){
|
||||
for(let dbi=0;dbi<256;dbi++){for(let hq=0;hq<32;hq++){const ht=hq/31;for(let c=0;c<2;c++){
|
||||
let cf,cd;
|
||||
if(c){cf=PAL[dbi];cd=PAL_DESAT[dbi]}else{cf=PAL_DARK[dbi];cd=PAL_DD[dbi]}
|
||||
if(!cf)cf=PAL[255];if(!cd)cd=PAL_DESAT[255];
|
||||
const r=Math.round(cd[0]+(cf[0]-cd[0])*ht);
|
||||
const g=Math.round(cd[1]+(cf[1]-cd[1])*ht);
|
||||
const b=Math.round(cd[2]+(cf[2]-cd[2])*ht);
|
||||
BASE_LUT[(dbi*32+hq)*2+c]=(255<<24)|(b<<16)|(g<<8)|r;
|
||||
}}}
|
||||
}
|
||||
buildLUT();
|
||||
function heatQ(pg){if(!hmMode||!hmCounts)return 31;const cnt=hmCounts[pg];return cnt>0?Math.min(31,Math.floor(Math.log2(cnt+1)*31/16)):0}
|
||||
function rollingFaults(dbi){let s=0;for(const w of faultWindow)s+=(w[dbi]||0);return s}
|
||||
function markZbDirty(){zbDirty=true}
|
||||
let blockRecording=null,blockHistory=[],replayMode=false,replayBlockRef=null,replayTimer=null,liveSnapshot=null;
|
||||
const BLOCK_HISTORY_CAP=15;
|
||||
let blockPanelDirty=false;
|
||||
let showFlame=false;const FLAME_SECS=120;let flameData={},flameHead=0;
|
||||
const SPARK_SECS=60;let sparkData={},sparkAccum={};
|
||||
const cv=document.getElementById("map"),cx=cv.getContext("2d"),wr=document.getElementById("canvas-wrap");
|
||||
const ldEl=document.getElementById("loading"),zmEl=document.getElementById("zoom-info");
|
||||
const tt=document.getElementById("tooltip"),ttSw=document.getElementById("tt-swatch");
|
||||
const ttNm=document.getElementById("tt-name"),ttDt=document.getElementById("tt-detail");
|
||||
const tb=document.getElementById("tbl-body");
|
||||
let W=0,H=0,iD=null,oC=null,oCx=null,pX=0,pY=0,sc=1,mS=.01;const xS=128;
|
||||
const sel=new Set();let tEv=0,rEv=0,lRT=performance.now();
|
||||
function cl(v,a,b){return Math.max(a,Math.min(b,v))}
|
||||
function growTo(newNP){
|
||||
if(newNP<=NP)return;
|
||||
const oldNP=NP;
|
||||
const nOm=new Uint8Array(newNP);nOm.set(oMap);nOm.fill(0xFF,oldNP);oMap=nOm;
|
||||
const nOp=new Uint8Array(newNP);nOp.set(lOp);lOp=nOp;
|
||||
const nTs=new Float64Array(newNP);nTs.set(lTs);lTs=nTs;
|
||||
if(hmCounts){const nHm=new Uint32Array(newNP);nHm.set(hmCounts);hmCounts=nHm}
|
||||
if(resMap){const nRm=new Uint8Array(newNP);nRm.set(resMap);resMap=nRm}
|
||||
unrefCount+=newNP-oldNP;
|
||||
NP=newNP;GR=Math.ceil(NP/GC);
|
||||
oMapDirty=true;
|
||||
iD=null;oC=null;markZbDirty();
|
||||
}
|
||||
function oX(){return 0}
|
||||
function oY(){return Math.max(0,(H-GR*sc)/2)}
|
||||
function cP(){const vw=W/sc,vh=H/sc;pX=cl(pX,0,Math.max(0,GC-vw));pY=cl(pY,0,Math.max(0,GR-vh))}
|
||||
function s2w(sx,sy){return{wx:pX+(sx-oX())/sc,wy:pY+(sy-oY())/sc}}
|
||||
function w2p(wx,wy){const px=Math.floor(wx),py=Math.floor(wy);if(px<0||py<0||px>=GC||py>=GR)return-1;const i=py*GC+px;return i<NP?i:-1}
|
||||
|
||||
function trailPush(from,to,op,ts){
|
||||
const idx=(trailStart+trailN)%TRAIL_CAP;
|
||||
if(!trailBuf[idx])trailBuf[idx]={from,to,op,ts};
|
||||
else{trailBuf[idx].from=from;trailBuf[idx].to=to;trailBuf[idx].op=op;trailBuf[idx].ts=ts}
|
||||
if(trailN<TRAIL_CAP)trailN++;
|
||||
else trailStart=(trailStart+1)%TRAIL_CAP;
|
||||
}
|
||||
function trailExpire(now){
|
||||
while(trailN>0){
|
||||
const t=trailBuf[trailStart];
|
||||
if(now-t.ts>=FADE){trailStart=(trailStart+1)%TRAIL_CAP;trailN--}
|
||||
else break;
|
||||
}
|
||||
}
|
||||
function pCBase(pg,flt){
|
||||
if(pg<0||pg>=NP)return false;
|
||||
const ow=oMap[pg];
|
||||
if(flt&&!sel.has(ow)){outR=25;outG=25;outB=28;return true}
|
||||
const hq=heatQ(pg);
|
||||
const cached=(cacheMode&&resMap&&pg<resMap.length)?resMap[pg]?1:0:1;
|
||||
const rgba=BASE_LUT[(ow*32+hq)*2+cached];
|
||||
outR=rgba&0xFF;outG=(rgba>>8)&0xFF;outB=(rgba>>16)&0xFF;
|
||||
return true;
|
||||
}
|
||||
function pC(pg,flt,now){
|
||||
if(!pCBase(pg,flt))return false;
|
||||
const op=lOp[pg];
|
||||
if(op&&((op===1&&sR)||(op===2&&sW)||(op===3&&sF))){
|
||||
const age=now-lTs[pg];
|
||||
if(age>=FADE){lOp[pg]=0;activePgs=Math.max(0,activePgs-1);activeSet.delete(pg)}
|
||||
else{const a2=Math.max(0,age);const t=1-(a2/FADE)*(a2/FADE);const fl=FLASH_TINT[op];const fi=t*0.7;
|
||||
outR=Math.min(255,Math.round(outR+(fl[0]-outR)*fi));
|
||||
outG=Math.min(255,Math.round(outG+(fl[1]-outG)*fi));
|
||||
outB=Math.min(255,Math.round(outB+(fl[2]-outB)*fi))}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
function pg2s(pg){const col=pg%GC,row=Math.floor(pg/GC);return{sx:oX()+(col-pX+.5)*sc,sy:oY()+(row-pY+.5)*sc}}
|
||||
function drawTrails(now){
|
||||
if(!showTrails||trailN===0)return;
|
||||
cx.lineCap="round";cx.lineWidth=trailW;
|
||||
const paths={1:[],2:[],3:[]};
|
||||
for(let j=0;j<trailN;j++){
|
||||
const idx=(trailStart+j)%TRAIL_CAP;
|
||||
const t=trailBuf[idx];
|
||||
const age=now-t.ts;if(age>=FADE)continue;
|
||||
if((t.op===1&&!sR)||(t.op===2&&!sW)||(t.op===3&&!sF))continue;
|
||||
const a=1-Math.pow(age/FADE,2);
|
||||
const f=pg2s(t.from),e=pg2s(t.to);
|
||||
(paths[t.op]||(paths[t.op]=[])).push({fx:f.sx,fy:f.sy,ex:e.sx,ey:e.sy,a});
|
||||
}
|
||||
for(const op of [1,2,3]){
|
||||
const segs=paths[op];if(!segs||!segs.length)continue;
|
||||
const groups={};
|
||||
for(const s of segs){
|
||||
const ak=Math.round(s.a*10);
|
||||
if(!groups[ak])groups[ak]=[];
|
||||
groups[ak].push(s);
|
||||
}
|
||||
const c=TRC[op]||TRC[1];
|
||||
for(const [ak,gs] of Object.entries(groups)){
|
||||
cx.strokeStyle=`rgba(${c},${(ak/10*0.6).toFixed(2)})`;
|
||||
cx.beginPath();
|
||||
for(const s of gs){cx.moveTo(s.fx,s.fy);cx.lineTo(s.ex,s.ey)}
|
||||
cx.stroke();
|
||||
}
|
||||
}
|
||||
}
|
||||
function rebuildZoomBase(flt){
|
||||
if(!zbCv){zbCv=document.createElement("canvas");zbCx=zbCv.getContext("2d")}
|
||||
zbCv.width=W;zbCv.height=H;zbW=W;zbH=H;zbPX=pX;zbPY=pY;zbSc=sc;zbFlt=flt;
|
||||
const zd=zbCx.createImageData(W,H),d=zd.data,ox=oX(),oy=oY(),ips=1/sc;
|
||||
for(let sy=0;sy<H;sy++){
|
||||
const wy0=Math.max(0,Math.floor(pY+(sy-oy)*ips)),wy1=Math.min(GR-1,Math.floor(pY+(sy-oy+1)*ips));
|
||||
for(let sx=0;sx<W;sx++){
|
||||
const wx0=Math.max(0,Math.floor(pX+(sx-ox)*ips)),wx1=Math.min(GC-1,Math.floor(pX+(sx-ox+1)*ips));
|
||||
const o=(sy*W+sx)<<2;
|
||||
if(wx1<0||wy1<0||wx0>=GC||wy0>=GR){d[o]=0;d[o+1]=0;d[o+2]=0;d[o+3]=255;continue}
|
||||
const cpx=Math.floor((wx0+wx1)/2),cpy=Math.floor((wy0+wy1)/2);
|
||||
const ci=cpy*GC+cpx;
|
||||
if(ci>=0&&ci<NP&&pCBase(ci,flt)){d[o]=outR;d[o+1]=outG;d[o+2]=outB}
|
||||
d[o+3]=255;
|
||||
}
|
||||
}
|
||||
zbCx.putImageData(zd,0,0);
|
||||
}
|
||||
function drawActiveOverlay(now,flt){
|
||||
if(activeSet.size===0)return;
|
||||
const ox=oX(),oy=oY(),ips=1/sc;
|
||||
const expired=[];
|
||||
for(const pg of activeSet){
|
||||
const op=lOp[pg];
|
||||
if(!op){expired.push(pg);continue}
|
||||
if((op===1&&!sR)||(op===2&&!sW)||(op===3&&!sF))continue;
|
||||
const age=now-lTs[pg];
|
||||
if(age>=FADE){lOp[pg]=0;activePgs=Math.max(0,activePgs-1);expired.push(pg);continue}
|
||||
pCBase(pg,flt);
|
||||
const a2=Math.max(0,age);const t=1-(a2/FADE)*(a2/FADE);const fl=FLASH_TINT[op];const fi=t*0.7;
|
||||
const r=Math.min(255,Math.round(outR+(fl[0]-outR)*fi));
|
||||
const g=Math.min(255,Math.round(outG+(fl[1]-outG)*fi));
|
||||
const b=Math.min(255,Math.round(outB+(fl[2]-outB)*fi));
|
||||
const col=pg%GC,row=Math.floor(pg/GC);
|
||||
const sx=ox+(col-pX)*sc,sy=oy+(row-pY)*sc;
|
||||
if(sx<-sc||sy<-sc||sx>W||sy>H)continue;
|
||||
const psz=Math.max(1,Math.ceil(sc));
|
||||
cx.fillStyle=`rgb(${r},${g},${b})`;
|
||||
cx.fillRect(Math.floor(sx),Math.floor(sy),psz,psz);
|
||||
}
|
||||
for(const pg of expired)activeSet.delete(pg);
|
||||
}
|
||||
function render(){
|
||||
if(!oMap||W<1||H<1)return;
|
||||
const flt=sel.size>0,now=performance.now();
|
||||
if(sc>=1){
|
||||
const vw=W/sc,vh=H/sc,x0=Math.floor(pX),y0=Math.floor(pY);
|
||||
const x1=Math.min(GC,Math.ceil(pX+vw)+1),y1=Math.min(GR,Math.ceil(pY+vh)+1);
|
||||
const vW=Math.max(1,x1-x0),vH=Math.max(1,y1-y0);
|
||||
if(!oC){oC=document.createElement("canvas");oCx=oC.getContext("2d")}
|
||||
if(oC.width!==vW||oC.height!==vH){oC.width=vW;oC.height=vH}
|
||||
const od=oCx.createImageData(vW,vH),d=od.data;
|
||||
for(let y=0;y<vH;y++){const gy=y0+y;for(let x=0;x<vW;x++){
|
||||
const i=gy*GC+(x0+x),o=(y*vW+x)<<2;
|
||||
const ok=(i>=0&&i<NP)?pC(i,flt,now):false;
|
||||
if(ok){d[o]=outR;d[o+1]=outG;d[o+2]=outB}else{d[o]=0;d[o+1]=0;d[o+2]=0}d[o+3]=255;
|
||||
}}
|
||||
oCx.putImageData(od,0,0);cx.imageSmoothingEnabled=false;cx.clearRect(0,0,W,H);
|
||||
cx.drawImage(oC,oX()-(pX-x0)*sc,oY()-(pY-y0)*sc,vW*sc,vH*sc);
|
||||
drawTrails(now);
|
||||
zmEl.textContent=sc.toFixed(sc>=10?0:1)+" px/page";
|
||||
}else{
|
||||
if(zbDirty||!zbCv||zbW!==W||zbH!==H||zbPX!==pX||zbPY!==pY||zbSc!==sc||zbFlt!==flt){
|
||||
rebuildZoomBase(flt);zbDirty=false;
|
||||
}
|
||||
cx.clearRect(0,0,W,H);
|
||||
cx.drawImage(zbCv,0,0);
|
||||
drawActiveOverlay(now,flt);
|
||||
drawTrails(now);
|
||||
const pp=1/sc;zmEl.textContent="1 px = "+pp.toFixed(pp>=10?0:1)+" pages";
|
||||
}
|
||||
}
|
||||
let anim=false;
|
||||
let fpsFrames=0,fpsLast=performance.now();
|
||||
function rLoop(){
|
||||
render();let act=0;const now=performance.now();
|
||||
act+=activePgs;
|
||||
if(showTrails){trailExpire(now);act+=trailN}
|
||||
document.getElementById("active-count").textContent=act.toLocaleString();
|
||||
fpsFrames++;
|
||||
const fpsNow=performance.now();
|
||||
if(fpsNow-fpsLast>=500){
|
||||
document.getElementById("fps-counter").textContent=Math.round(fpsFrames/((fpsNow-fpsLast)/1000));
|
||||
fpsFrames=0;fpsLast=fpsNow;
|
||||
}
|
||||
if(act>0&&!(replayMode&&!replayTimer))requestAnimationFrame(rLoop);else{anim=false;document.getElementById("fps-counter").textContent="-"}
|
||||
}
|
||||
function kick(){if(!anim){anim=true;requestAnimationFrame(rLoop)}}
|
||||
let schedPending=false;
|
||||
function sched(){if(!schedPending){schedPending=true;requestAnimationFrame(()=>{schedPending=false;render()})}}
|
||||
function szCv(){const w=wr.clientWidth,h=wr.clientHeight;if(w<1||h<1)return;W=w;H=h;cv.width=W;cv.height=H;iD=null}
|
||||
function fmtSz(mb){if(mb>=1024)return(mb/1024).toFixed(1)+" GB";if(mb>=1)return mb.toFixed(1)+" MB";return(mb*1024).toFixed(0)+" KB"}
|
||||
function bldTbl(){
|
||||
tb.innerHTML="";const mx=Math.max(...DBIS.map(d=>d.pct),.1);
|
||||
DBIS.forEach(d=>{const tr=document.createElement("tr");tr.className="tbl-row";tr.dataset.dbiIndex=d.dbi_index;
|
||||
const c=PAL[d.dbi_index]||PAL[255];
|
||||
tr.innerHTML=`<td><div class="swatch-cell" style="background:rgb(${c[0]},${c[1]},${c[2]})"></div></td><td>${d.name}</td><td>${d.pages.toLocaleString()}</td><td>${fmtSz(d.size_mb)}</td><td><span class="pct-bar" style="width:${d.pct/mx*60}px;background:rgb(${c[0]},${c[1]},${c[2]})"></span>${d.pct}%</td>`;
|
||||
tr.addEventListener("click",()=>{const i=d.dbi_index;if(sel.has(i))sel.delete(i);else sel.add(i);uAR();markZbDirty();sched()});
|
||||
tb.appendChild(tr);
|
||||
});
|
||||
}
|
||||
function uAR(){document.querySelectorAll(".tbl-row").forEach(r=>{sel.has(parseInt(r.dataset.dbiIndex,10))?r.classList.add("active"):r.classList.remove("active")});document.querySelectorAll(".utbl-row").forEach(r=>{sel.has(parseInt(r.dataset.dbiIndex,10))?r.classList.add("active"):r.classList.remove("active")})}
|
||||
function reflow(){if(!fW)return;GC=Math.max(1,Math.floor(W/sc));GR=Math.ceil(NP/GC);pX=0;cP()}
|
||||
function boot(){szCv();const aspect=W>0&&H>0?W/H:1;GC=Math.max(1,Math.ceil(Math.sqrt(NP*aspect)));GR=Math.ceil(NP/GC);mS=Math.min(W/GC,H/GR,1);sc=mS;pX=0;pY=0;render()}
|
||||
function zA(ns,sx,sy){ns=cl(ns,mS,xS);if(Math.abs(ns-sc)<1e-6)return;const b=s2w(sx,sy);sc=ns;if(fW){reflow();pY=b.wy-(sy-oY())/sc;cP()}else{pX=b.wx-(sx-oX())/sc;pY=b.wy-(sy-oY())/sc;cP()}sched()}
|
||||
document.getElementById("btn-zin").addEventListener("click",()=>zA(sc*1.3,W>>1,H>>1));
|
||||
document.getElementById("btn-zout").addEventListener("click",()=>zA(sc/1.3,W>>1,H>>1));
|
||||
document.getElementById("btn-reset").addEventListener("click",()=>{sc=mS;pX=0;pY=0;sel.clear();uAR();markZbDirty();render()});
|
||||
document.getElementById("btn-clear").addEventListener("click",()=>{if(lOp)lOp.fill(0);if(hmCounts)hmCounts.fill(0);activePgs=0;activeSet.clear();markZbDirty();sched()});
|
||||
wr.addEventListener("wheel",e=>{e.preventDefault();const r=cv.getBoundingClientRect();zA(sc*Math.exp(-(e.deltaMode===1?e.deltaY*16:e.deltaY)*.002),(e.clientX-r.left)/r.width*W,(e.clientY-r.top)/r.height*H)},{passive:false});
|
||||
let drag=false,lCX=0,lCY=0;
|
||||
cv.addEventListener("pointerdown",e=>{drag=true;lCX=e.clientX;lCY=e.clientY;cv.setPointerCapture(e.pointerId);cv.style.cursor="grabbing"});
|
||||
cv.addEventListener("pointermove",e=>{
|
||||
if(drag){if(!fW)pX-=(e.clientX-lCX)/sc;pY-=(e.clientY-lCY)/sc;lCX=e.clientX;lCY=e.clientY;cP();sched();tt.style.display="none";return}
|
||||
const r=cv.getBoundingClientRect(),sx=(e.clientX-r.left)/r.width*W,sy=(e.clientY-r.top)/r.height*H;
|
||||
const w=s2w(sx,sy),pg=w2p(w.wx,w.wy);if(pg<0){tt.style.display="none";return}
|
||||
const ow=oMap[pg],inf=DBI_IDX[ow],c=PAL[ow]||PAL[255];
|
||||
ttSw.style.background=`rgb(${c[0]},${c[1]},${c[2]})`;
|
||||
let ex="";const op=lOp[pg];if(op===1)ex=" [READ]";else if(op===2)ex=" [WRITE]";else if(op===3)ex=" [FREE]";
|
||||
ttNm.textContent=`Page ${pg.toLocaleString()} \u2014 ${inf?inf.name:"idx "+ow}${ex}`;
|
||||
let dtxt=inf?`${inf.pages.toLocaleString()} pages, ${inf.pct}%`:"";
|
||||
if(hmCounts&&pg<hmCounts.length&&hmCounts[pg]>0)dtxt+=(dtxt?" \u00b7 ":"")+hmCounts[pg].toLocaleString()+" hits";
|
||||
if(resMap&&pg<resMap.length){
|
||||
const cached=resMap[pg];
|
||||
dtxt+=(dtxt?" \u00b7 ":"")+(cached?'<span style="color:#28c840;font-weight:600">CACHED</span>':'<span style="color:#f04040;font-weight:600">NOT CACHED</span>');
|
||||
}
|
||||
ttDt.innerHTML=dtxt;
|
||||
tt.style.display="block";tt.style.left=(e.clientX+14)+"px";tt.style.top=(e.clientY+14)+"px";
|
||||
});
|
||||
cv.addEventListener("pointerup",()=>{drag=false;cv.style.cursor="crosshair"});
|
||||
cv.addEventListener("pointercancel",()=>{drag=false;cv.style.cursor="crosshair"});
|
||||
cv.addEventListener("mouseleave",()=>{tt.style.display="none"});
|
||||
document.getElementById("chk-read").addEventListener("change",e=>{sR=e.target.checked;sched()});
|
||||
document.getElementById("chk-write").addEventListener("change",e=>{sW=e.target.checked;sched()});
|
||||
document.getElementById("chk-free").addEventListener("change",e=>{sF=e.target.checked;sched()});
|
||||
document.getElementById("fade-slider").addEventListener("input",e=>{FADE=parseInt(e.target.value);document.getElementById("fade-val").textContent=(FADE/1000).toFixed(1)+"s"});
|
||||
document.getElementById("chk-trails").addEventListener("change",e=>{showTrails=e.target.checked;if(!showTrails){trailStart=0;trailN=0}sched()});
|
||||
document.getElementById("trail-width").addEventListener("input",e=>{trailW=parseFloat(e.target.value);document.getElementById("trail-w-val").textContent=trailW.toFixed(1);sched()});
|
||||
document.getElementById("chk-heatmap").addEventListener("change",e=>{hmMode=e.target.checked;markZbDirty();sched()});
|
||||
document.getElementById("chk-cache").addEventListener("change",e=>{cacheMode=e.target.checked;markZbDirty();sched()});
|
||||
document.getElementById("chk-flame").addEventListener("change",e=>{
|
||||
showFlame=e.target.checked;const fw=document.getElementById("flame-wrap");
|
||||
const at=DBIS.filter(d=>d.pages>0);fw.style.height=showFlame?Math.min(at.length*8+24,200)+"px":"0";
|
||||
if(showFlame){szFlame();drawFlame()}
|
||||
});
|
||||
document.getElementById("chk-fixw").addEventListener("change",e=>{fW=e.target.checked;if(fW){reflow()}else{GC=Math.ceil(Math.sqrt(NP));GR=Math.ceil(NP/GC);mS=Math.min(W/GC,H/GR,1);pX=0;cP()}sched()});
|
||||
window.addEventListener("resize",()=>{if(!oMap)return;szCv();reflow();mS=Math.min(W/GC,H/GR,1);if(sc<mS)sc=mS;cP();render();if(showFlame){szFlame();drawFlame()}});
|
||||
const wD=document.getElementById("ws-dot"),wS=document.getElementById("ws-status");
|
||||
function connectWS(){
|
||||
const ws=new WebSocket(`${location.protocol==="https:"?"wss:":"ws:"}//${location.host}/ws`);
|
||||
ws.binaryType="arraybuffer";
|
||||
ws.onopen=()=>{wD.className="dot connected";wS.textContent="Connected"};
|
||||
ws.onclose=()=>{wD.className="dot disconnected";wS.textContent="Reconnecting...";setTimeout(connectWS,1000)};
|
||||
ws.onerror=()=>ws.close();
|
||||
let lagTimer=0;
|
||||
ws.onmessage=msg=>{
|
||||
if(typeof msg.data==="string"){try{const j=JSON.parse(msg.data);if(j.lagged){const el=document.getElementById("lag-warn"),lc=document.getElementById("lag-count");lc.textContent=j.lagged.toLocaleString();el.style.display="inline";clearTimeout(lagTimer);lagTimer=setTimeout(()=>{el.style.display="none"},5000)}}catch(e){}return}
|
||||
if(!oMap)return;const v=new DataView(msg.data);const now=performance.now();let cnt=0;
|
||||
let lastPg=-1,lastDbi=0,lastOp=0,prevPg=-1;
|
||||
{let scan=0,maxPg=NP;while(scan+8<=msg.data.byteLength){const pg=v.getUint32(scan,true),op=v.getUint8(scan+6);scan+=8;if(op>=4)continue;if(pg>=maxPg)maxPg=pg+1}if(maxPg>NP)growTo(maxPg)}
|
||||
let off=0;
|
||||
while(off+8<=msg.data.byteLength){
|
||||
const pg=v.getUint32(off,true),dbi=v.getUint16(off+4,true),op=v.getUint8(off+6),rsv=v.getUint8(off+7);off+=8;
|
||||
if(op===4){if(!replayMode){blockRecording={number:pg,events:[],before:{},faults:0,startTime:performance.now()};blockPanelDirty=true}continue}
|
||||
if(op===5){
|
||||
if(!replayMode&&blockRecording&&blockRecording.number===pg){
|
||||
const txCount=dbi&0xFF,gasEnc=(dbi>>8)&0xFF;
|
||||
blockRecording.txCount=txCount;
|
||||
blockRecording.duration=rsv>0?Math.pow(2,rsv/20.0):0;
|
||||
blockRecording.gasUsed=gasEnc>0?Math.pow(2,gasEnc/10.0):0;
|
||||
let r=0,w=0,f=0;
|
||||
for(const e of blockRecording.events){if(e.op===1)r++;else if(e.op===2)w++;else if(e.op===3)f++}
|
||||
blockRecording.reads=r;blockRecording.writes=w;blockRecording.frees=f;
|
||||
blockRecording.uniquePages=Object.keys(blockRecording.before).length;
|
||||
blockRecording.panelSnapshot={
|
||||
dbiPages:DBIS.map(d=>d.pages),
|
||||
cacheCachedPer:cacheCachedPer?cacheCachedPer.slice():null,
|
||||
cachePrevPer:cachePrevPer?cachePrevPer.slice():null,
|
||||
cacheTotalCached,cachePrevTotal,unrefCount,
|
||||
faultWindow:faultWindow.map(w=>({...w})),
|
||||
faultCurrent:{...faultCurrent}
|
||||
};
|
||||
blockHistory.push(blockRecording);
|
||||
if(blockHistory.length>BLOCK_HISTORY_CAP)blockHistory.shift();
|
||||
blockRecording=null;
|
||||
blockPanelDirty=true;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if(blockRecording){
|
||||
if(!(pg in blockRecording.before))blockRecording.before[pg]={om:oMap[pg],rm:resMap?resMap[pg]:0};
|
||||
blockRecording.events.push({pg,dbi,op});
|
||||
}
|
||||
if(pg>=NP)continue;cnt++;
|
||||
if(op===1)cntR++;else if(op===2)cntW++;else if(op===3)cntF++;
|
||||
if(cacheMode&&resMap&&op===1&&pg<resMap.length&&!resMap[pg]){faultCounts[dbi]=(faultCounts[dbi]||0)+1;faultCurrent[dbi]=(faultCurrent[dbi]||0)+1;if(blockRecording){blockRecording.faults=(blockRecording.faults||0)+1}}
|
||||
if(resMap&&pg<resMap.length&&op!==3)resMap[pg]=1;
|
||||
if(hmCounts)hmCounts[pg]++;
|
||||
if(sparkAccum[dbi]!==undefined)sparkAccum[dbi]++;
|
||||
if(op===2&&dbi>0&&dbi<0xFE){const old=oMap[pg];if(old!==dbi){oMap[pg]=dbi;oMapDirty=true;
|
||||
const di=DBI_IDX[dbi];if(di)di.pages++;
|
||||
const oi=DBI_IDX[old];if(oi&&oi.pages>0)oi.pages--;
|
||||
if(old===0xFF)unrefCount--;
|
||||
}}
|
||||
if(op===3){const old=oMap[pg];if(old!==1){oMap[pg]=1;oMapDirty=true;
|
||||
const di=DBI_IDX[1];if(di)di.pages++;
|
||||
const oi=DBI_IDX[old];if(oi&&oi.pages>0)oi.pages--;
|
||||
if(old===0xFF)unrefCount--;
|
||||
}}
|
||||
if(replayMode)continue;
|
||||
const prev=lOp[pg],prevLive=prev&&(now-lTs[pg])<FADE;
|
||||
if(!prevLive||op>=prev){
|
||||
if(prev===0)activePgs++;
|
||||
lOp[pg]=op;lTs[pg]=now;
|
||||
activeSet.add(pg);
|
||||
}
|
||||
if(showTrails&&prevPg>=0&&prevPg!==pg&&!((op===1&&!sR)||(op===2&&!sW)||(op===3&&!sF)))trailPush(prevPg,pg,op,now);
|
||||
prevPg=pg;
|
||||
if(op>=2){lastPg=pg;lastDbi=dbi;lastOp=op}else if(lastPg<0){lastPg=pg;lastDbi=dbi;lastOp=op}
|
||||
}
|
||||
tEv+=cnt;rEv+=cnt;document.getElementById("evt-total").textContent=tEv.toLocaleString();
|
||||
document.getElementById("cnt-read").textContent=cntR.toLocaleString();
|
||||
document.getElementById("cnt-write").textContent=cntW.toLocaleString();
|
||||
document.getElementById("cnt-free").textContent=cntF.toLocaleString();
|
||||
if(lastPg>=0){const inf=DBI_IDX[lastDbi];const nm=inf?inf.name:"dbi"+lastDbi;const opN=lastOp===1?"R":lastOp===2?"W":"F";document.getElementById("evt-latest").textContent=opN+" pg"+lastPg.toLocaleString()+" "+nm}
|
||||
const el=now-lRT;if(el>=1000){document.getElementById("evt-rate").textContent=Math.round(rEv/(el/1000)).toLocaleString();rEv=0;lRT=now}
|
||||
if(cnt>0&&!replayMode)kick();
|
||||
};
|
||||
}
|
||||
function szFlame(){
|
||||
const fw=document.getElementById("flame-wrap"),fc=document.getElementById("flame");
|
||||
if(!fw||!fc)return;fc.width=fw.clientWidth||300;fc.height=fw.clientHeight||100;
|
||||
}
|
||||
function drawFlame(){
|
||||
const fc=document.getElementById("flame");if(!fc)return;
|
||||
const ctx=fc.getContext("2d"),cw=fc.width,ch=fc.height;ctx.clearRect(0,0,cw,ch);
|
||||
const at=DBIS.filter(d=>d.pages>0);if(!at.length)return;
|
||||
const lw=42,rw=cw-lw,rowH=Math.max(4,Math.floor((ch-14)/Math.max(1,at.length)));
|
||||
ctx.font="7px monospace";
|
||||
for(let r=0;r<at.length;r++){
|
||||
const d=at[r],fd=flameData[d.dbi_index];if(!fd)continue;
|
||||
const c=PAL[d.dbi_index]||PAL[255],y=r*rowH;
|
||||
let mx=1;for(let s=0;s<FLAME_SECS;s++){const v=fd[s];if(v>mx)mx=v}
|
||||
for(let s=0;s<FLAME_SECS;s++){
|
||||
const idx=((flameHead-FLAME_SECS+s)%FLAME_SECS+FLAME_SECS)%FLAME_SECS;
|
||||
const ops=fd[idx];if(ops<=0)continue;
|
||||
const x=lw+(s/FLAME_SECS)*rw,w=Math.ceil(rw/FLAME_SECS);
|
||||
const a=Math.min(1,Math.log2(ops+1)/Math.log2(mx+1));
|
||||
ctx.fillStyle=`rgba(${c[0]},${c[1]},${c[2]},${a.toFixed(2)})`;
|
||||
ctx.fillRect(x,y,w,rowH-1);
|
||||
}
|
||||
ctx.fillStyle="#8b949e";ctx.fillText(d.name.slice(0,6),2,y+rowH-2);
|
||||
}
|
||||
const ay=at.length*rowH+2;ctx.fillStyle="#30363d";ctx.fillRect(lw,ay,rw,1);
|
||||
ctx.fillStyle="#8b949e";ctx.font="7px monospace";
|
||||
for(let s=30;s<FLAME_SECS;s+=30){
|
||||
const x=lw+((FLAME_SECS-s)/FLAME_SECS)*rw;
|
||||
ctx.fillText("-"+s+"s",x,ay+10);
|
||||
}
|
||||
}
|
||||
function buildUnifiedPanel(){
|
||||
const el=document.getElementById("unified-tables");if(!el)return;el.innerHTML="";
|
||||
const hdr=document.createElement("div");hdr.className="utbl-row";hdr.style.cssText="cursor:default;border-bottom:1px solid #30363d;margin-bottom:2px;padding-bottom:4px";
|
||||
hdr.innerHTML='<div class="utbl-sw" style="visibility:hidden"></div><div class="utbl-name" style="color:#8b949e;font-weight:500;text-transform:uppercase;font-size:9px">Table</div><div class="utbl-spark" style="width:80px;text-align:center;color:#8b949e;font-weight:500;text-transform:uppercase;font-size:9px">Activity</div><div class="utbl-cache" style="color:#8b949e;font-weight:500;text-transform:uppercase;font-size:9px;text-align:center">Cache</div><div class="utbl-diff" style="color:#8b949e;font-weight:500;text-transform:uppercase;font-size:9px">Diff</div><div class="utbl-faults" style="color:#8b949e;font-weight:500;text-transform:uppercase;font-size:9px" title="Page faults over last 3 residency updates">Fault<span style="font-size:7px;text-transform:none"> (3)</span></div>';
|
||||
el.appendChild(hdr);
|
||||
DBIS.forEach(d=>{
|
||||
if(d.pages<=0&&d.dbi_index>2)return;
|
||||
const c=PAL[d.dbi_index]||PAL[255];
|
||||
const row=document.createElement("div");row.className="utbl-row";
|
||||
row.dataset.dbiIndex=d.dbi_index;
|
||||
const sw=document.createElement("div");sw.className="utbl-sw";
|
||||
sw.style.background=`rgb(${c[0]},${c[1]},${c[2]})`;
|
||||
const nm=document.createElement("div");nm.className="utbl-name";nm.textContent=d.name;
|
||||
const cv=document.createElement("canvas");cv.className="utbl-spark";cv.width=80;cv.height=16;
|
||||
cv.style.cssText="width:80px;height:16px;border:1px solid #21262d;border-radius:2px;background:#0d1117";
|
||||
const cp=document.createElement("div");cp.className="utbl-cache";
|
||||
const cpBar=document.createElement("div");cpBar.className="utbl-cache-bar-wrap";
|
||||
const cpFill=document.createElement("div");cpFill.className="utbl-cache-fill";cpFill.style.width="0%";cpFill.style.background=`rgb(${c[0]},${c[1]},${c[2]})`;
|
||||
cpBar.appendChild(cpFill);
|
||||
const cpPct=document.createElement("div");cpPct.className="utbl-cache-pct";
|
||||
cp.append(cpBar,cpPct);
|
||||
const cd=document.createElement("div");cd.className="utbl-diff";
|
||||
const ft=document.createElement("div");ft.className="utbl-faults";
|
||||
row.append(sw,nm,cv,cp,cd,ft);
|
||||
row.addEventListener("click",()=>{const i=d.dbi_index;if(sel.has(i))sel.delete(i);else sel.add(i);uAR();markZbDirty();sched()});
|
||||
el.appendChild(row);
|
||||
d._sparkCv=cv;d._sparkCtx=cv.getContext("2d");
|
||||
d._cacheEl=cp;d._cacheFill=cpFill;d._cachePct=cpPct;d._diffEl=cd;d._faultEl=ft;
|
||||
});
|
||||
}
|
||||
function drawSparkline(d){
|
||||
const cv=d._sparkCv,ctx=d._sparkCtx;if(!cv||!ctx)return;
|
||||
const sd=sparkData[d.dbi_index];if(!sd)return;
|
||||
const w=cv.width,h=cv.height,c=PAL[d.dbi_index]||PAL[255];
|
||||
ctx.clearRect(0,0,w,h);
|
||||
let mx=1;for(let i=0;i<SPARK_SECS;i++){const v=sd.ring[i];if(v>mx)mx=v}
|
||||
const latest=sd.ring[((sd.head-1)%SPARK_SECS+SPARK_SECS)%SPARK_SECS];
|
||||
ctx.beginPath();ctx.moveTo(0,h);
|
||||
for(let i=0;i<SPARK_SECS;i++){
|
||||
const idx=((sd.head-SPARK_SECS+i)%SPARK_SECS+SPARK_SECS)%SPARK_SECS;
|
||||
const x=(i/(SPARK_SECS-1))*w,y=h-(sd.ring[idx]/mx)*h;
|
||||
if(i===0)ctx.lineTo(x,y);else ctx.lineTo(x,y);
|
||||
}
|
||||
ctx.lineTo(w,h);ctx.closePath();
|
||||
ctx.fillStyle=`rgba(${c[0]},${c[1]},${c[2]},0.3)`;ctx.fill();
|
||||
ctx.beginPath();
|
||||
for(let i=0;i<SPARK_SECS;i++){
|
||||
const idx=((sd.head-SPARK_SECS+i)%SPARK_SECS+SPARK_SECS)%SPARK_SECS;
|
||||
const x=(i/(SPARK_SECS-1))*w,y=h-(sd.ring[idx]/mx)*h;
|
||||
if(i===0)ctx.moveTo(x,y);else ctx.lineTo(x,y);
|
||||
}
|
||||
ctx.strokeStyle=`rgba(${c[0]},${c[1]},${c[2]},0.8)`;ctx.lineWidth=1;ctx.stroke();
|
||||
if(d._sparkVal)d._sparkVal.textContent=latest.toLocaleString();
|
||||
}
|
||||
function buildTreeInfo(info){
|
||||
const el=document.getElementById("tree-info");if(!el)return;el.innerHTML="";
|
||||
info.forEach(ti=>{
|
||||
if(ti.height===0&&ti.branch_pages===0&&ti.leaf_pages===0&&ti.large_pages===0)return;
|
||||
const card=document.createElement("div");card.className="tree-card";
|
||||
const total=ti.branch_pages+ti.leaf_pages+ti.large_pages;
|
||||
const ipl=ti.leaf_pages>0?Math.round(ti.items/ti.leaf_pages):0;
|
||||
let html=`<div class="tree-name">${ti.name} <span style="color:#8b949e;font-weight:400">depth ${ti.height}</span></div>`;
|
||||
html+=`<div class="tree-stat">B:${ti.branch_pages.toLocaleString()} L:${ti.leaf_pages.toLocaleString()} O:${ti.large_pages.toLocaleString()} items:${ti.items.toLocaleString()}</div>`;
|
||||
if(total>0){
|
||||
const bp=Math.max(1,Math.round(ti.branch_pages/total*100)),lp=Math.max(1,Math.round(ti.leaf_pages/total*100)),op=Math.max(1,100-bp-lp);
|
||||
html+=`<div class="tree-bar"><div class="tree-seg" style="width:${bp}%;background:#3c82f6"></div><div class="tree-seg" style="width:${lp}%;background:#4ade80"></div><div class="tree-seg" style="width:${op}%;background:#f58231"></div></div>`;
|
||||
}
|
||||
if(ipl>0)html+=`<div class="tree-stat">~${ipl.toLocaleString()} items/leaf</div>`;
|
||||
card.innerHTML=html;
|
||||
card.addEventListener("click",()=>{const i=ti.dbi_index;if(sel.has(i))sel.delete(i);else sel.add(i);uAR();markZbDirty();sched()});
|
||||
el.appendChild(card);
|
||||
});
|
||||
}
|
||||
function fmtGas(g){if(g>=1e6)return(g/1e6).toFixed(1)+"M";if(g>=1e3)return(g/1e3).toFixed(0)+"K";return g.toFixed(0)}
|
||||
function updateBlockPanel(){
|
||||
blockPanelDirty=false;
|
||||
const el=document.getElementById("block-panel");if(!el)return;
|
||||
el.innerHTML="";
|
||||
if(blockRecording){
|
||||
const c=document.createElement("div");c.className="block-card recording";
|
||||
const elapsed=performance.now()-blockRecording.startTime;
|
||||
const elStr=elapsed<1000?elapsed.toFixed(0)+"ms":(elapsed/1000).toFixed(1)+"s";
|
||||
c.innerHTML=`<div class="block-num">⏱ #${blockRecording.number.toLocaleString()} recording\u2026</div><div class="block-stat">${blockRecording.events.length.toLocaleString()} events ${elStr}</div>`;
|
||||
el.appendChild(c);
|
||||
}
|
||||
for(let i=blockHistory.length-1;i>=0;i--){
|
||||
const b=blockHistory[i],isReplaying=replayMode&&replayBlockRef===b;
|
||||
const c=document.createElement("div");c.className="block-card"+(isReplaying?" replaying":"");
|
||||
const total=b.reads+b.writes+b.frees;
|
||||
const dur=b.duration>0?(b.duration<1000?b.duration.toFixed(0)+"ms":(b.duration/1000).toFixed(1)+"s"):"";
|
||||
const gas=b.gasUsed>0?fmtGas(b.gasUsed)+"gas":"";
|
||||
c.innerHTML=`<div class="block-num">▶ #${b.number.toLocaleString()} ${b.txCount}tx${dur?" "+dur:""}${gas?" "+gas:""} ${total.toLocaleString()} ops</div><div class="block-stat"><span style="color:#3c82f6">R:${b.reads.toLocaleString()}</span> <span style="color:#ef4444">W:${b.writes.toLocaleString()}</span> <span style="color:#eab308">F:${b.frees.toLocaleString()}</span>${b.faults?` <span style="color:#d946ef">${b.faults}f</span>`:""} ${(b.uniquePages||0).toLocaleString()} pages</div>`;
|
||||
c.addEventListener("click",()=>startReplay(b));
|
||||
el.appendChild(c);
|
||||
}
|
||||
}
|
||||
function snapPanelState(){
|
||||
return{dbiPages:DBIS.map(d=>d.pages),cacheCachedPer:cacheCachedPer?cacheCachedPer.slice():null,cachePrevPer:cachePrevPer?cachePrevPer.slice():null,cacheTotalCached,cachePrevTotal,unrefCount,faultWindow:faultWindow.map(w=>({...w})),faultCurrent:{...faultCurrent}};
|
||||
}
|
||||
function applyPanelSnap(snap){
|
||||
DBIS.forEach((d,i)=>{d.pages=snap.dbiPages[i]||0;d.pct=NP>0?Math.round(d.pages/NP*10000)/100:0;d.size_mb=Math.round(d.pages*PS/(1024*1024)*10)/10});
|
||||
if(snap.cacheCachedPer&&cacheCachedPer){for(let i=0;i<cacheCachedPer.length;i++)cacheCachedPer[i]=snap.cacheCachedPer[i]||0}
|
||||
cachePrevPer=snap.cachePrevPer?snap.cachePrevPer.slice():null;
|
||||
cacheTotalCached=snap.cacheTotalCached;cachePrevTotal=snap.cachePrevTotal;
|
||||
unrefCount=snap.unrefCount;
|
||||
faultWindow=snap.faultWindow.map(w=>({...w}));faultCurrent={...snap.faultCurrent};
|
||||
cacheStale=true;bldTbl();updateCacheStats();
|
||||
const rf=NP-unrefCount,gb=(NP*PS/(1024**3)).toFixed(2);
|
||||
document.getElementById("subtitle").textContent=`${NP.toLocaleString()} pages \u00b7 ${rf.toLocaleString()} ref \u00b7 ${unrefCount.toLocaleString()} unref \u00b7 ${PS>=1024?(PS/1024)+"K":PS} \u00b7 ${gb} GB \u00b7 ${DBIS.length} tables`;
|
||||
}
|
||||
function startReplay(block){
|
||||
if(replayMode&&replayBlockRef===block&&replayTimer)return;
|
||||
if(!liveSnapshot)liveSnapshot=snapPanelState();
|
||||
replayMode=true;replayBlockRef=block;
|
||||
if(replayTimer){cancelAnimationFrame(replayTimer);replayTimer=null}
|
||||
lOp.fill(0);activeSet.clear();activePgs=0;
|
||||
if(hmCounts)hmCounts.fill(0);
|
||||
trailStart=0;trailN=0;
|
||||
for(const pgStr in block.before){
|
||||
const pg=parseInt(pgStr,10);if(pg>=NP)continue;
|
||||
const snap=block.before[pg];
|
||||
oMap[pg]=snap.om;
|
||||
if(resMap&&pg<resMap.length)resMap[pg]=snap.rm;
|
||||
}
|
||||
if(block.panelSnapshot)applyPanelSnap(block.panelSnapshot);
|
||||
markZbDirty();sched();
|
||||
document.getElementById("block-live").style.color="#8b949e";
|
||||
updateBlockPanel();
|
||||
const events=block.events;
|
||||
const batchSize=Math.max(1,Math.ceil(events.length/60));
|
||||
let idx=0;
|
||||
function step(){
|
||||
if(!replayMode){replayTimer=null;return}
|
||||
const end=Math.min(idx+batchSize,events.length);
|
||||
const now=performance.now();
|
||||
for(let i=idx;i<end;i++){
|
||||
const e=events[i];
|
||||
if(e.pg>=NP)continue;
|
||||
if(hmCounts)hmCounts[e.pg]++;
|
||||
if(cacheMode&&resMap&&e.op===1&&e.pg<resMap.length&&!resMap[e.pg]){faultCounts[e.dbi]=(faultCounts[e.dbi]||0)+1;faultCurrent[e.dbi]=(faultCurrent[e.dbi]||0)+1}
|
||||
if(resMap&&e.pg<resMap.length&&e.op!==3)resMap[e.pg]=1;
|
||||
lOp[e.pg]=e.op;lTs[e.pg]=now;
|
||||
if(!activeSet.has(e.pg)){activeSet.add(e.pg);activePgs++}
|
||||
if(e.op===2&&e.dbi>0&&e.dbi<0xFE)oMap[e.pg]=e.dbi;
|
||||
if(showTrails&&i>0&&!((e.op===1&&!sR)||(e.op===2&&!sW)||(e.op===3&&!sF))){const p=events[i-1];if(p.pg!==e.pg)trailPush(p.pg,e.pg,e.op,now)}
|
||||
}
|
||||
idx=end;
|
||||
markZbDirty();kick();
|
||||
if(idx<events.length){replayTimer=requestAnimationFrame(step)}
|
||||
else{
|
||||
replayTimer=null;
|
||||
const pin=performance.now()+FADE*10;
|
||||
for(const pg of activeSet)lTs[pg]=pin;
|
||||
markZbDirty();sched();
|
||||
updateBlockPanel();
|
||||
}
|
||||
}
|
||||
replayTimer=requestAnimationFrame(step);
|
||||
}
|
||||
function goLive(){
|
||||
replayMode=false;replayBlockRef=null;
|
||||
if(replayTimer){cancelAnimationFrame(replayTimer);replayTimer=null}
|
||||
lOp.fill(0);activeSet.clear();activePgs=0;
|
||||
trailStart=0;trailN=0;
|
||||
if(liveSnapshot){applyPanelSnap(liveSnapshot);liveSnapshot=null}
|
||||
oMapDirty=true;cacheStale=true;
|
||||
markZbDirty();sched();
|
||||
document.getElementById("block-live").style.color="#4ade80";
|
||||
updateBlockPanel();updateCacheStats();
|
||||
}
|
||||
window._goLive=goLive;
|
||||
function connectResidencyWS(){
|
||||
const ws=new WebSocket(`${location.protocol==="https:"?"wss:":"ws:"}//${location.host}/ws_residency`);
|
||||
ws.binaryType="arraybuffer";
|
||||
ws.onclose=()=>setTimeout(connectResidencyWS,3000);
|
||||
ws.onerror=()=>ws.close();
|
||||
ws.onmessage=msg=>{
|
||||
if(!msg.data||msg.data.byteLength<1)return;
|
||||
if(replayMode)return;
|
||||
const v=new DataView(msg.data);
|
||||
const msgType=v.getUint8(0);
|
||||
if(msgType===0){
|
||||
const src=new Uint8Array(msg.data,1);
|
||||
const newLen=src.length;
|
||||
if(!resMap||resMap.length!==newLen){
|
||||
const oldMap=resMap;const oldLen=oldMap?oldMap.length:0;
|
||||
resMap=new Uint8Array(newLen);
|
||||
if(oldMap){
|
||||
const shared=Math.min(oldLen,newLen,NP);
|
||||
for(let i=0;i<shared;i++){
|
||||
const ns=src[i],os=oldMap[i];
|
||||
if(ns!==os){
|
||||
const dbi=oMap?oMap[i]:0;
|
||||
if(ns&&!os){cacheTotalCached++;if(cacheCachedPer&&dbi<cacheCachedPer.length)cacheCachedPer[dbi]++}
|
||||
else if(!ns&&os){cacheTotalCached--;if(cacheCachedPer&&dbi<cacheCachedPer.length)cacheCachedPer[dbi]--}
|
||||
}
|
||||
resMap[i]=ns;
|
||||
}
|
||||
for(let i=shared;i<newLen;i++){
|
||||
resMap[i]=src[i];
|
||||
if(src[i]){cacheTotalCached++;if(cacheCachedPer&&oMap){const dbi=oMap[i];if(dbi<cacheCachedPer.length)cacheCachedPer[dbi]++}}
|
||||
}
|
||||
}else{
|
||||
resMap.set(src);
|
||||
cacheTotalCached=0;if(cacheCachedPer)cacheCachedPer.fill(0);
|
||||
const lim=Math.min(newLen,NP,oMap?oMap.length:0);
|
||||
for(let i=0;i<lim;i++){
|
||||
if(src[i]){cacheTotalCached++;if(cacheCachedPer){const dbi=oMap[i];if(dbi<cacheCachedPer.length)cacheCachedPer[dbi]++}}
|
||||
}
|
||||
}
|
||||
}else{
|
||||
const lim=Math.min(newLen,NP,oMap?oMap.length:0);
|
||||
for(let i=0;i<lim;i++){
|
||||
const ns=src[i],os=resMap[i];
|
||||
if(ns!==os){
|
||||
const dbi=oMap?oMap[i]:0;
|
||||
if(ns&&!os){cacheTotalCached++;if(cacheCachedPer&&dbi<cacheCachedPer.length)cacheCachedPer[dbi]++}
|
||||
else if(!ns&&os){cacheTotalCached--;if(cacheCachedPer&&dbi<cacheCachedPer.length)cacheCachedPer[dbi]--}
|
||||
}
|
||||
resMap[i]=ns;
|
||||
}
|
||||
}
|
||||
cacheStale=true;
|
||||
cacheLastUpdate=performance.now();
|
||||
faultWindow.push({...faultCurrent});if(faultWindow.length>3)faultWindow.shift();faultCurrent={};
|
||||
if(cacheMode)markZbDirty();
|
||||
updateCacheStats();
|
||||
if(cacheMode)sched();
|
||||
}else if(msgType===1){
|
||||
if(!resMap)return;
|
||||
const spanCount=v.getUint32(1,true);
|
||||
let off=5;
|
||||
for(let s=0;s<spanCount;s++){
|
||||
const start=v.getUint32(off,true);off+=4;
|
||||
const len=v.getUint32(off,true);off+=4;
|
||||
const state=v.getUint8(off);off+=1;
|
||||
for(let p=start;p<start+len&&p<resMap.length;p++){
|
||||
const oldState=resMap[p];
|
||||
if(oldState!==state){
|
||||
const dbi=oMap?oMap[p]:0;
|
||||
if(state&&!oldState){cacheTotalCached++;if(cacheCachedPer&&dbi<cacheCachedPer.length)cacheCachedPer[dbi]++}
|
||||
else if(!state&&oldState){cacheTotalCached--;if(cacheCachedPer&&dbi<cacheCachedPer.length)cacheCachedPer[dbi]--}
|
||||
}
|
||||
resMap[p]=state;
|
||||
}
|
||||
}
|
||||
cacheStale=true;
|
||||
cacheLastUpdate=performance.now();
|
||||
faultWindow.push({...faultCurrent});if(faultWindow.length>3)faultWindow.shift();faultCurrent={};
|
||||
if(cacheMode)markZbDirty();
|
||||
updateCacheStats();
|
||||
if(cacheMode)sched();
|
||||
}
|
||||
};
|
||||
}
|
||||
let cacheStatsPending=false;
|
||||
function updateCacheStats(){
|
||||
if(!cacheStale||!cacheCachedPer)return;
|
||||
if(cacheStatsPending)return;
|
||||
cacheStatsPending=true;
|
||||
requestAnimationFrame(()=>{
|
||||
cacheStatsPending=false;
|
||||
if(!cacheStale||!cacheCachedPer)return;
|
||||
cacheStale=false;
|
||||
const limit=Math.min(resMap?resMap.length:0,NP);
|
||||
const totalPct=limit>0?(cacheTotalCached/limit*100).toFixed(1):"0";
|
||||
const prevTotalPct=limit>0?(cachePrevTotal/limit*100):0;
|
||||
const totalDiff=limit>0?(cacheTotalCached/limit*100-prevTotalPct):0;
|
||||
const tdS=cachePrevPer?(totalDiff>=0?"+":"")+totalDiff.toFixed(2)+"%":"";
|
||||
const tdC=totalDiff>0?"#4ade80":totalDiff<0?"#f87171":"#8b949e";
|
||||
document.getElementById("cache-total").innerHTML=`${cacheTotalCached.toLocaleString()}/${limit.toLocaleString()} (${totalPct}%) ${cachePrevPer?`<span style="color:${tdC}">${tdS}</span>`:""}`;
|
||||
DBIS.forEach(d=>{
|
||||
if(!d._cacheEl)return;
|
||||
const total=d.pages;if(total<=0){if(d._cachePct)d._cachePct.textContent="-";if(d._cacheFill)d._cacheFill.style.width="0%";d._diffEl.textContent="";d._faultEl.textContent="";return}
|
||||
const cached=cacheCachedPer[d.dbi_index]||0;
|
||||
const pct=total>0?(cached/total*100):0;
|
||||
if(d._cacheFill)d._cacheFill.style.width=pct.toFixed(1)+"%";
|
||||
if(d._cachePct){d._cachePct.textContent=pct.toFixed(1)+"%";d._cachePct.style.color=pct>50?"#4ade80":"#f87171"}
|
||||
const prevCached=cachePrevPer?cachePrevPer[d.dbi_index]||0:0;
|
||||
const prevPct=total>0?(prevCached/total*100):0;
|
||||
const diff=cachePrevPer?(pct-prevPct):0;
|
||||
if(cachePrevPer){
|
||||
d._diffEl.textContent=(diff>=0?"+":"")+diff.toFixed(2)+"%";
|
||||
d._diffEl.style.color=diff>0?"#4ade80":diff<0?"#f87171":"#8b949e";
|
||||
}
|
||||
const fc=rollingFaults(d.dbi_index);
|
||||
d._faultEl.textContent=fc>0?fc+"f":"";
|
||||
});
|
||||
cachePrevPer=cacheCachedPer.slice();
|
||||
cachePrevTotal=cacheTotalCached;
|
||||
});
|
||||
}
|
||||
async function init(){
|
||||
try{
|
||||
const r=await fetch("/api/info"),info=await r.json();NP=info.page_count;PS=info.page_size;
|
||||
DBIS=info.dbi_names.map((n,i)=>({name:n,dbi_index:i,pages:0,pct:0,size_mb:0}));
|
||||
DBI_IDX={};DBIS.forEach(d=>{DBI_IDX[d.dbi_index]=d});
|
||||
document.getElementById("subtitle").textContent=`${NP.toLocaleString()} pages \u00b7 loading owner map\u2026`;
|
||||
}catch(e){document.getElementById("subtitle").textContent="Failed: "+e.message}
|
||||
try{
|
||||
const lt=document.getElementById("load-text"),lbw=document.getElementById("load-bar-wrap"),lb=document.getElementById("load-bar");
|
||||
lt.textContent="Downloading owner map\u2026";lbw.style.display="block";
|
||||
const r=await fetch("/api/owner_map");
|
||||
if(r.ok){
|
||||
const total=parseInt(r.headers.get("content-length")||"0",10)||NP;
|
||||
const reader=r.body.getReader();
|
||||
const buf=new Uint8Array(total||NP);let pos=0;
|
||||
for(;;){const{done,value}=await reader.read();if(done)break;buf.set(value,pos);pos+=value.length;
|
||||
const pct=total>0?Math.min(100,pos/total*100):0;
|
||||
lb.style.width=pct.toFixed(1)+"%";
|
||||
lt.textContent=`Downloading owner map\u2026 ${(pos/(1024*1024)).toFixed(1)}/${(total/(1024*1024)).toFixed(1)} MB`;
|
||||
}
|
||||
oMap=buf.length===pos?buf:buf.slice(0,pos);
|
||||
lt.textContent="Processing owner map\u2026";lb.style.width="100%";
|
||||
await new Promise(r=>setTimeout(r,0));
|
||||
const ct={};for(let i=0;i<NP;i++){const o=oMap[i];ct[o]=(ct[o]||0)+1}
|
||||
DBIS.forEach(d=>{d.pages=ct[d.dbi_index]||0;d.pct=NP>0?Math.round(d.pages/NP*10000)/100:0;d.size_mb=Math.round(d.pages*PS/(1024*1024)*10)/10});
|
||||
}else{oMap=new Uint8Array(NP);oMap.fill(0xFF)}
|
||||
}catch(e){oMap=new Uint8Array(NP);oMap.fill(0xFF)}
|
||||
unrefCount=oMap.reduce((n,b)=>n+(b===0xFF?1:0),0);const ref=NP-unrefCount;
|
||||
const gb=(NP*PS/(1024**3)).toFixed(2);
|
||||
document.getElementById("subtitle").textContent=`${NP.toLocaleString()} pages \u00b7 ${ref.toLocaleString()} ref \u00b7 ${unrefCount.toLocaleString()} unref \u00b7 ${PS>=1024?(PS/1024)+"K":PS} \u00b7 ${gb} GB \u00b7 ${DBIS.length} tables`;
|
||||
lOp=new Uint8Array(NP);lTs=new Float64Array(NP);hmCounts=new Uint32Array(NP);
|
||||
cacheCachedPer=new Array(DBIS.length).fill(0);
|
||||
DBIS.forEach(d=>{sparkData[d.dbi_index]={ring:new Array(SPARK_SECS).fill(0),head:0};sparkAccum[d.dbi_index]=0;flameData[d.dbi_index]=new Array(FLAME_SECS).fill(0)});
|
||||
bldTbl();buildUnifiedPanel();
|
||||
let treeInfo=[];
|
||||
try{const r=await fetch("/api/tree_info");if(r.ok)treeInfo=await r.json()}catch(e){}
|
||||
if(treeInfo.length>0)buildTreeInfo(treeInfo);
|
||||
ldEl.style.display="none";cv.style.display="block";boot();connectWS();connectResidencyWS();
|
||||
const flCv=document.getElementById("flame");
|
||||
flCv.addEventListener("mousemove",e=>{
|
||||
if(!showFlame)return;
|
||||
const at=DBIS.filter(d=>d.pages>0);if(!at.length){tt.style.display="none";return}
|
||||
const rect=flCv.getBoundingClientRect();
|
||||
const mx=e.clientX-rect.left,my=e.clientY-rect.top;
|
||||
const lw=42,rw=flCv.width-lw;
|
||||
const rowH=Math.max(4,Math.floor((flCv.height-14)/Math.max(1,at.length)));
|
||||
const ri=Math.floor(my/rowH);
|
||||
if(ri<0||ri>=at.length||mx<lw){tt.style.display="none";return}
|
||||
const d=at[ri],c=PAL[d.dbi_index]||PAL[255];
|
||||
const secIdx=Math.floor((mx-lw)/rw*FLAME_SECS);
|
||||
const idx=((flameHead-FLAME_SECS+secIdx)%FLAME_SECS+FLAME_SECS)%FLAME_SECS;
|
||||
const fd=flameData[d.dbi_index];
|
||||
const ops=fd?fd[idx]:0;
|
||||
const ago=FLAME_SECS-secIdx;
|
||||
ttSw.style.background=`rgb(${c[0]},${c[1]},${c[2]})`;
|
||||
ttNm.textContent=d.name;
|
||||
ttDt.innerHTML=`${ops.toLocaleString()} ops/s \u00b7 ${ago}s ago`;
|
||||
tt.style.display="block";tt.style.left=(e.clientX+14)+"px";tt.style.top=(e.clientY+14)+"px";
|
||||
});
|
||||
flCv.addEventListener("mouseleave",()=>{tt.style.display="none"});
|
||||
setInterval(()=>{
|
||||
DBIS.forEach(d=>{
|
||||
const v=sparkAccum[d.dbi_index]||0;
|
||||
const sd=sparkData[d.dbi_index];if(sd){sd.ring[sd.head%SPARK_SECS]=v;sd.head++}
|
||||
const fd=flameData[d.dbi_index];if(fd)fd[flameHead%FLAME_SECS]=v;
|
||||
sparkAccum[d.dbi_index]=0;
|
||||
});
|
||||
flameHead++;
|
||||
DBIS.forEach(d=>drawSparkline(d));
|
||||
if(showFlame)drawFlame();
|
||||
},1000);
|
||||
setInterval(()=>{if(!oMap||!oMapDirty||replayMode)return;oMapDirty=false;
|
||||
DBIS.forEach(d=>{d.pct=NP>0?Math.round(d.pages/NP*10000)/100:0;d.size_mb=Math.round(d.pages*PS/(1024*1024)*10)/10});
|
||||
bldTbl();uAR();markZbDirty();sched();
|
||||
const rf=NP-unrefCount,gb=(NP*PS/(1024**3)).toFixed(2);
|
||||
document.getElementById("subtitle").textContent=`${NP.toLocaleString()} pages \u00b7 ${rf.toLocaleString()} ref \u00b7 ${unrefCount.toLocaleString()} unref \u00b7 ${PS>=1024?(PS/1024)+"K":PS} \u00b7 ${gb} GB \u00b7 ${DBIS.length} tables`;
|
||||
},2000);
|
||||
setInterval(()=>{
|
||||
if(!cacheLastUpdate)return;
|
||||
const ago=Math.round((performance.now()-cacheLastUpdate)/1000);
|
||||
let txt;
|
||||
if(ago<60)txt=ago+"s ago";
|
||||
else if(ago<3600)txt=Math.floor(ago/60)+"m "+ago%60+"s ago";
|
||||
else txt=Math.floor(ago/3600)+"h ago";
|
||||
document.getElementById("cache-ago").textContent=txt;
|
||||
},1000);
|
||||
setInterval(()=>{if(blockPanelDirty)updateBlockPanel()},250);
|
||||
setInterval(()=>{
|
||||
if(activeSet.size<5000)return;
|
||||
const now=performance.now();
|
||||
for(const pg of activeSet){
|
||||
if(!lOp[pg]||now-lTs[pg]>=FADE){activeSet.delete(pg);if(lOp[pg]){lOp[pg]=0;activePgs=Math.max(0,activePgs-1)}}
|
||||
}
|
||||
},5000);
|
||||
}
|
||||
init();
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,577 +0,0 @@
|
||||
use std::path::PathBuf;
|
||||
use std::sync::{
|
||||
atomic::{AtomicUsize, Ordering},
|
||||
mpsc, Arc, RwLock,
|
||||
};
|
||||
|
||||
use axum::{
|
||||
extract::{
|
||||
ws::{Message, WebSocket, WebSocketUpgrade},
|
||||
State,
|
||||
},
|
||||
http::{header, StatusCode},
|
||||
response::{Html, IntoResponse},
|
||||
routing::get,
|
||||
Router,
|
||||
};
|
||||
use reth_libmdbx::pageviz::{PageEvent, PageOp};
|
||||
use serde::Serialize;
|
||||
use tokio::sync::broadcast;
|
||||
|
||||
pub mod walker;
|
||||
|
||||
unsafe extern "C" {
|
||||
fn mdbx_pageviz_emit_block_marker(op: u8, block_number: u32, tx_count: u16, duration_encoded: u8, gas_encoded: u8);
|
||||
}
|
||||
|
||||
pub fn pageviz_emit_block_marker(block_number: u64, start: bool, tx_count: u16, duration_ms: u32, gas_used: u64) {
|
||||
let dur_enc = if duration_ms == 0 { 0u8 } else { ((duration_ms as f64).log2() * 20.0).round().min(255.0) as u8 };
|
||||
let gas_enc = if gas_used == 0 { 0u8 } else { ((gas_used as f64).log2() * 10.0).round().min(255.0) as u8 };
|
||||
unsafe {
|
||||
let op = if start { 4u8 } else { 5u8 };
|
||||
mdbx_pageviz_emit_block_marker(op, block_number as u32, tx_count, dur_enc, gas_enc);
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Default, Serialize)]
|
||||
struct ResidencyStats {
|
||||
total_pages: u64,
|
||||
cached_pages: u64,
|
||||
pct: f64,
|
||||
per_table: Vec<TableResidency>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Default, Serialize)]
|
||||
struct TableResidency {
|
||||
dbi: usize,
|
||||
name: String,
|
||||
total: u64,
|
||||
cached: u64,
|
||||
pct: f64,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct AppState {
|
||||
info: VizInfo,
|
||||
owner_map: Arc<RwLock<Vec<u8>>>,
|
||||
tree_info: Arc<Vec<walker::TreeInfo>>,
|
||||
subscribers: Arc<AtomicUsize>,
|
||||
event_tx: broadcast::Sender<Vec<u8>>,
|
||||
residency: Arc<RwLock<Vec<u8>>>,
|
||||
residency_stats: Arc<RwLock<ResidencyStats>>,
|
||||
residency_tx: broadcast::Sender<Vec<u8>>,
|
||||
residency_subscribers: Arc<AtomicUsize>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
pub struct VizInfo {
|
||||
pub page_count: u64,
|
||||
pub page_size: u32,
|
||||
pub dbi_names: Vec<String>,
|
||||
}
|
||||
|
||||
pub struct VizConfig {
|
||||
pub port: u16,
|
||||
pub page_count: u64,
|
||||
pub page_size: u32,
|
||||
pub dbi_names: Vec<String>,
|
||||
pub owner_map: Vec<u8>,
|
||||
pub tree_info: Vec<walker::TreeInfo>,
|
||||
pub db_path: PathBuf,
|
||||
}
|
||||
|
||||
const WIRE_EVENT_SIZE: usize = 8;
|
||||
|
||||
fn encode_events(events: &[PageEvent]) -> Vec<u8> {
|
||||
let mut buf = Vec::with_capacity(events.len() * WIRE_EVENT_SIZE);
|
||||
for ev in events {
|
||||
buf.extend_from_slice(&ev.pgno.to_le_bytes());
|
||||
if ev.op.is_block_marker() {
|
||||
let tx_lo = (ev.dbi & 0xFF) as u8;
|
||||
let gas = ((ev.dbi >> 24) & 0xFF) as u8;
|
||||
buf.extend_from_slice(&u16::to_le_bytes((gas as u16) << 8 | tx_lo as u16));
|
||||
buf.push(ev.op as u8);
|
||||
buf.push(((ev.dbi >> 16) & 0xFF) as u8);
|
||||
} else {
|
||||
buf.extend_from_slice(&(ev.dbi as u16).to_le_bytes());
|
||||
buf.push(ev.op as u8);
|
||||
buf.push(0u8);
|
||||
}
|
||||
}
|
||||
buf
|
||||
}
|
||||
|
||||
struct ReadOnlyMapping {
|
||||
base: *mut libc::c_void,
|
||||
len: usize,
|
||||
}
|
||||
|
||||
impl ReadOnlyMapping {
|
||||
fn new(path: &std::path::Path) -> Option<Self> {
|
||||
use std::os::unix::io::AsRawFd;
|
||||
let file = std::fs::File::open(path).ok()?;
|
||||
let len = file.metadata().ok()?.len() as usize;
|
||||
if len == 0 {
|
||||
return None;
|
||||
}
|
||||
let base = unsafe {
|
||||
libc::mmap(
|
||||
std::ptr::null_mut(),
|
||||
len,
|
||||
libc::PROT_READ,
|
||||
libc::MAP_SHARED,
|
||||
file.as_raw_fd(),
|
||||
0,
|
||||
)
|
||||
};
|
||||
if base == libc::MAP_FAILED {
|
||||
return None;
|
||||
}
|
||||
Some(Self { base, len })
|
||||
}
|
||||
|
||||
fn remap(&mut self, path: &std::path::Path) -> bool {
|
||||
use std::os::unix::io::AsRawFd;
|
||||
let file = match std::fs::File::open(path) {
|
||||
Ok(f) => f,
|
||||
Err(_) => return false,
|
||||
};
|
||||
let new_len = match file.metadata() {
|
||||
Ok(m) => m.len() as usize,
|
||||
Err(_) => return false,
|
||||
};
|
||||
if new_len == 0 {
|
||||
return false;
|
||||
}
|
||||
unsafe {
|
||||
libc::munmap(self.base, self.len);
|
||||
}
|
||||
let base = unsafe {
|
||||
libc::mmap(
|
||||
std::ptr::null_mut(),
|
||||
new_len,
|
||||
libc::PROT_READ,
|
||||
libc::MAP_SHARED,
|
||||
file.as_raw_fd(),
|
||||
0,
|
||||
)
|
||||
};
|
||||
if base == libc::MAP_FAILED {
|
||||
return false;
|
||||
}
|
||||
self.base = base;
|
||||
self.len = new_len;
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for ReadOnlyMapping {
|
||||
fn drop(&mut self) {
|
||||
unsafe {
|
||||
libc::munmap(self.base, self.len);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
unsafe impl Send for ReadOnlyMapping {}
|
||||
|
||||
pub async fn start_viz_server(
|
||||
config: VizConfig,
|
||||
rx: mpsc::Receiver<Vec<PageEvent>>,
|
||||
) -> std::io::Result<()> {
|
||||
let (event_tx, _) = broadcast::channel::<Vec<u8>>(256);
|
||||
let (residency_tx, _) = broadcast::channel::<Vec<u8>>(64);
|
||||
|
||||
let owner_map = Arc::new(RwLock::new(config.owner_map));
|
||||
let subscribers = Arc::new(AtomicUsize::new(0));
|
||||
|
||||
let state = AppState {
|
||||
info: VizInfo {
|
||||
page_count: config.page_count,
|
||||
page_size: config.page_size,
|
||||
dbi_names: config.dbi_names,
|
||||
},
|
||||
owner_map: owner_map.clone(),
|
||||
tree_info: Arc::new(config.tree_info),
|
||||
subscribers: subscribers.clone(),
|
||||
event_tx: event_tx.clone(),
|
||||
residency: Arc::new(RwLock::new(Vec::new())),
|
||||
residency_stats: Arc::new(RwLock::new(ResidencyStats::default())),
|
||||
residency_tx: residency_tx.clone(),
|
||||
residency_subscribers: Arc::new(AtomicUsize::new(0)),
|
||||
};
|
||||
|
||||
let bridge_tx = event_tx.clone();
|
||||
std::thread::Builder::new()
|
||||
.name("viz-bridge".into())
|
||||
.spawn(move || {
|
||||
while let Ok(events) = rx.recv() {
|
||||
if events.is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
{
|
||||
let mut map = owner_map.write().unwrap();
|
||||
for ev in &events {
|
||||
let pg = ev.pgno as usize;
|
||||
if ev.op == PageOp::Write && ev.dbi > 0 && ev.dbi < 0xFE && pg < map.len()
|
||||
{
|
||||
map[pg] = ev.dbi as u8;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if subscribers.load(Ordering::Relaxed) > 0 {
|
||||
let encoded = encode_events(&events);
|
||||
let _ = bridge_tx.send(encoded);
|
||||
}
|
||||
}
|
||||
})
|
||||
.expect("failed to spawn viz-bridge thread");
|
||||
|
||||
let res_map = state.residency.clone();
|
||||
let res_stats = state.residency_stats.clone();
|
||||
let res_tx = state.residency_tx.clone();
|
||||
let res_subs = state.residency_subscribers.clone();
|
||||
let own_map = state.owner_map.clone();
|
||||
let info_clone = state.info.clone();
|
||||
let db_path = config.db_path;
|
||||
let mdbx_ps = config.page_size;
|
||||
let sys_ps = unsafe { libc::sysconf(libc::_SC_PAGESIZE) } as u32;
|
||||
|
||||
std::thread::Builder::new()
|
||||
.name("residency-poller".into())
|
||||
.spawn(move || {
|
||||
let mut mapping: Option<ReadOnlyMapping> = None;
|
||||
let mut prev: Vec<u8> = Vec::new();
|
||||
|
||||
if db_path.as_os_str().is_empty() {
|
||||
tracing::warn!("residency-poller: no db_path configured, residency polling disabled");
|
||||
return;
|
||||
}
|
||||
|
||||
loop {
|
||||
std::thread::sleep(std::time::Duration::from_secs(3));
|
||||
|
||||
if res_subs.load(Ordering::Relaxed) == 0 {
|
||||
continue;
|
||||
}
|
||||
|
||||
if mapping.is_none() {
|
||||
match ReadOnlyMapping::new(&db_path) {
|
||||
Some(m) => {
|
||||
tracing::info!(
|
||||
"residency-poller: mapped {} ({} bytes, mdbx_ps={}, sys_ps={})",
|
||||
db_path.display(),
|
||||
m.len,
|
||||
mdbx_ps,
|
||||
sys_ps,
|
||||
);
|
||||
mapping = Some(m);
|
||||
}
|
||||
None => {
|
||||
tracing::warn!(
|
||||
"residency-poller: failed to mmap {}",
|
||||
db_path.display()
|
||||
);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let file_len = match std::fs::metadata(&db_path) {
|
||||
Ok(m) => m.len() as usize,
|
||||
Err(_) => continue,
|
||||
};
|
||||
let m = mapping.as_mut().unwrap();
|
||||
if file_len > m.len {
|
||||
tracing::info!(
|
||||
"residency-poller: file grew {} -> {}, remapping",
|
||||
m.len,
|
||||
file_len,
|
||||
);
|
||||
if !m.remap(&db_path) {
|
||||
tracing::warn!("residency-poller: remap failed");
|
||||
mapping = None;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
let m = mapping.as_ref().unwrap();
|
||||
let base = m.base;
|
||||
let len = m.len;
|
||||
|
||||
if mdbx_ps == 0 || sys_ps == 0 || len == 0 {
|
||||
continue;
|
||||
}
|
||||
|
||||
let sys_pages = (len + sys_ps as usize - 1) / sys_ps as usize;
|
||||
let mdbx_page_count = len / mdbx_ps as usize;
|
||||
|
||||
let mut mincore_vec = vec![0u8; sys_pages];
|
||||
let ret = unsafe { libc::mincore(base, len, mincore_vec.as_mut_ptr()) };
|
||||
if ret != 0 {
|
||||
continue;
|
||||
}
|
||||
|
||||
let mut cur = vec![0u8; mdbx_page_count];
|
||||
if mdbx_ps == sys_ps {
|
||||
for i in 0..mdbx_page_count.min(sys_pages) {
|
||||
cur[i] = mincore_vec[i] & 1;
|
||||
}
|
||||
} else if mdbx_ps > sys_ps {
|
||||
let ratio = mdbx_ps as usize / sys_ps as usize;
|
||||
for i in 0..mdbx_page_count {
|
||||
let base_idx = i * ratio;
|
||||
let mut all_resident = true;
|
||||
for j in 0..ratio {
|
||||
if base_idx + j >= sys_pages
|
||||
|| (mincore_vec[base_idx + j] & 1) == 0
|
||||
{
|
||||
all_resident = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
cur[i] = if all_resident { 1 } else { 0 };
|
||||
}
|
||||
} else {
|
||||
let ratio = sys_ps as usize / mdbx_ps as usize;
|
||||
for i in 0..mdbx_page_count {
|
||||
let sys_idx = i / ratio;
|
||||
cur[i] = if sys_idx < sys_pages {
|
||||
mincore_vec[sys_idx] & 1
|
||||
} else {
|
||||
0
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
let mut rm = res_map.write().unwrap();
|
||||
rm.clear();
|
||||
rm.extend_from_slice(&cur);
|
||||
}
|
||||
|
||||
{
|
||||
let omap = own_map.read().unwrap();
|
||||
let max_dbi = info_clone.dbi_names.len();
|
||||
let mut total_per = vec![0u64; max_dbi];
|
||||
let mut cached_per = vec![0u64; max_dbi];
|
||||
let mut total_cached = 0u64;
|
||||
let limit = mdbx_page_count.min(omap.len());
|
||||
|
||||
for i in 0..limit {
|
||||
let dbi = omap[i] as usize;
|
||||
if dbi < max_dbi {
|
||||
total_per[dbi] += 1;
|
||||
if cur[i] == 1 {
|
||||
cached_per[dbi] += 1;
|
||||
total_cached += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut stats = res_stats.write().unwrap();
|
||||
stats.total_pages = mdbx_page_count as u64;
|
||||
stats.cached_pages = total_cached;
|
||||
stats.pct = if mdbx_page_count > 0 {
|
||||
(total_cached as f64 / mdbx_page_count as f64) * 100.0
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
|
||||
stats.per_table = (0..max_dbi)
|
||||
.filter(|&d| total_per[d] > 0)
|
||||
.map(|d| TableResidency {
|
||||
dbi: d,
|
||||
name: info_clone.dbi_names.get(d).cloned().unwrap_or_default(),
|
||||
total: total_per[d],
|
||||
cached: cached_per[d],
|
||||
pct: if total_per[d] > 0 {
|
||||
(cached_per[d] as f64 / total_per[d] as f64) * 100.0
|
||||
} else {
|
||||
0.0
|
||||
},
|
||||
})
|
||||
.collect();
|
||||
}
|
||||
|
||||
if prev.len() != cur.len() {
|
||||
let mut buf = Vec::with_capacity(1 + cur.len());
|
||||
buf.push(0u8);
|
||||
buf.extend_from_slice(&cur);
|
||||
let _ = res_tx.send(buf);
|
||||
tracing::info!(
|
||||
"residency-poller: broadcast full snapshot ({} pages)",
|
||||
cur.len(),
|
||||
);
|
||||
} else {
|
||||
let mut spans: Vec<u8> = Vec::new();
|
||||
let mut span_count: u32 = 0;
|
||||
spans.extend_from_slice(&[1u8]);
|
||||
spans.extend_from_slice(&[0u8; 4]);
|
||||
|
||||
let mut i = 0;
|
||||
while i < cur.len() {
|
||||
if cur[i] != prev[i] {
|
||||
let state_val = cur[i];
|
||||
let start = i as u32;
|
||||
let mut end = i + 1;
|
||||
while end < cur.len()
|
||||
&& cur[end] != prev[end]
|
||||
&& cur[end] == state_val
|
||||
{
|
||||
end += 1;
|
||||
}
|
||||
let run_len = (end - i) as u32;
|
||||
spans.extend_from_slice(&start.to_le_bytes());
|
||||
spans.extend_from_slice(&run_len.to_le_bytes());
|
||||
spans.push(state_val);
|
||||
span_count += 1;
|
||||
i = end;
|
||||
} else {
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
|
||||
if span_count > 0 {
|
||||
spans[1..5].copy_from_slice(&span_count.to_le_bytes());
|
||||
let _ = res_tx.send(spans);
|
||||
}
|
||||
}
|
||||
|
||||
prev = cur;
|
||||
}
|
||||
})
|
||||
.expect("failed to spawn residency-poller thread");
|
||||
|
||||
let app = Router::new()
|
||||
.route("/", get(index_handler))
|
||||
.route("/ws", get(ws_handler))
|
||||
.route("/api/info", get(info_handler))
|
||||
.route("/api/owner_map", get(owner_map_handler))
|
||||
.route("/api/tree_info", get(tree_info_handler))
|
||||
.route("/api/residency", get(residency_handler))
|
||||
.route("/api/residency_stats", get(residency_stats_handler))
|
||||
.route("/ws_residency", get(ws_residency_handler))
|
||||
.with_state(state);
|
||||
|
||||
let addr = format!("0.0.0.0:{}", config.port);
|
||||
tracing::info!("mdbx-viz server listening on {addr}");
|
||||
|
||||
let listener = tokio::net::TcpListener::bind(&addr).await?;
|
||||
axum::serve(listener, app).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn index_handler() -> Html<&'static str> {
|
||||
Html(include_str!("index.html"))
|
||||
}
|
||||
|
||||
async fn info_handler(State(state): State<AppState>) -> impl IntoResponse {
|
||||
axum::Json(state.info)
|
||||
}
|
||||
|
||||
async fn owner_map_handler(State(state): State<AppState>) -> impl IntoResponse {
|
||||
let map = state.owner_map.read().unwrap();
|
||||
(
|
||||
StatusCode::OK,
|
||||
[(header::CONTENT_TYPE, "application/octet-stream")],
|
||||
map.clone(),
|
||||
)
|
||||
.into_response()
|
||||
}
|
||||
|
||||
async fn tree_info_handler(State(state): State<AppState>) -> impl IntoResponse {
|
||||
axum::Json(state.tree_info.as_ref().clone())
|
||||
}
|
||||
|
||||
async fn ws_handler(
|
||||
ws: WebSocketUpgrade,
|
||||
State(state): State<AppState>,
|
||||
) -> impl IntoResponse {
|
||||
ws.on_upgrade(move |socket| handle_ws(socket, state))
|
||||
}
|
||||
|
||||
async fn handle_ws(mut socket: WebSocket, state: AppState) {
|
||||
state.subscribers.fetch_add(1, Ordering::Relaxed);
|
||||
let mut rx = state.event_tx.subscribe();
|
||||
|
||||
loop {
|
||||
match rx.recv().await {
|
||||
Ok(data) => {
|
||||
if socket.send(Message::Binary(data.into())).await.is_err() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
Err(broadcast::error::RecvError::Lagged(n)) => {
|
||||
tracing::warn!("ws client lagged by {n} messages");
|
||||
let msg = format!("{{\"lagged\":{n}}}");
|
||||
if socket.send(Message::Text(msg)).await.is_err() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
Err(broadcast::error::RecvError::Closed) => break,
|
||||
}
|
||||
}
|
||||
|
||||
state.subscribers.fetch_sub(1, Ordering::Relaxed);
|
||||
}
|
||||
|
||||
async fn residency_handler(State(state): State<AppState>) -> impl IntoResponse {
|
||||
let map = state.residency.read().unwrap();
|
||||
(
|
||||
StatusCode::OK,
|
||||
[(header::CONTENT_TYPE, "application/octet-stream")],
|
||||
map.clone(),
|
||||
)
|
||||
.into_response()
|
||||
}
|
||||
|
||||
async fn residency_stats_handler(State(state): State<AppState>) -> impl IntoResponse {
|
||||
let stats = state.residency_stats.read().unwrap();
|
||||
axum::Json(stats.clone())
|
||||
}
|
||||
|
||||
async fn ws_residency_handler(
|
||||
ws: WebSocketUpgrade,
|
||||
State(state): State<AppState>,
|
||||
) -> impl IntoResponse {
|
||||
ws.on_upgrade(move |socket| handle_ws_residency(socket, state))
|
||||
}
|
||||
|
||||
async fn handle_ws_residency(mut socket: WebSocket, state: AppState) {
|
||||
state.residency_subscribers.fetch_add(1, Ordering::Relaxed);
|
||||
|
||||
{
|
||||
let msg = {
|
||||
let map = state.residency.read().unwrap();
|
||||
if !map.is_empty() {
|
||||
let mut buf = Vec::with_capacity(1 + map.len());
|
||||
buf.push(0u8);
|
||||
buf.extend_from_slice(&map);
|
||||
Some(buf)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
};
|
||||
if let Some(msg) = msg {
|
||||
let _ = socket.send(Message::Binary(msg.into())).await;
|
||||
}
|
||||
}
|
||||
|
||||
let mut rx = state.residency_tx.subscribe();
|
||||
loop {
|
||||
match rx.recv().await {
|
||||
Ok(data) => {
|
||||
if socket.send(Message::Binary(data.into())).await.is_err() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
Err(broadcast::error::RecvError::Lagged(_)) => continue,
|
||||
Err(broadcast::error::RecvError::Closed) => break,
|
||||
}
|
||||
}
|
||||
|
||||
state.residency_subscribers.fetch_sub(1, Ordering::Relaxed);
|
||||
}
|
||||
@@ -1,93 +0,0 @@
|
||||
use std::sync::mpsc;
|
||||
use std::time::Duration;
|
||||
|
||||
use clap::Parser;
|
||||
use reth_libmdbx::pageviz::{PageEvent, PageOp};
|
||||
|
||||
#[derive(Parser)]
|
||||
#[command(name = "reth-mdbx-viz", about = "Real-time MDBX page visualization")]
|
||||
struct Args {
|
||||
/// Path to the .bin owner map file (generated by mdbx-viz Python tool)
|
||||
#[arg(short, long)]
|
||||
bin: Option<String>,
|
||||
|
||||
/// Page size in bytes
|
||||
#[arg(long, default_value = "4096")]
|
||||
page_size: u32,
|
||||
|
||||
/// Port to serve on
|
||||
#[arg(short, long, default_value = "3141")]
|
||||
port: u16,
|
||||
|
||||
/// Generate fake events for testing
|
||||
#[arg(long)]
|
||||
demo: bool,
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
tracing_subscriber::fmt()
|
||||
.with_env_filter(
|
||||
tracing_subscriber::EnvFilter::try_from_default_env()
|
||||
.unwrap_or_else(|_| "info".into()),
|
||||
)
|
||||
.init();
|
||||
|
||||
let args = Args::parse();
|
||||
|
||||
let (owner_map, page_count) = if let Some(ref bin_path) = args.bin {
|
||||
let data = std::fs::read(bin_path).expect("failed to read .bin file");
|
||||
let pc = data.len() as u64;
|
||||
tracing::info!("loaded owner map: {pc} pages from {bin_path}");
|
||||
(data, pc)
|
||||
} else {
|
||||
tracing::info!("no --bin provided, starting with empty owner map");
|
||||
(vec![0xFFu8; 1_000_000], 1_000_000)
|
||||
};
|
||||
|
||||
let (tx, rx) = mpsc::channel();
|
||||
|
||||
if args.demo {
|
||||
let np = page_count as u32;
|
||||
std::thread::Builder::new()
|
||||
.name("demo-events".into())
|
||||
.spawn(move || {
|
||||
let mut pgno: u32 = 0;
|
||||
loop {
|
||||
let mut batch = Vec::with_capacity(256);
|
||||
for _ in 0..256 {
|
||||
let p = pgno % np;
|
||||
batch.push(PageEvent {
|
||||
pgno: p,
|
||||
dbi: (p % 8) + 2,
|
||||
op: match p % 10 {
|
||||
0..=6 => PageOp::Read,
|
||||
7..=8 => PageOp::Write,
|
||||
_ => PageOp::Free,
|
||||
},
|
||||
});
|
||||
pgno = pgno.wrapping_add(37);
|
||||
}
|
||||
if tx.send(batch).is_err() {
|
||||
break;
|
||||
}
|
||||
std::thread::sleep(Duration::from_millis(33));
|
||||
}
|
||||
})
|
||||
.expect("failed to spawn demo thread");
|
||||
}
|
||||
|
||||
let config = reth_mdbx_viz::VizConfig {
|
||||
port: args.port,
|
||||
page_count,
|
||||
page_size: args.page_size,
|
||||
dbi_names: vec![],
|
||||
tree_info: vec![],
|
||||
owner_map,
|
||||
db_path: std::path::PathBuf::new(),
|
||||
};
|
||||
|
||||
if let Err(e) = reth_mdbx_viz::start_viz_server(config, rx).await {
|
||||
tracing::error!("server error: {e}");
|
||||
}
|
||||
}
|
||||
@@ -1,757 +0,0 @@
|
||||
use rayon::prelude::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
io::{self, Read, Write},
|
||||
os::unix::io::AsRawFd,
|
||||
path::Path,
|
||||
sync::atomic::{AtomicU8, AtomicU64, Ordering},
|
||||
time::Instant,
|
||||
};
|
||||
|
||||
const PAGEHDRSZ: usize = 20;
|
||||
const NODESIZE: usize = 8;
|
||||
|
||||
const P_BRANCH: u16 = 0x01;
|
||||
const P_LEAF: u16 = 0x02;
|
||||
const P_LARGE: u16 = 0x04;
|
||||
const P_META: u16 = 0x08;
|
||||
const P_DUPFIX: u16 = 0x20;
|
||||
|
||||
const N_BIG: u8 = 0x01;
|
||||
const N_TREE: u8 = 0x02;
|
||||
const N_DUP: u8 = 0x04;
|
||||
|
||||
const P_INVALID: u32 = 0xFFFFFFFF;
|
||||
const UNREFERENCED: u8 = 0xFF;
|
||||
const DBI_META: u8 = 0xFE;
|
||||
|
||||
const META_GEO: usize = 0x14;
|
||||
const META_FREE_DB: usize = 0x28;
|
||||
const META_MAIN_DB: usize = 0x58;
|
||||
const META_TXNID_A: usize = 0x08;
|
||||
const META_TXNID_B: usize = 0xB0;
|
||||
const GEO_NOW: usize = 0x0C;
|
||||
|
||||
const TREE_ROOT: usize = 0x08;
|
||||
const TREE_BRANCH_PAGES: usize = 0x0C;
|
||||
const TREE_LEAF_PAGES: usize = 0x10;
|
||||
const TREE_LARGE_PAGES: usize = 0x14;
|
||||
const TREE_ITEMS: usize = 0x20;
|
||||
|
||||
const MDBX_MAGIC: u64 = 0x59659DBDEF4C11;
|
||||
|
||||
const CACHE_MAGIC: u64 = 0x5056495A_43414348; // "PVIZC ACH"
|
||||
const CACHE_VERSION: u32 = 1;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct TreeInfo {
|
||||
pub name: String,
|
||||
pub dbi_index: u8,
|
||||
pub height: u16,
|
||||
pub branch_pages: u32,
|
||||
pub leaf_pages: u32,
|
||||
pub large_pages: u32,
|
||||
pub items: u64,
|
||||
pub root_pgno: u32,
|
||||
}
|
||||
|
||||
pub struct WalkResult {
|
||||
pub owner_map: Vec<u8>,
|
||||
pub page_count: usize,
|
||||
pub page_size: usize,
|
||||
pub tree_info: Vec<TreeInfo>,
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
struct TreeDescriptor {
|
||||
flags: u16,
|
||||
height: u16,
|
||||
dupfix_size: u32,
|
||||
root: u32,
|
||||
branch_pages: u32,
|
||||
leaf_pages: u32,
|
||||
large_pages: u32,
|
||||
sequence: u64,
|
||||
items: u64,
|
||||
mod_txnid: u64,
|
||||
}
|
||||
|
||||
struct DBIInfo {
|
||||
name: String,
|
||||
dbi_index: u8,
|
||||
tree: TreeDescriptor,
|
||||
}
|
||||
|
||||
struct MmapFile {
|
||||
ptr: *mut u8,
|
||||
len: usize,
|
||||
}
|
||||
|
||||
impl MmapFile {
|
||||
fn open(path: &Path) -> io::Result<Self> {
|
||||
let file = std::fs::File::open(path)?;
|
||||
let len = file.metadata()?.len() as usize;
|
||||
if len == 0 {
|
||||
return Err(io::Error::new(io::ErrorKind::InvalidData, "empty file"));
|
||||
}
|
||||
let ptr = unsafe {
|
||||
libc::mmap(
|
||||
std::ptr::null_mut(),
|
||||
len,
|
||||
libc::PROT_READ,
|
||||
libc::MAP_SHARED,
|
||||
file.as_raw_fd(),
|
||||
0,
|
||||
)
|
||||
};
|
||||
if ptr == libc::MAP_FAILED {
|
||||
return Err(io::Error::last_os_error());
|
||||
}
|
||||
Ok(Self { ptr: ptr as *mut u8, len })
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn page(&self, pgno: usize, ps: usize) -> Option<&[u8]> {
|
||||
let off = pgno.checked_mul(ps)?;
|
||||
if off + ps > self.len {
|
||||
return None;
|
||||
}
|
||||
Some(unsafe { std::slice::from_raw_parts(self.ptr.add(off), ps) })
|
||||
}
|
||||
|
||||
fn evict(&self) {
|
||||
unsafe {
|
||||
libc::madvise(self.ptr as *mut libc::c_void, self.len, libc::MADV_DONTNEED);
|
||||
}
|
||||
tracing::info!(size_gb = self.len / (1024 * 1024 * 1024), "evicted walker mmap from page cache");
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for MmapFile {
|
||||
fn drop(&mut self) {
|
||||
unsafe { libc::munmap(self.ptr as *mut libc::c_void, self.len); }
|
||||
}
|
||||
}
|
||||
|
||||
unsafe impl Send for MmapFile {}
|
||||
unsafe impl Sync for MmapFile {}
|
||||
|
||||
#[inline]
|
||||
fn u16_le(buf: &[u8], off: usize) -> u16 {
|
||||
u16::from_le_bytes([buf[off], buf[off + 1]])
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn u32_le(buf: &[u8], off: usize) -> u32 {
|
||||
u32::from_le_bytes([buf[off], buf[off + 1], buf[off + 2], buf[off + 3]])
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn u64_le(buf: &[u8], off: usize) -> u64 {
|
||||
u64::from_le_bytes([
|
||||
buf[off], buf[off + 1], buf[off + 2], buf[off + 3],
|
||||
buf[off + 4], buf[off + 5], buf[off + 6], buf[off + 7],
|
||||
])
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn page_flags(buf: &[u8]) -> u16 {
|
||||
u16_le(buf, 0x0A)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn page_nkeys(buf: &[u8]) -> usize {
|
||||
u16_le(buf, 0x0C) as usize / 2
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn page_overflow_count(buf: &[u8]) -> u32 {
|
||||
u32_le(buf, 0x0C)
|
||||
}
|
||||
|
||||
fn parse_tree_descriptor(buf: &[u8], off: usize) -> TreeDescriptor {
|
||||
TreeDescriptor {
|
||||
flags: u16_le(buf, off),
|
||||
height: u16_le(buf, off + 0x02),
|
||||
dupfix_size: u32_le(buf, off + 0x04),
|
||||
root: u32_le(buf, off + TREE_ROOT),
|
||||
branch_pages: u32_le(buf, off + TREE_BRANCH_PAGES),
|
||||
leaf_pages: u32_le(buf, off + TREE_LEAF_PAGES),
|
||||
large_pages: u32_le(buf, off + TREE_LARGE_PAGES),
|
||||
sequence: u64_le(buf, off + 0x18),
|
||||
items: u64_le(buf, off + TREE_ITEMS),
|
||||
mod_txnid: u64_le(buf, off + 0x28),
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn claim(owner_map: &[AtomicU8], pgno: usize, dbi_index: u8, conflicts: &AtomicU64) -> bool {
|
||||
if pgno >= owner_map.len() {
|
||||
return false;
|
||||
}
|
||||
match owner_map[pgno].compare_exchange(UNREFERENCED, dbi_index, Ordering::Relaxed, Ordering::Relaxed) {
|
||||
Ok(_) => true,
|
||||
Err(existing) => {
|
||||
if existing != dbi_index {
|
||||
conflicts.fetch_add(1, Ordering::Relaxed);
|
||||
}
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn walk_tree_claim(
|
||||
mmap: &MmapFile,
|
||||
ps: usize,
|
||||
root_pgno: u32,
|
||||
page_count: usize,
|
||||
dbi_index: u8,
|
||||
owner_map: &[AtomicU8],
|
||||
conflicts: &AtomicU64,
|
||||
) {
|
||||
if root_pgno == P_INVALID || root_pgno as usize >= page_count {
|
||||
return;
|
||||
}
|
||||
|
||||
let mut stack: Vec<usize> = vec![root_pgno as usize];
|
||||
|
||||
while let Some(pgno) = stack.pop() {
|
||||
if pgno >= page_count {
|
||||
continue;
|
||||
}
|
||||
if !claim(owner_map, pgno, dbi_index, conflicts) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let buf = match mmap.page(pgno, ps) {
|
||||
Some(b) => b,
|
||||
None => continue,
|
||||
};
|
||||
|
||||
let flags = page_flags(buf);
|
||||
|
||||
if flags & P_BRANCH != 0 {
|
||||
let nkeys = page_nkeys(buf);
|
||||
for i in 0..nkeys {
|
||||
let node_rel = u16_le(buf, PAGEHDRSZ + i * 2) as usize;
|
||||
let child_pgno = u32_le(buf, PAGEHDRSZ + node_rel) as usize;
|
||||
stack.push(child_pgno);
|
||||
}
|
||||
} else if flags & P_LEAF != 0 && flags & P_DUPFIX == 0 {
|
||||
let nkeys = page_nkeys(buf);
|
||||
for i in 0..nkeys {
|
||||
let node_rel = u16_le(buf, PAGEHDRSZ + i * 2) as usize;
|
||||
let node_off = PAGEHDRSZ + node_rel;
|
||||
if node_off + NODESIZE > ps {
|
||||
continue;
|
||||
}
|
||||
let mn_flags = buf[node_off + 4];
|
||||
let ksize = u16_le(buf, node_off + 6) as usize;
|
||||
let data_off = node_off + NODESIZE + ksize;
|
||||
|
||||
if mn_flags & N_BIG != 0 {
|
||||
if data_off + 4 > ps {
|
||||
continue;
|
||||
}
|
||||
let ov_pgno = u32_le(buf, data_off) as usize;
|
||||
if ov_pgno >= page_count {
|
||||
continue;
|
||||
}
|
||||
let ov_buf = match mmap.page(ov_pgno, ps) {
|
||||
Some(b) => b,
|
||||
None => continue,
|
||||
};
|
||||
let ov_count = page_overflow_count(ov_buf) as usize;
|
||||
for op in ov_pgno..ov_pgno + ov_count {
|
||||
if op < page_count {
|
||||
claim(owner_map, op, dbi_index, conflicts);
|
||||
}
|
||||
}
|
||||
} else if (mn_flags & (N_DUP | N_TREE)) == (N_DUP | N_TREE) {
|
||||
if data_off + TREE_ROOT + 4 > ps {
|
||||
continue;
|
||||
}
|
||||
let sub_root = u32_le(buf, data_off + TREE_ROOT);
|
||||
if sub_root != P_INVALID {
|
||||
stack.push(sub_root as usize);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if flags & P_LARGE != 0 {
|
||||
let ov_count = page_overflow_count(buf) as usize;
|
||||
for op in (pgno + 1)..pgno + ov_count {
|
||||
if op < page_count {
|
||||
claim(owner_map, op, dbi_index, conflicts);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn discover_named_dbis(
|
||||
mmap: &MmapFile,
|
||||
ps: usize,
|
||||
main_root: u32,
|
||||
page_count: usize,
|
||||
owner_map: &[AtomicU8],
|
||||
conflicts: &AtomicU64,
|
||||
dbi_main: u8,
|
||||
dbi_start: u8,
|
||||
) -> Vec<DBIInfo> {
|
||||
let mut named = Vec::new();
|
||||
if main_root == P_INVALID || main_root as usize >= page_count {
|
||||
return named;
|
||||
}
|
||||
|
||||
let mut next_index = dbi_start;
|
||||
let mut stack: Vec<usize> = vec![main_root as usize];
|
||||
|
||||
while let Some(pgno) = stack.pop() {
|
||||
if pgno >= page_count {
|
||||
continue;
|
||||
}
|
||||
if !claim(owner_map, pgno, dbi_main, conflicts) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let buf = match mmap.page(pgno, ps) {
|
||||
Some(b) => b,
|
||||
None => continue,
|
||||
};
|
||||
|
||||
let flags = page_flags(buf);
|
||||
|
||||
if flags & P_BRANCH != 0 {
|
||||
let nkeys = page_nkeys(buf);
|
||||
for i in 0..nkeys {
|
||||
let node_rel = u16_le(buf, PAGEHDRSZ + i * 2) as usize;
|
||||
let child_pgno = u32_le(buf, PAGEHDRSZ + node_rel) as usize;
|
||||
stack.push(child_pgno);
|
||||
}
|
||||
} else if flags & P_LEAF != 0 && flags & P_DUPFIX == 0 {
|
||||
let nkeys = page_nkeys(buf);
|
||||
for i in 0..nkeys {
|
||||
let node_rel = u16_le(buf, PAGEHDRSZ + i * 2) as usize;
|
||||
let node_off = PAGEHDRSZ + node_rel;
|
||||
if node_off + NODESIZE > ps {
|
||||
continue;
|
||||
}
|
||||
let mn_flags = buf[node_off + 4];
|
||||
let ksize = u16_le(buf, node_off + 6) as usize;
|
||||
let data_off = node_off + NODESIZE + ksize;
|
||||
|
||||
if mn_flags & N_TREE != 0 {
|
||||
let key_off = node_off + NODESIZE;
|
||||
if key_off + ksize > ps || data_off + 0x30 > ps {
|
||||
continue;
|
||||
}
|
||||
let key_bytes = &buf[key_off..key_off + ksize];
|
||||
let name = match key_bytes.iter().position(|&b| b == 0) {
|
||||
Some(nul) => &key_bytes[..nul],
|
||||
None => key_bytes,
|
||||
};
|
||||
let name = String::from_utf8_lossy(name).into_owned();
|
||||
let tree = parse_tree_descriptor(buf, data_off);
|
||||
named.push(DBIInfo { name, dbi_index: next_index, tree });
|
||||
next_index = next_index.wrapping_add(1);
|
||||
} else if mn_flags & N_BIG != 0 {
|
||||
if data_off + 4 > ps {
|
||||
continue;
|
||||
}
|
||||
let ov_pgno = u32_le(buf, data_off) as usize;
|
||||
if ov_pgno >= page_count {
|
||||
continue;
|
||||
}
|
||||
let ov_buf = match mmap.page(ov_pgno, ps) {
|
||||
Some(b) => b,
|
||||
None => continue,
|
||||
};
|
||||
let ov_count = page_overflow_count(ov_buf) as usize;
|
||||
for op in ov_pgno..ov_pgno + ov_count {
|
||||
claim(owner_map, op, dbi_main, conflicts);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
named
|
||||
}
|
||||
|
||||
fn mark_free_pages(
|
||||
mmap: &MmapFile,
|
||||
ps: usize,
|
||||
free_root: u32,
|
||||
page_count: usize,
|
||||
owner_map: &[AtomicU8],
|
||||
conflicts: &AtomicU64,
|
||||
dbi_free: u8,
|
||||
) -> u64 {
|
||||
let mut marked: u64 = 0;
|
||||
if free_root == P_INVALID || free_root as usize >= page_count {
|
||||
return marked;
|
||||
}
|
||||
|
||||
let mut stack: Vec<usize> = vec![free_root as usize];
|
||||
|
||||
while let Some(pgno) = stack.pop() {
|
||||
if pgno >= page_count {
|
||||
continue;
|
||||
}
|
||||
if !claim(owner_map, pgno, dbi_free, conflicts) {
|
||||
continue;
|
||||
}
|
||||
marked += 1;
|
||||
|
||||
let buf = match mmap.page(pgno, ps) {
|
||||
Some(b) => b,
|
||||
None => continue,
|
||||
};
|
||||
|
||||
let flags = page_flags(buf);
|
||||
|
||||
if flags & P_BRANCH != 0 {
|
||||
let nkeys = page_nkeys(buf);
|
||||
for i in 0..nkeys {
|
||||
let node_rel = u16_le(buf, PAGEHDRSZ + i * 2) as usize;
|
||||
let child_pgno = u32_le(buf, PAGEHDRSZ + node_rel) as usize;
|
||||
stack.push(child_pgno);
|
||||
}
|
||||
} else if flags & P_LEAF != 0 && flags & P_DUPFIX == 0 {
|
||||
let nkeys = page_nkeys(buf);
|
||||
for i in 0..nkeys {
|
||||
let node_rel = u16_le(buf, PAGEHDRSZ + i * 2) as usize;
|
||||
let node_off = PAGEHDRSZ + node_rel;
|
||||
if node_off + NODESIZE > ps {
|
||||
continue;
|
||||
}
|
||||
let mn_flags = buf[node_off + 4];
|
||||
let ksize = u16_le(buf, node_off + 6) as usize;
|
||||
let dsize = u32_le(buf, node_off) as usize;
|
||||
let data_off = node_off + NODESIZE + ksize;
|
||||
|
||||
if mn_flags & N_BIG != 0 {
|
||||
if data_off + 4 > ps {
|
||||
continue;
|
||||
}
|
||||
let ov_pgno = u32_le(buf, data_off) as usize;
|
||||
if ov_pgno >= page_count {
|
||||
continue;
|
||||
}
|
||||
let ov_buf = match mmap.page(ov_pgno, ps) {
|
||||
Some(b) => b,
|
||||
None => continue,
|
||||
};
|
||||
let ov_count = page_overflow_count(ov_buf) as usize;
|
||||
for op in ov_pgno..ov_pgno + ov_count {
|
||||
if claim(owner_map, op, dbi_free, conflicts) {
|
||||
marked += 1;
|
||||
}
|
||||
}
|
||||
let ov_data_off = PAGEHDRSZ;
|
||||
if dsize >= 4 && ov_data_off + 4 <= ps {
|
||||
let pnl_count = u32_le(ov_buf, ov_data_off) as usize;
|
||||
let max_entries = (ps - ov_data_off - 4) / 4;
|
||||
let pnl_count = pnl_count.min(max_entries);
|
||||
for j in 0..pnl_count {
|
||||
let off = ov_data_off + 4 + j * 4;
|
||||
if off + 4 > ps { break; }
|
||||
let fp = u32_le(ov_buf, off) as usize;
|
||||
if fp < page_count {
|
||||
if claim(owner_map, fp, dbi_free, conflicts) {
|
||||
marked += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if dsize >= 4 && data_off + 4 <= ps {
|
||||
let pnl_count = u32_le(buf, data_off) as usize;
|
||||
let max_entries = (ps - data_off - 4) / 4;
|
||||
let pnl_count = pnl_count.min(max_entries);
|
||||
for j in 0..pnl_count {
|
||||
let off = data_off + 4 + j * 4;
|
||||
if off + 4 > ps { break; }
|
||||
let fp = u32_le(buf, off) as usize;
|
||||
if fp < page_count {
|
||||
if claim(owner_map, fp, dbi_free, conflicts) {
|
||||
marked += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
marked
|
||||
}
|
||||
|
||||
fn cache_path(mdbx_path: &Path) -> std::path::PathBuf {
|
||||
mdbx_path.with_file_name("mdbx_pageviz_cache.bin")
|
||||
}
|
||||
|
||||
fn try_load_cache(
|
||||
path: &Path,
|
||||
file_size: u64,
|
||||
txnid: u64,
|
||||
page_size: usize,
|
||||
page_count: usize,
|
||||
) -> Option<WalkResult> {
|
||||
let cp = cache_path(path);
|
||||
let mut f = std::fs::File::open(&cp).ok()?;
|
||||
let mut hdr = [0u8; 40];
|
||||
f.read_exact(&mut hdr).ok()?;
|
||||
|
||||
let magic = u64::from_le_bytes(hdr[0..8].try_into().unwrap());
|
||||
let ver = u32::from_le_bytes(hdr[8..12].try_into().unwrap());
|
||||
let c_txnid = u64::from_le_bytes(hdr[12..20].try_into().unwrap());
|
||||
let c_pc = u64::from_le_bytes(hdr[20..28].try_into().unwrap());
|
||||
let c_ps = u32::from_le_bytes(hdr[28..32].try_into().unwrap());
|
||||
let c_fsz = u64::from_le_bytes(hdr[32..40].try_into().unwrap());
|
||||
|
||||
if magic != CACHE_MAGIC || ver != CACHE_VERSION {
|
||||
return None;
|
||||
}
|
||||
if c_txnid != txnid || c_pc as usize != page_count || c_ps as usize != page_size || c_fsz != file_size {
|
||||
tracing::info!(
|
||||
cache_txnid = c_txnid, current_txnid = txnid,
|
||||
"cache stale, will re-walk"
|
||||
);
|
||||
return None;
|
||||
}
|
||||
|
||||
let mut ti_len_buf = [0u8; 4];
|
||||
f.read_exact(&mut ti_len_buf).ok()?;
|
||||
let ti_len = u32::from_le_bytes(ti_len_buf) as usize;
|
||||
|
||||
let mut ti_buf = vec![0u8; ti_len];
|
||||
f.read_exact(&mut ti_buf).ok()?;
|
||||
let tree_info: Vec<TreeInfo> = serde_json::from_slice(&ti_buf).ok()?;
|
||||
|
||||
let mut owner_map = vec![0u8; page_count];
|
||||
f.read_exact(&mut owner_map).ok()?;
|
||||
|
||||
tracing::info!(page_count, "loaded owner_map from cache");
|
||||
Some(WalkResult { owner_map, page_count, page_size, tree_info })
|
||||
}
|
||||
|
||||
fn save_cache(
|
||||
path: &Path,
|
||||
file_size: u64,
|
||||
txnid: u64,
|
||||
result: &WalkResult,
|
||||
) {
|
||||
let cp = cache_path(path);
|
||||
let tmp = cp.with_extension("tmp");
|
||||
|
||||
let write_inner = || -> io::Result<()> {
|
||||
let mut f = std::fs::File::create(&tmp)?;
|
||||
f.write_all(&CACHE_MAGIC.to_le_bytes())?;
|
||||
f.write_all(&CACHE_VERSION.to_le_bytes())?;
|
||||
f.write_all(&txnid.to_le_bytes())?;
|
||||
f.write_all(&(result.page_count as u64).to_le_bytes())?;
|
||||
f.write_all(&(result.page_size as u32).to_le_bytes())?;
|
||||
f.write_all(&file_size.to_le_bytes())?;
|
||||
|
||||
let ti_json = serde_json::to_vec(&result.tree_info).map_err(|e| io::Error::new(io::ErrorKind::Other, e))?;
|
||||
f.write_all(&(ti_json.len() as u32).to_le_bytes())?;
|
||||
f.write_all(&ti_json)?;
|
||||
f.write_all(&result.owner_map)?;
|
||||
f.flush()?;
|
||||
Ok(())
|
||||
};
|
||||
|
||||
match write_inner() {
|
||||
Ok(()) => {
|
||||
if let Err(e) = std::fs::rename(&tmp, &cp) {
|
||||
tracing::warn!("failed to rename cache file: {e}");
|
||||
let _ = std::fs::remove_file(&tmp);
|
||||
} else {
|
||||
tracing::info!(
|
||||
path = %cp.display(),
|
||||
size_mb = result.owner_map.len() / (1024 * 1024),
|
||||
"saved owner_map cache"
|
||||
);
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::warn!("failed to write cache: {e}");
|
||||
let _ = std::fs::remove_file(&tmp);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn walk_mdbx(path: &Path, name_to_dbi: &HashMap<&str, u8>) -> eyre::Result<WalkResult> {
|
||||
let t0 = Instant::now();
|
||||
|
||||
let mmap = MmapFile::open(path)?;
|
||||
let file_size = mmap.len as u64;
|
||||
|
||||
let meta0 = mmap.page(0, 4096).ok_or_else(|| eyre::eyre!("cannot read page 0"))?;
|
||||
let pflags = page_flags(meta0);
|
||||
eyre::ensure!(pflags & P_META != 0, "page 0 missing P_META flag");
|
||||
|
||||
let magic = u64_le(meta0, PAGEHDRSZ);
|
||||
eyre::ensure!((magic >> 8) == MDBX_MAGIC, "MDBX magic mismatch");
|
||||
|
||||
let candidates: &[usize] = &[4096, 8192, 16384, 32768, 65536, 1024, 2048];
|
||||
let geo_now_raw = u32_le(meta0, PAGEHDRSZ + META_GEO + GEO_NOW) as usize;
|
||||
|
||||
let mut ps = 4096usize;
|
||||
for &candidate in candidates {
|
||||
let mapped = geo_now_raw * candidate;
|
||||
if mapped >= mmap.len / 2 && mapped <= mmap.len * 4 {
|
||||
ps = candidate;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
tracing::info!(page_size = ps, file_size_gb = file_size / (1024 * 1024 * 1024), "detected page size");
|
||||
|
||||
let meta_pages: Vec<&[u8]> = (0..3)
|
||||
.map(|i| mmap.page(i, ps).ok_or_else(|| eyre::eyre!("cannot read meta page {i}")))
|
||||
.collect::<eyre::Result<Vec<_>>>()?;
|
||||
|
||||
let mut best_idx = 0usize;
|
||||
let mut best_txnid = 0u64;
|
||||
|
||||
for (i, meta) in meta_pages.iter().enumerate() {
|
||||
let m = u64_le(meta, PAGEHDRSZ);
|
||||
if (m >> 8) != MDBX_MAGIC {
|
||||
continue;
|
||||
}
|
||||
let txnid_a = u64_le(meta, PAGEHDRSZ + META_TXNID_A);
|
||||
let txnid_b = u64_le(meta, PAGEHDRSZ + META_TXNID_B);
|
||||
if txnid_a == txnid_b && txnid_a > best_txnid {
|
||||
best_txnid = txnid_a;
|
||||
best_idx = i;
|
||||
}
|
||||
}
|
||||
|
||||
eyre::ensure!(best_txnid > 0, "no valid meta page found");
|
||||
|
||||
let best = meta_pages[best_idx];
|
||||
let mb = PAGEHDRSZ;
|
||||
let geo_now = u32_le(best, mb + META_GEO + GEO_NOW) as usize;
|
||||
let page_count = geo_now;
|
||||
|
||||
tracing::info!(best_meta = best_idx, txnid = best_txnid, page_count, "selected best meta");
|
||||
|
||||
if let Some(cached) = try_load_cache(path, file_size, best_txnid, ps, page_count) {
|
||||
let elapsed = t0.elapsed();
|
||||
tracing::info!(elapsed_ms = elapsed.as_millis() as u64, "walk skipped (cache hit)");
|
||||
return Ok(cached);
|
||||
}
|
||||
|
||||
let free_tree = parse_tree_descriptor(best, mb + META_FREE_DB);
|
||||
let main_tree = parse_tree_descriptor(best, mb + META_MAIN_DB);
|
||||
|
||||
tracing::info!(
|
||||
page_count,
|
||||
free_root = free_tree.root,
|
||||
main_root = main_tree.root,
|
||||
"parsed meta, starting walk"
|
||||
);
|
||||
|
||||
let owner_map: Vec<AtomicU8> = (0..page_count).map(|_| AtomicU8::new(UNREFERENCED)).collect();
|
||||
let conflicts = AtomicU64::new(0);
|
||||
|
||||
for pgno in 0..3.min(page_count) {
|
||||
owner_map[pgno].store(DBI_META, Ordering::Relaxed);
|
||||
}
|
||||
|
||||
let dbi_free: u8 = 1;
|
||||
let dbi_main: u8 = 2;
|
||||
let dbi_start: u8 = 3;
|
||||
|
||||
tracing::info!("discovering named DBIs via MainDB walk");
|
||||
let mut named_dbis =
|
||||
discover_named_dbis(&mmap, ps, main_tree.root, page_count, &owner_map, &conflicts, dbi_main, dbi_start);
|
||||
tracing::info!(count = named_dbis.len(), "found named DBIs");
|
||||
|
||||
for dbi in &mut named_dbis {
|
||||
if let Some(&mapped_idx) = name_to_dbi.get(dbi.name.as_str()) {
|
||||
dbi.dbi_index = mapped_idx;
|
||||
}
|
||||
}
|
||||
|
||||
tracing::info!("walking FreeDB");
|
||||
let free_marked =
|
||||
mark_free_pages(&mmap, ps, free_tree.root, page_count, &owner_map, &conflicts, dbi_free);
|
||||
tracing::info!(marked = free_marked, "FreeDB walk complete");
|
||||
|
||||
let walk_tasks: Vec<(u32, u8)> = named_dbis
|
||||
.iter()
|
||||
.filter(|d| d.tree.root != P_INVALID)
|
||||
.map(|d| (d.tree.root, d.dbi_index))
|
||||
.collect();
|
||||
|
||||
tracing::info!(count = walk_tasks.len(), "walking named DBIs in parallel");
|
||||
|
||||
walk_tasks
|
||||
.par_iter()
|
||||
.for_each(|&(root, dbi_idx)| {
|
||||
walk_tree_claim(&mmap, ps, root, page_count, dbi_idx, &owner_map, &conflicts);
|
||||
});
|
||||
|
||||
let final_conflicts = conflicts.load(Ordering::Relaxed);
|
||||
let owner_map_vec: Vec<u8> = owner_map.iter().map(|a| a.load(Ordering::Relaxed)).collect();
|
||||
let assigned = owner_map_vec.iter().filter(|&&b| b != UNREFERENCED).count();
|
||||
|
||||
let elapsed = t0.elapsed();
|
||||
tracing::info!(
|
||||
elapsed_ms = elapsed.as_millis() as u64,
|
||||
assigned,
|
||||
unreferenced = page_count - assigned,
|
||||
conflicts = final_conflicts,
|
||||
"walk complete"
|
||||
);
|
||||
|
||||
let mut tree_info = Vec::new();
|
||||
|
||||
tree_info.push(TreeInfo {
|
||||
name: "FreeDB".to_string(),
|
||||
dbi_index: dbi_free,
|
||||
height: free_tree.height,
|
||||
branch_pages: free_tree.branch_pages,
|
||||
leaf_pages: free_tree.leaf_pages,
|
||||
large_pages: free_tree.large_pages,
|
||||
items: free_tree.items,
|
||||
root_pgno: free_tree.root,
|
||||
});
|
||||
|
||||
tree_info.push(TreeInfo {
|
||||
name: "MainDB".to_string(),
|
||||
dbi_index: dbi_main,
|
||||
height: main_tree.height,
|
||||
branch_pages: main_tree.branch_pages,
|
||||
leaf_pages: main_tree.leaf_pages,
|
||||
large_pages: main_tree.large_pages,
|
||||
items: main_tree.items,
|
||||
root_pgno: main_tree.root,
|
||||
});
|
||||
|
||||
for dbi in &named_dbis {
|
||||
tree_info.push(TreeInfo {
|
||||
name: dbi.name.clone(),
|
||||
dbi_index: dbi.dbi_index,
|
||||
height: dbi.tree.height,
|
||||
branch_pages: dbi.tree.branch_pages,
|
||||
leaf_pages: dbi.tree.leaf_pages,
|
||||
large_pages: dbi.tree.large_pages,
|
||||
items: dbi.tree.items,
|
||||
root_pgno: dbi.tree.root,
|
||||
});
|
||||
}
|
||||
|
||||
let result = WalkResult { owner_map: owner_map_vec, page_count, page_size: ps, tree_info };
|
||||
|
||||
save_cache(path, file_size, best_txnid, &result);
|
||||
|
||||
mmap.evict();
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
@@ -282,7 +282,7 @@ where
|
||||
level = "debug",
|
||||
target = "providers::state::overlay",
|
||||
skip_all,
|
||||
fields(db_tip_block)
|
||||
fields(%db_tip_block)
|
||||
)]
|
||||
fn calculate_overlay(
|
||||
&self,
|
||||
|
||||
@@ -39,7 +39,7 @@ impl<T: BlockExecutionWriter> BlockExecutionWriter for &T {
|
||||
}
|
||||
|
||||
/// Block Writer
|
||||
#[auto_impl::auto_impl(&, Box)]
|
||||
#[auto_impl::auto_impl(&, Arc, Box)]
|
||||
pub trait BlockWriter {
|
||||
/// The body this writer can write.
|
||||
type Block: Block;
|
||||
|
||||
@@ -9,7 +9,7 @@ use reth_primitives_traits::{Account, StorageEntry};
|
||||
use reth_storage_errors::provider::ProviderResult;
|
||||
|
||||
/// Hashing Writer
|
||||
#[auto_impl(&, Box)]
|
||||
#[auto_impl(&, Arc, Box)]
|
||||
pub trait HashingWriter: Send {
|
||||
/// Unwind and clear account hashing.
|
||||
///
|
||||
|
||||
@@ -7,7 +7,7 @@ use reth_db_models::AccountBeforeTx;
|
||||
use reth_storage_errors::provider::ProviderResult;
|
||||
|
||||
/// History Writer
|
||||
#[auto_impl(&, Box)]
|
||||
#[auto_impl(&, Arc, Box)]
|
||||
pub trait HistoryWriter: Send {
|
||||
/// Unwind and clear account history indices.
|
||||
///
|
||||
|
||||
@@ -11,7 +11,7 @@ pub mod keys {
|
||||
}
|
||||
|
||||
/// Client trait for reading node metadata from the database.
|
||||
#[auto_impl::auto_impl(&)]
|
||||
#[auto_impl::auto_impl(&, Arc)]
|
||||
pub trait MetadataProvider: Send {
|
||||
/// Get a metadata value by key
|
||||
fn get_metadata(&self, key: &str) -> ProviderResult<Option<Vec<u8>>>;
|
||||
|
||||
@@ -3,7 +3,7 @@ use reth_prune_types::{PruneCheckpoint, PruneSegment};
|
||||
use reth_storage_errors::provider::ProviderResult;
|
||||
|
||||
/// The trait for fetching prune checkpoint related data.
|
||||
#[auto_impl::auto_impl(&)]
|
||||
#[auto_impl::auto_impl(&, Arc)]
|
||||
pub trait PruneCheckpointReader: Send {
|
||||
/// Fetch the prune checkpoint for the given segment.
|
||||
fn get_prune_checkpoint(
|
||||
@@ -16,7 +16,7 @@ pub trait PruneCheckpointReader: Send {
|
||||
}
|
||||
|
||||
/// The trait for updating prune checkpoint related data.
|
||||
#[auto_impl::auto_impl(&)]
|
||||
#[auto_impl::auto_impl(&, Arc)]
|
||||
pub trait PruneCheckpointWriter {
|
||||
/// Save prune checkpoint.
|
||||
fn save_prune_checkpoint(
|
||||
|
||||
@@ -4,7 +4,7 @@ use reth_stages_types::{StageCheckpoint, StageId};
|
||||
use reth_storage_errors::provider::ProviderResult;
|
||||
|
||||
/// The trait for fetching stage checkpoint related data.
|
||||
#[auto_impl::auto_impl(&)]
|
||||
#[auto_impl::auto_impl(&, Arc)]
|
||||
pub trait StageCheckpointReader: Send {
|
||||
/// Fetch the checkpoint for the given stage.
|
||||
fn get_stage_checkpoint(&self, id: StageId) -> ProviderResult<Option<StageCheckpoint>>;
|
||||
@@ -18,7 +18,7 @@ pub trait StageCheckpointReader: Send {
|
||||
}
|
||||
|
||||
/// The trait for updating stage checkpoint related data.
|
||||
#[auto_impl::auto_impl(&)]
|
||||
#[auto_impl::auto_impl(&, Arc)]
|
||||
pub trait StageCheckpointWriter {
|
||||
/// Save stage checkpoint.
|
||||
fn save_stage_checkpoint(&self, id: StageId, checkpoint: StageCheckpoint)
|
||||
|
||||
@@ -14,7 +14,7 @@ use reth_trie_common::HashedPostState;
|
||||
use revm_database::BundleState;
|
||||
|
||||
/// This just receives state, or [`ExecutionOutcome`], from the provider
|
||||
#[auto_impl::auto_impl(&, Box)]
|
||||
#[auto_impl::auto_impl(&, Arc, Box)]
|
||||
pub trait StateReader: Send {
|
||||
/// Receipt type in [`ExecutionOutcome`].
|
||||
type Receipt: Send + Sync;
|
||||
@@ -30,7 +30,7 @@ pub trait StateReader: Send {
|
||||
pub type StateProviderBox = Box<dyn StateProvider + Send + 'static>;
|
||||
|
||||
/// An abstraction for a type that provides state data.
|
||||
#[auto_impl(&, Box)]
|
||||
#[auto_impl(&, Arc, Box)]
|
||||
pub trait StateProvider:
|
||||
BlockHashReader
|
||||
+ AccountReader
|
||||
@@ -110,14 +110,14 @@ pub trait AccountInfoReader: AccountReader + BytecodeReader {}
|
||||
impl<T: AccountReader + BytecodeReader> AccountInfoReader for T {}
|
||||
|
||||
/// Trait that provides the hashed state from various sources.
|
||||
#[auto_impl(&, Box)]
|
||||
#[auto_impl(&, Arc, Box)]
|
||||
pub trait HashedPostStateProvider {
|
||||
/// Returns the `HashedPostState` of the provided [`BundleState`].
|
||||
fn hashed_post_state(&self, bundle_state: &BundleState) -> HashedPostState;
|
||||
}
|
||||
|
||||
/// Trait for reading bytecode associated with a given code hash.
|
||||
#[auto_impl(&, Box)]
|
||||
#[auto_impl(&, Arc, Box)]
|
||||
pub trait BytecodeReader {
|
||||
/// Get account code by its hash
|
||||
fn bytecode_by_hash(&self, code_hash: &B256) -> ProviderResult<Option<Bytecode>>;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use reth_db_api::table::Table;
|
||||
|
||||
/// The trait for fetching provider statistics.
|
||||
#[auto_impl::auto_impl(&)]
|
||||
#[auto_impl::auto_impl(&, Arc)]
|
||||
pub trait StatsReader {
|
||||
/// Fetch the number of entries in the corresponding [Table]. Depending on the provider, it may
|
||||
/// route to different data sources other than [Table].
|
||||
|
||||
@@ -3,7 +3,7 @@ use alloc::{
|
||||
vec::Vec,
|
||||
};
|
||||
use alloy_primitives::{Address, BlockNumber, B256, U256};
|
||||
use core::ops::{RangeBounds, RangeInclusive};
|
||||
use core::ops::RangeInclusive;
|
||||
use reth_primitives_traits::{StorageEntry, StorageSlotKey};
|
||||
use reth_storage_errors::provider::ProviderResult;
|
||||
|
||||
@@ -35,7 +35,7 @@ impl From<ChangesetEntry> for StorageEntry {
|
||||
}
|
||||
|
||||
/// Storage reader
|
||||
#[auto_impl::auto_impl(&, Box)]
|
||||
#[auto_impl::auto_impl(&, Arc, Box)]
|
||||
pub trait StorageReader: Send {
|
||||
/// Get plainstate storages for addresses and storage keys.
|
||||
fn plain_state_storages(
|
||||
@@ -61,7 +61,7 @@ pub trait StorageReader: Send {
|
||||
|
||||
/// Storage `ChangeSet` reader
|
||||
#[cfg(feature = "db-api")]
|
||||
#[auto_impl::auto_impl(&, Box)]
|
||||
#[auto_impl::auto_impl(&, Arc, Box)]
|
||||
pub trait StorageChangeSetReader: Send {
|
||||
/// Iterate over storage changesets and return the storage state from before this block.
|
||||
///
|
||||
@@ -91,7 +91,7 @@ pub trait StorageChangeSetReader: Send {
|
||||
/// [`StorageSlotKey::Hashed`] based on the current storage mode.
|
||||
fn storage_changesets_range(
|
||||
&self,
|
||||
range: impl RangeBounds<BlockNumber>,
|
||||
range: impl core::ops::RangeBounds<BlockNumber>,
|
||||
) -> ProviderResult<Vec<(reth_db_api::models::BlockNumberAddress, ChangesetEntry)>>;
|
||||
|
||||
/// Get the total count of all storage changes.
|
||||
|
||||
@@ -40,7 +40,7 @@ pub trait StateRootProvider {
|
||||
}
|
||||
|
||||
/// A type that can compute the storage root for a given account.
|
||||
#[auto_impl::auto_impl(&, Box)]
|
||||
#[auto_impl::auto_impl(&, Box, Arc)]
|
||||
pub trait StorageRootProvider {
|
||||
/// Returns the storage root of the `HashedStorage` for target address on top of the current
|
||||
/// state.
|
||||
@@ -66,7 +66,7 @@ pub trait StorageRootProvider {
|
||||
}
|
||||
|
||||
/// A type that can generate state proof on top of a given post state.
|
||||
#[auto_impl::auto_impl(&, Box)]
|
||||
#[auto_impl::auto_impl(&, Box, Arc)]
|
||||
pub trait StateProofProvider {
|
||||
/// Get account and storage proofs of target keys in the `HashedPostState`
|
||||
/// on top of the current state.
|
||||
@@ -90,7 +90,7 @@ pub trait StateProofProvider {
|
||||
}
|
||||
|
||||
/// Trie Writer
|
||||
#[auto_impl::auto_impl(&, Box)]
|
||||
#[auto_impl::auto_impl(&, Arc, Box)]
|
||||
pub trait TrieWriter: Send {
|
||||
/// Writes trie updates to the database.
|
||||
///
|
||||
@@ -106,7 +106,7 @@ pub trait TrieWriter: Send {
|
||||
}
|
||||
|
||||
/// Storage Trie Writer
|
||||
#[auto_impl::auto_impl(&, Box)]
|
||||
#[auto_impl::auto_impl(&, Arc, Box)]
|
||||
pub trait StorageTrieWriter: Send {
|
||||
/// Writes storage trie updates from the given storage trie map with already sorted updates.
|
||||
///
|
||||
|
||||
@@ -29,11 +29,12 @@ dyn-clone.workspace = true
|
||||
|
||||
# feature `rayon`
|
||||
rayon = { workspace = true, optional = true }
|
||||
crossbeam-utils = { workspace = true, optional = true }
|
||||
pin-project = { workspace = true, optional = true }
|
||||
|
||||
[dev-dependencies]
|
||||
tokio = { workspace = true, features = ["sync", "rt", "rt-multi-thread", "time", "macros"] }
|
||||
|
||||
[features]
|
||||
rayon = ["dep:rayon", "pin-project"]
|
||||
rayon = ["dep:rayon", "dep:crossbeam-utils", "pin-project"]
|
||||
test-utils = []
|
||||
|
||||
298
crates/tasks/src/for_each_ordered.rs
Normal file
298
crates/tasks/src/for_each_ordered.rs
Normal file
@@ -0,0 +1,298 @@
|
||||
use crossbeam_utils::CachePadded;
|
||||
use rayon::iter::{IndexedParallelIterator, ParallelIterator};
|
||||
use std::{
|
||||
cell::UnsafeCell,
|
||||
mem::MaybeUninit,
|
||||
sync::atomic::{AtomicBool, Ordering},
|
||||
};
|
||||
|
||||
/// Extension trait for [`IndexedParallelIterator`]
|
||||
/// that streams results to a sequential consumer in index order.
|
||||
pub trait ForEachOrdered: IndexedParallelIterator {
|
||||
/// Executes the parallel iterator, calling `f` on each result **sequentially in index
|
||||
/// order**.
|
||||
///
|
||||
/// Items are computed in parallel, but `f` is invoked as `f(item_0)`, `f(item_1)`, …,
|
||||
/// `f(item_{n-1})` on the calling thread. The calling thread receives each item as soon
|
||||
/// as it (and all preceding items) are ready.
|
||||
///
|
||||
/// `f` does **not** need to be [`Send`] — it runs exclusively on the calling thread.
|
||||
fn for_each_ordered<F>(self, f: F)
|
||||
where
|
||||
Self::Item: Send,
|
||||
F: FnMut(Self::Item);
|
||||
}
|
||||
|
||||
impl<I: IndexedParallelIterator> ForEachOrdered for I {
|
||||
fn for_each_ordered<F>(self, f: F)
|
||||
where
|
||||
Self::Item: Send,
|
||||
F: FnMut(Self::Item),
|
||||
{
|
||||
ordered_impl(self, f);
|
||||
}
|
||||
}
|
||||
|
||||
/// A cache-line-padded slot with an atomic ready flag.
|
||||
struct Slot<T> {
|
||||
value: UnsafeCell<MaybeUninit<T>>,
|
||||
ready: AtomicBool,
|
||||
}
|
||||
|
||||
// SAFETY: Each slot is written by exactly one producer and read by exactly one consumer.
|
||||
// The AtomicBool synchronizes access (Release on write, Acquire on read).
|
||||
unsafe impl<T: Send> Send for Slot<T> {}
|
||||
unsafe impl<T: Send> Sync for Slot<T> {}
|
||||
|
||||
struct Shared<T> {
|
||||
slots: Box<[CachePadded<Slot<T>>]>,
|
||||
panicked: AtomicBool,
|
||||
}
|
||||
|
||||
impl<T> Shared<T> {
|
||||
fn new(n: usize) -> Self {
|
||||
Self {
|
||||
// SAFETY: Zero is a valid bit pattern for Slot.
|
||||
// Needs to be zero for `ready` to be false.
|
||||
slots: unsafe {
|
||||
Box::<[_]>::assume_init(Box::<[CachePadded<Slot<T>>]>::new_zeroed_slice(n))
|
||||
},
|
||||
panicked: AtomicBool::new(false),
|
||||
}
|
||||
}
|
||||
|
||||
/// # Safety
|
||||
/// Index `i` must be in bounds and must only be written once.
|
||||
#[inline]
|
||||
unsafe fn write(&self, i: usize, val: T) {
|
||||
let slot = unsafe { self.slots.get_unchecked(i) };
|
||||
unsafe { (*slot.value.get()).write(val) };
|
||||
slot.ready.store(true, Ordering::Release);
|
||||
}
|
||||
|
||||
/// # Safety
|
||||
/// Index `i` must be in bounds. Must only be called after `ready` is observed `true`.
|
||||
#[inline]
|
||||
unsafe fn take(&self, i: usize) -> T {
|
||||
let slot = unsafe { self.slots.get_unchecked(i) };
|
||||
let v = unsafe { (*slot.value.get()).assume_init_read() };
|
||||
// Clear ready so Drop doesn't double-free this slot.
|
||||
slot.ready.store(false, Ordering::Relaxed);
|
||||
v
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn is_ready(&self, i: usize) -> bool {
|
||||
// SAFETY: caller ensures `i < n`.
|
||||
unsafe { self.slots.get_unchecked(i) }.ready.load(Ordering::Acquire)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Drop for Shared<T> {
|
||||
fn drop(&mut self) {
|
||||
for slot in &mut *self.slots {
|
||||
if *slot.ready.get_mut() {
|
||||
unsafe { (*slot.value.get()).assume_init_drop() };
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Executes a parallel iterator and delivers results to a sequential callback in index order.
|
||||
///
|
||||
/// This works by pre-allocating one cache-line-padded slot per item. Each slot holds an
|
||||
/// `UnsafeCell<MaybeUninit<T>>` and an `AtomicBool` ready flag. A rayon task computes all
|
||||
/// items in parallel, writing each result into its slot and setting the flag (`Release`).
|
||||
/// The calling thread walks slots 0, 1, 2, … in order, spinning on the flag (`Acquire`),
|
||||
/// then reading the value and passing it to `f`.
|
||||
fn ordered_impl<I, F>(iter: I, mut f: F)
|
||||
where
|
||||
I: IndexedParallelIterator,
|
||||
I::Item: Send,
|
||||
F: FnMut(I::Item),
|
||||
{
|
||||
use std::panic::{catch_unwind, AssertUnwindSafe};
|
||||
|
||||
let n = iter.len();
|
||||
if n == 0 {
|
||||
return;
|
||||
}
|
||||
|
||||
let shared = Shared::<I::Item>::new(n);
|
||||
|
||||
rayon::in_place_scope(|s| {
|
||||
// Producer: compute items in parallel and write them into their slots.
|
||||
s.spawn(|_| {
|
||||
let res = catch_unwind(AssertUnwindSafe(|| {
|
||||
iter.enumerate().for_each(|(i, item)| {
|
||||
// SAFETY: `enumerate()` on an IndexedParallelIterator yields each
|
||||
// index exactly once.
|
||||
unsafe { shared.write(i, item) };
|
||||
});
|
||||
}));
|
||||
if let Err(payload) = res {
|
||||
shared.panicked.store(true, Ordering::Release);
|
||||
std::panic::resume_unwind(payload);
|
||||
}
|
||||
});
|
||||
|
||||
// Consumer: sequential, ordered, on the calling thread.
|
||||
// Exponential backoff: 1, 2, 4, …, 64 pause instructions, then OS yields.
|
||||
const SPIN_SHIFT_LIMIT: u32 = 6;
|
||||
for i in 0..n {
|
||||
let mut backoff = 0u32;
|
||||
'wait: loop {
|
||||
if shared.is_ready(i) {
|
||||
break 'wait;
|
||||
}
|
||||
|
||||
if shared.panicked.load(Ordering::Relaxed) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Yield to rayon's work-stealing so the producer can make progress,
|
||||
// especially important when the thread pool is small.
|
||||
if rayon::yield_now() == Some(rayon::Yield::Executed) {
|
||||
continue 'wait;
|
||||
}
|
||||
|
||||
if backoff < SPIN_SHIFT_LIMIT {
|
||||
for _ in 0..(1u32 << backoff) {
|
||||
std::hint::spin_loop();
|
||||
}
|
||||
backoff += 1;
|
||||
} else {
|
||||
// Producer is genuinely slow; fall back to OS-level yield.
|
||||
std::thread::yield_now();
|
||||
}
|
||||
}
|
||||
// SAFETY: `i < n` and we just observed the ready flag with Acquire ordering.
|
||||
let value = unsafe { shared.take(i) };
|
||||
f(value);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use rayon::prelude::*;
|
||||
use std::sync::{
|
||||
atomic::{AtomicUsize, Ordering},
|
||||
Barrier,
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn preserves_order() {
|
||||
let input: Vec<u64> = (0..1000).collect();
|
||||
let mut output = Vec::with_capacity(input.len());
|
||||
input.par_iter().map(|x| x * 2).for_each_ordered(|x| output.push(x));
|
||||
let expected: Vec<u64> = (0..1000).map(|x| x * 2).collect();
|
||||
assert_eq!(output, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn empty_iterator() {
|
||||
let input: Vec<u64> = vec![];
|
||||
let mut output = Vec::new();
|
||||
input.par_iter().map(|x| *x).for_each_ordered(|x| output.push(x));
|
||||
assert!(output.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn single_element() {
|
||||
let mut output = Vec::new();
|
||||
vec![42u64].par_iter().map(|x| *x).for_each_ordered(|x| output.push(x));
|
||||
assert_eq!(output, vec![42]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn slow_early_items_still_delivered_in_order() {
|
||||
// Item 0 is deliberately delayed; all other items complete quickly.
|
||||
// The consumer must still deliver items in order 0, 1, 2, … regardless.
|
||||
let barrier = Barrier::new(2);
|
||||
let n = 64usize;
|
||||
let input: Vec<usize> = (0..n).collect();
|
||||
let mut output = Vec::with_capacity(n);
|
||||
|
||||
input
|
||||
.par_iter()
|
||||
.map(|&i| {
|
||||
if i == 0 {
|
||||
// Wait until at least one other item has been produced.
|
||||
barrier.wait();
|
||||
} else if i == n - 1 {
|
||||
// Signal that other items are ready.
|
||||
barrier.wait();
|
||||
}
|
||||
i
|
||||
})
|
||||
.for_each_ordered(|x| output.push(x));
|
||||
|
||||
assert_eq!(output, input);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn drops_unconsumed_slots_on_panic() {
|
||||
static DROP_COUNT: AtomicUsize = AtomicUsize::new(0);
|
||||
|
||||
#[derive(Clone)]
|
||||
struct Tracked(#[allow(dead_code)] u64);
|
||||
impl Drop for Tracked {
|
||||
fn drop(&mut self) {
|
||||
DROP_COUNT.fetch_add(1, Ordering::Relaxed);
|
||||
}
|
||||
}
|
||||
|
||||
DROP_COUNT.store(0, Ordering::Relaxed);
|
||||
|
||||
let input: Vec<u64> = (0..100).collect();
|
||||
let result = std::panic::catch_unwind(|| {
|
||||
input
|
||||
.par_iter()
|
||||
.map(|&i| {
|
||||
assert!(i != 50, "intentional");
|
||||
Tracked(i)
|
||||
})
|
||||
.for_each_ordered(|_item| {});
|
||||
});
|
||||
|
||||
assert!(result.is_err());
|
||||
// All produced Tracked values must have been dropped (either consumed or cleaned up).
|
||||
// We can't assert an exact count since the panic may cut production short.
|
||||
let drops = DROP_COUNT.load(Ordering::Relaxed);
|
||||
assert!(drops > 0, "some items should have been dropped");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn no_double_drop() {
|
||||
// Verify that consumed items are dropped exactly once (not double-freed by Drop).
|
||||
static DROP_COUNT: AtomicUsize = AtomicUsize::new(0);
|
||||
|
||||
struct Counted(#[allow(dead_code)] u64);
|
||||
impl Drop for Counted {
|
||||
fn drop(&mut self) {
|
||||
DROP_COUNT.fetch_add(1, Ordering::Relaxed);
|
||||
}
|
||||
}
|
||||
|
||||
DROP_COUNT.store(0, Ordering::Relaxed);
|
||||
let n = 200u64;
|
||||
let input: Vec<u64> = (0..n).collect();
|
||||
input.par_iter().map(|&i| Counted(i)).for_each_ordered(|_item| {});
|
||||
|
||||
assert_eq!(DROP_COUNT.load(Ordering::Relaxed), n as usize);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn callback_is_not_send() {
|
||||
// Verify that the callback does not need to be Send.
|
||||
use std::rc::Rc;
|
||||
let counter = Rc::new(std::cell::Cell::new(0u64));
|
||||
let input: Vec<u64> = (0..100).collect();
|
||||
input.par_iter().map(|&x| x).for_each_ordered(|x| {
|
||||
counter.set(counter.get() + x);
|
||||
});
|
||||
assert_eq!(counter.get(), (0..100u64).sum::<u64>());
|
||||
}
|
||||
}
|
||||
@@ -40,6 +40,12 @@ pub mod shutdown;
|
||||
#[cfg(feature = "rayon")]
|
||||
pub mod pool;
|
||||
|
||||
/// Lock-free ordered parallel iterator extension trait.
|
||||
#[cfg(feature = "rayon")]
|
||||
pub mod for_each_ordered;
|
||||
#[cfg(feature = "rayon")]
|
||||
pub use for_each_ordered::ForEachOrdered;
|
||||
|
||||
#[cfg(feature = "rayon")]
|
||||
pub use runtime::RayonConfig;
|
||||
pub use runtime::{Runtime, RuntimeBuildError, RuntimeBuilder, RuntimeConfig, TokioConfig};
|
||||
|
||||
@@ -501,11 +501,26 @@ where
|
||||
results.pop().expect("result length is the same as the input")
|
||||
}
|
||||
|
||||
async fn add_transactions(
|
||||
&self,
|
||||
origin: TransactionOrigin,
|
||||
transactions: Vec<Self::Transaction>,
|
||||
) -> Vec<PoolResult<AddedTransactionOutcome>> {
|
||||
if transactions.is_empty() {
|
||||
return Vec::new()
|
||||
}
|
||||
let validated = self
|
||||
.pool
|
||||
.validator()
|
||||
.validate_transactions(transactions.into_iter().map(|tx| (origin, tx)))
|
||||
.await;
|
||||
self.pool.add_transactions(origin, validated)
|
||||
}
|
||||
|
||||
async fn add_transactions_with_origins(
|
||||
&self,
|
||||
transactions: impl IntoIterator<Item = (TransactionOrigin, Self::Transaction)> + Send,
|
||||
transactions: Vec<(TransactionOrigin, Self::Transaction)>,
|
||||
) -> Vec<PoolResult<AddedTransactionOutcome>> {
|
||||
let transactions: Vec<_> = transactions.into_iter().collect();
|
||||
if transactions.is_empty() {
|
||||
return Vec::new()
|
||||
}
|
||||
|
||||
@@ -84,9 +84,23 @@ impl<T: EthPoolTransaction> TransactionPool for NoopTransactionPool<T> {
|
||||
Err(PoolError::other(hash, Box::new(NoopInsertError::new(transaction))))
|
||||
}
|
||||
|
||||
async fn add_transactions(
|
||||
&self,
|
||||
_origin: TransactionOrigin,
|
||||
transactions: Vec<Self::Transaction>,
|
||||
) -> Vec<PoolResult<AddedTransactionOutcome>> {
|
||||
transactions
|
||||
.into_iter()
|
||||
.map(|transaction| {
|
||||
let hash = *transaction.hash();
|
||||
Err(PoolError::other(hash, Box::new(NoopInsertError::new(transaction))))
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
async fn add_transactions_with_origins(
|
||||
&self,
|
||||
transactions: impl IntoIterator<Item = (TransactionOrigin, Self::Transaction)> + Send,
|
||||
transactions: Vec<(TransactionOrigin, Self::Transaction)>,
|
||||
) -> Vec<PoolResult<AddedTransactionOutcome>> {
|
||||
transactions
|
||||
.into_iter()
|
||||
|
||||
@@ -704,6 +704,10 @@ impl PoolTransaction for MockTransaction {
|
||||
|
||||
type Pooled = PooledTransactionVariant;
|
||||
|
||||
fn consensus_ref(&self) -> Recovered<&Self::Consensus> {
|
||||
unimplemented!("mock transaction does not wrap a consensus transaction")
|
||||
}
|
||||
|
||||
fn into_consensus(self) -> Recovered<Self::Consensus> {
|
||||
self.into()
|
||||
}
|
||||
|
||||
@@ -175,9 +175,7 @@ pub trait TransactionPool: Clone + Debug + Send + Sync {
|
||||
&self,
|
||||
origin: TransactionOrigin,
|
||||
transactions: Vec<Self::Transaction>,
|
||||
) -> impl Future<Output = Vec<PoolResult<AddedTransactionOutcome>>> + Send {
|
||||
self.add_transactions_with_origins(transactions.into_iter().map(move |tx| (origin, tx)))
|
||||
}
|
||||
) -> impl Future<Output = Vec<PoolResult<AddedTransactionOutcome>>> + Send;
|
||||
|
||||
/// Adds the given _unvalidated_ transactions into the pool.
|
||||
///
|
||||
@@ -188,7 +186,7 @@ pub trait TransactionPool: Clone + Debug + Send + Sync {
|
||||
/// Consumer: RPC
|
||||
fn add_transactions_with_origins(
|
||||
&self,
|
||||
transactions: impl IntoIterator<Item = (TransactionOrigin, Self::Transaction)> + Send,
|
||||
transactions: Vec<(TransactionOrigin, Self::Transaction)>,
|
||||
) -> impl Future<Output = Vec<PoolResult<AddedTransactionOutcome>>> + Send;
|
||||
|
||||
/// Submit a consensus transaction directly to the pool
|
||||
@@ -1259,6 +1257,9 @@ pub trait PoolTransaction:
|
||||
self.clone().into_consensus()
|
||||
}
|
||||
|
||||
/// Returns a reference to the consensus transaction with the recovered sender.
|
||||
fn consensus_ref(&self) -> Recovered<&Self::Consensus>;
|
||||
|
||||
/// Define a method to convert from the `Self` type to `Consensus`
|
||||
fn into_consensus(self) -> Recovered<Self::Consensus>;
|
||||
|
||||
@@ -1449,6 +1450,10 @@ impl PoolTransaction for EthPooledTransaction {
|
||||
self.transaction().clone()
|
||||
}
|
||||
|
||||
fn consensus_ref(&self) -> Recovered<&Self::Consensus> {
|
||||
Recovered::new_unchecked(&*self.transaction, self.transaction.signer())
|
||||
}
|
||||
|
||||
fn into_consensus(self) -> Recovered<Self::Consensus> {
|
||||
self.transaction
|
||||
}
|
||||
|
||||
@@ -325,7 +325,7 @@ mod tests {
|
||||
reth_provider::providers::OverlayStateProviderFactory::new(factory, changeset_cache);
|
||||
let task_ctx = ProofTaskCtx::new(factory);
|
||||
let runtime = reth_tasks::Runtime::test();
|
||||
let proof_worker_handle = ProofWorkerHandle::new(&runtime, task_ctx, false);
|
||||
let proof_worker_handle = ProofWorkerHandle::new(&runtime, task_ctx, false, false);
|
||||
|
||||
let parallel_result = ParallelProof::new(Default::default(), proof_worker_handle.clone())
|
||||
.decoded_multiproof(targets.clone())
|
||||
|
||||
@@ -75,7 +75,7 @@ use std::{
|
||||
},
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
use tracing::{debug, debug_span, error, trace};
|
||||
use tracing::{debug, debug_span, error, instrument, trace};
|
||||
|
||||
#[cfg(feature = "metrics")]
|
||||
use crate::proof_task_metrics::{
|
||||
@@ -134,10 +134,18 @@ impl ProofWorkerHandle {
|
||||
/// # Parameters
|
||||
/// - `runtime`: The centralized runtime used to spawn blocking worker tasks
|
||||
/// - `task_ctx`: Shared context with database view and prefix sets
|
||||
/// - `halve_workers`: Whether to halve the worker pool size (for small blocks)
|
||||
/// - `v2_proofs_enabled`: Whether to enable V2 storage proofs
|
||||
#[instrument(
|
||||
name = "ProofWorkerHandle::new",
|
||||
level = "debug",
|
||||
target = "trie::proof_task",
|
||||
skip_all
|
||||
)]
|
||||
pub fn new<Factory>(
|
||||
runtime: &Runtime,
|
||||
task_ctx: ProofTaskCtx<Factory>,
|
||||
halve_workers: bool,
|
||||
v2_proofs_enabled: bool,
|
||||
) -> Self
|
||||
where
|
||||
@@ -154,13 +162,17 @@ impl ProofWorkerHandle {
|
||||
|
||||
let cached_storage_roots = Arc::<DashMap<_, _>>::default();
|
||||
|
||||
let storage_worker_count = runtime.proof_storage_worker_pool().current_num_threads();
|
||||
let account_worker_count = runtime.proof_account_worker_pool().current_num_threads();
|
||||
let divisor = if halve_workers { 2 } else { 1 };
|
||||
let storage_worker_count =
|
||||
runtime.proof_storage_worker_pool().current_num_threads() / divisor;
|
||||
let account_worker_count =
|
||||
runtime.proof_account_worker_pool().current_num_threads() / divisor;
|
||||
|
||||
debug!(
|
||||
target: "trie::proof_task",
|
||||
storage_worker_count,
|
||||
account_worker_count,
|
||||
halve_workers,
|
||||
?v2_proofs_enabled,
|
||||
"Spawning proof worker pools"
|
||||
);
|
||||
@@ -2012,7 +2024,7 @@ mod tests {
|
||||
let ctx = test_ctx(factory);
|
||||
|
||||
let runtime = reth_tasks::Runtime::test();
|
||||
let proof_handle = ProofWorkerHandle::new(&runtime, ctx, false);
|
||||
let proof_handle = ProofWorkerHandle::new(&runtime, ctx, false, false);
|
||||
|
||||
// Verify handle can be cloned
|
||||
let _cloned_handle = proof_handle.clone();
|
||||
|
||||
@@ -1159,7 +1159,7 @@ where
|
||||
name = "SparseStateTrie::prune",
|
||||
target = "trie::sparse",
|
||||
skip_all,
|
||||
fields(max_depth, max_storage_tries)
|
||||
fields(%max_depth, %max_storage_tries)
|
||||
)]
|
||||
pub fn prune(&mut self, max_depth: usize, max_storage_tries: usize) {
|
||||
// Prune state and storage tries in parallel
|
||||
|
||||
@@ -83,6 +83,7 @@ async fn main() -> eyre::Result<()> {
|
||||
IncomingEthRequest::GetReceipts { .. } => {}
|
||||
IncomingEthRequest::GetReceipts69 { .. } => {}
|
||||
IncomingEthRequest::GetReceipts70 { .. } => {}
|
||||
IncomingEthRequest::GetBlockAccessLists { .. } => {}
|
||||
}
|
||||
}
|
||||
transaction_message = transactions_rx.recv() => {
|
||||
|
||||
Reference in New Issue
Block a user