mirror of
https://github.com/hl-archive-node/nanoreth.git
synced 2025-12-06 10:59:55 +00:00
feat(bin): Format db list & db status subcommands (#667)
Co-authored-by: Georgios Konstantopoulos <me@gakonst.com>
This commit is contained in:
@ -53,3 +53,6 @@ tokio-stream = "0.1"
|
||||
futures = "0.3.25"
|
||||
tempfile = { version = "3.3.0" }
|
||||
backon = "0.2.0"
|
||||
comfy-table = "6.1.4"
|
||||
crossterm = "0.25.0"
|
||||
tui = "0.19.0"
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
//! Database debugging tool
|
||||
use crate::dirs::{DbPath, PlatformPath};
|
||||
use clap::{Parser, Subcommand};
|
||||
use comfy_table::{Cell, Row, Table as ComfyTable};
|
||||
use eyre::{Result, WrapErr};
|
||||
use reth_db::{
|
||||
cursor::{DbCursorRO, Walker},
|
||||
@ -11,7 +12,11 @@ use reth_db::{
|
||||
};
|
||||
use reth_interfaces::test_utils::generators::random_block_range;
|
||||
use reth_provider::insert_canonical_block;
|
||||
use tracing::info;
|
||||
use std::collections::BTreeMap;
|
||||
use tracing::{error, info};
|
||||
|
||||
/// DB List TUI
|
||||
mod tui;
|
||||
|
||||
/// `reth db` command
|
||||
#[derive(Debug, Parser)]
|
||||
@ -78,6 +83,17 @@ impl Command {
|
||||
match &self.command {
|
||||
// TODO: We'll need to add this on the DB trait.
|
||||
Subcommands::Stats { .. } => {
|
||||
let mut stats_table = ComfyTable::new();
|
||||
stats_table.load_preset(comfy_table::presets::ASCII_MARKDOWN);
|
||||
stats_table.set_header([
|
||||
"Table Name",
|
||||
"# Entries",
|
||||
"Branch Pages",
|
||||
"Leaf Pages",
|
||||
"Overflow Pages",
|
||||
"Total Size (KB)",
|
||||
]);
|
||||
|
||||
tool.db.view(|tx| {
|
||||
for table in tables::TABLES.iter().map(|(_, name)| name) {
|
||||
let table_db =
|
||||
@ -97,22 +113,69 @@ impl Command {
|
||||
let overflow_pages = stats.overflow_pages();
|
||||
let num_pages = leaf_pages + branch_pages + overflow_pages;
|
||||
let table_size = page_size * num_pages;
|
||||
info!(
|
||||
target: "reth::cli",
|
||||
"Table {} has {} entries (total size: {} KB)",
|
||||
table,
|
||||
stats.entries(),
|
||||
table_size / 1024
|
||||
);
|
||||
|
||||
let mut row = Row::new();
|
||||
row.add_cell(Cell::new(table))
|
||||
.add_cell(Cell::new(stats.entries()))
|
||||
.add_cell(Cell::new(branch_pages))
|
||||
.add_cell(Cell::new(leaf_pages))
|
||||
.add_cell(Cell::new(overflow_pages))
|
||||
.add_cell(Cell::new(table_size / 1024));
|
||||
stats_table.add_row(row);
|
||||
}
|
||||
Ok::<(), eyre::Report>(())
|
||||
})??;
|
||||
|
||||
println!("{stats_table}");
|
||||
}
|
||||
Subcommands::Seed { len } => {
|
||||
tool.seed(*len)?;
|
||||
}
|
||||
Subcommands::List(args) => {
|
||||
tool.list(args)?;
|
||||
macro_rules! table_tui {
|
||||
($arg:expr, $start:expr, $len:expr => [$($table:ident),*]) => {
|
||||
match $arg {
|
||||
$(stringify!($table) => {
|
||||
tool.db.view(|tx| {
|
||||
let table_db = tx.inner.open_db(Some(stringify!($table))).wrap_err("Could not open db.")?;
|
||||
let stats = tx.inner.db_stat(&table_db).wrap_err(format!("Could not find table: {}", stringify!($table)))?;
|
||||
let total_entries = stats.entries();
|
||||
if $start > total_entries - 1 {
|
||||
error!(
|
||||
target: "reth::cli",
|
||||
"Start index {start} is greater than the final entry index ({final_entry_idx}) in the table {table}",
|
||||
start = $start,
|
||||
final_entry_idx = total_entries - 1,
|
||||
table = stringify!($table)
|
||||
);
|
||||
return Ok(());
|
||||
}
|
||||
let map = tool.list::<tables::$table>($start, $len)?;
|
||||
tui::DbListTUI::<tables::$table>::show_tui(map, $start, total_entries)
|
||||
})??
|
||||
},)*
|
||||
_ => {
|
||||
error!(target: "reth::cli", "Unknown table.");
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
table_tui!(args.table.as_str(), args.start, args.len => [
|
||||
CanonicalHeaders,
|
||||
HeaderTD,
|
||||
HeaderNumbers,
|
||||
Headers,
|
||||
BlockBodies,
|
||||
BlockOmmers,
|
||||
TxHashNumber,
|
||||
PlainAccountState,
|
||||
BlockTransitionIndex,
|
||||
TxTransitionIndex,
|
||||
SyncStage,
|
||||
Transactions
|
||||
]);
|
||||
}
|
||||
Subcommands::Drop => {
|
||||
tool.drop(&self.db)?;
|
||||
@ -150,41 +213,9 @@ impl<'a, DB: Database> DbTool<'a, DB> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Lists the given table data
|
||||
fn list(&mut self, args: &ListArgs) -> Result<()> {
|
||||
macro_rules! list_tables {
|
||||
($arg:expr, $start:expr, $len:expr => [$($table:ident,)*]) => {
|
||||
match $arg {
|
||||
$(stringify!($table) => {
|
||||
self.list_table::<tables::$table>($start, $len)?
|
||||
},)*
|
||||
_ => {
|
||||
tracing::error!(target: "reth::cli", "Unknown table.");
|
||||
return Ok(())
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
list_tables!(args.table.as_str(), args.start, args.len => [
|
||||
CanonicalHeaders,
|
||||
HeaderTD,
|
||||
HeaderNumbers,
|
||||
Headers,
|
||||
BlockBodies,
|
||||
BlockOmmers,
|
||||
TxHashNumber,
|
||||
PlainAccountState,
|
||||
BlockTransitionIndex,
|
||||
TxTransitionIndex,
|
||||
SyncStage,
|
||||
Transactions,
|
||||
]);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn list_table<T: Table>(&mut self, start: usize, len: usize) -> Result<()> {
|
||||
/// Grabs the contents of the table within a certain index range and places the
|
||||
/// entries into a [HashMap].
|
||||
fn list<T: Table>(&mut self, start: usize, len: usize) -> Result<BTreeMap<T::Key, T::Value>> {
|
||||
let data = self.db.view(|tx| {
|
||||
let mut cursor = tx.cursor_read::<T>().expect("Was not able to obtain a cursor.");
|
||||
|
||||
@ -195,8 +226,9 @@ impl<'a, DB: Database> DbTool<'a, DB> {
|
||||
walker.skip(start).take(len).collect::<Vec<_>>()
|
||||
})?;
|
||||
|
||||
println!("{data:?}");
|
||||
Ok(())
|
||||
data.into_iter()
|
||||
.collect::<Result<BTreeMap<T::Key, T::Value>, _>>()
|
||||
.map_err(|e| eyre::eyre!(e))
|
||||
}
|
||||
|
||||
fn drop(&mut self, path: &PlatformPath<DbPath>) -> Result<()> {
|
||||
|
||||
209
bin/reth/src/db/tui.rs
Normal file
209
bin/reth/src/db/tui.rs
Normal file
@ -0,0 +1,209 @@
|
||||
use crossterm::{
|
||||
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, MouseEventKind},
|
||||
execute,
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
};
|
||||
use reth_db::table::Table;
|
||||
use std::{
|
||||
collections::BTreeMap,
|
||||
io,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
use tracing::error;
|
||||
use tui::{
|
||||
backend::{Backend, CrosstermBackend},
|
||||
layout::{Alignment, Constraint, Corner, Direction, Layout},
|
||||
style::{Color, Modifier, Style},
|
||||
widgets::{Block, Borders, List, ListItem, ListState, Paragraph, Wrap},
|
||||
Frame, Terminal,
|
||||
};
|
||||
|
||||
/// Available keybindings for the [DbListTUI]
|
||||
static CMDS: [(&str, &str); 3] = [("q", "Quit"), ("up", "Entry Above"), ("down", "Entry Below")];
|
||||
|
||||
/// Modified version of the [ListState] struct that exposes the `offset` field.
|
||||
/// Used to make the [DbListTUI] keys clickable.
|
||||
struct ExpListState {
|
||||
pub(crate) offset: usize,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub(crate) struct DbListTUI<T: Table> {
|
||||
/// The state of the key list.
|
||||
pub(crate) state: ListState,
|
||||
/// The starting index of the key list in the DB.
|
||||
pub(crate) start: usize,
|
||||
/// The total number of entries in the database
|
||||
pub(crate) total_entries: usize,
|
||||
/// Entries to show in the TUI.
|
||||
pub(crate) entries: BTreeMap<T::Key, T::Value>,
|
||||
}
|
||||
|
||||
impl<T: Table> DbListTUI<T> {
|
||||
fn new(entries: BTreeMap<T::Key, T::Value>, start: usize, total_entries: usize) -> Self {
|
||||
Self { state: ListState::default(), start, total_entries, entries }
|
||||
}
|
||||
|
||||
/// Move to the next list selection
|
||||
fn next(&mut self) {
|
||||
let i = match self.state.selected() {
|
||||
Some(i) => {
|
||||
if i >= self.entries.len() - 1 {
|
||||
0
|
||||
} else {
|
||||
i + 1
|
||||
}
|
||||
}
|
||||
None => 0,
|
||||
};
|
||||
self.state.select(Some(i));
|
||||
}
|
||||
|
||||
/// Move to the previous list selection
|
||||
fn previous(&mut self) {
|
||||
let i = match self.state.selected() {
|
||||
Some(i) => {
|
||||
if i == 0 {
|
||||
self.entries.len() - 1
|
||||
} else {
|
||||
i - 1
|
||||
}
|
||||
}
|
||||
None => 0,
|
||||
};
|
||||
self.state.select(Some(i));
|
||||
}
|
||||
|
||||
/// Show the [DbListTUI] in the terminal.
|
||||
pub(crate) fn show_tui(
|
||||
entries: BTreeMap<T::Key, T::Value>,
|
||||
start: usize,
|
||||
total_entries: usize,
|
||||
) -> eyre::Result<()> {
|
||||
// setup terminal
|
||||
enable_raw_mode()?;
|
||||
let mut stdout = io::stdout();
|
||||
execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
|
||||
let backend = CrosstermBackend::new(stdout);
|
||||
let mut terminal = Terminal::new(backend)?;
|
||||
|
||||
// create app and run it
|
||||
let tick_rate = Duration::from_millis(250);
|
||||
let mut app = DbListTUI::<T>::new(entries, start, total_entries);
|
||||
app.state.select(Some(0));
|
||||
let res = run(&mut terminal, app, tick_rate);
|
||||
|
||||
// restore terminal
|
||||
disable_raw_mode()?;
|
||||
execute!(terminal.backend_mut(), LeaveAlternateScreen, DisableMouseCapture)?;
|
||||
terminal.show_cursor()?;
|
||||
|
||||
if let Err(err) = res {
|
||||
error!("{:?}", err)
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn run<B: Backend, T: Table>(
|
||||
terminal: &mut Terminal<B>,
|
||||
mut app: DbListTUI<T>,
|
||||
tick_rate: Duration,
|
||||
) -> io::Result<()> {
|
||||
let mut last_tick = Instant::now();
|
||||
loop {
|
||||
terminal.draw(|f| ui(f, &mut app))?;
|
||||
|
||||
let timeout =
|
||||
tick_rate.checked_sub(last_tick.elapsed()).unwrap_or_else(|| Duration::from_secs(0));
|
||||
if crossterm::event::poll(timeout)? {
|
||||
match event::read()? {
|
||||
Event::Key(key) => match key.code {
|
||||
KeyCode::Char('q') | KeyCode::Char('Q') => return Ok(()),
|
||||
KeyCode::Down => app.next(),
|
||||
KeyCode::Up => app.previous(),
|
||||
_ => {}
|
||||
},
|
||||
Event::Mouse(e) => match e.kind {
|
||||
MouseEventKind::ScrollDown => app.next(),
|
||||
MouseEventKind::ScrollUp => app.previous(),
|
||||
// TODO: This click event can be triggered outside of the list widget.
|
||||
MouseEventKind::Down(_) => {
|
||||
// SAFETY: The pointer to the app's state will always be valid for
|
||||
// reads here, and the source is larger than the destination.
|
||||
//
|
||||
// This is technically unsafe, but because the alignment requirements
|
||||
// in both the source and destination are the same and we can ensure
|
||||
// that the pointer to `app.state` is valid for reads, this is safe.
|
||||
let state: ExpListState = unsafe { std::mem::transmute_copy(&app.state) };
|
||||
let new_idx = (e.row as usize + state.offset).saturating_sub(1);
|
||||
if new_idx < app.entries.len() {
|
||||
app.state.select(Some(new_idx));
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
if last_tick.elapsed() >= tick_rate {
|
||||
last_tick = Instant::now();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn ui<B: Backend, T: Table>(f: &mut Frame<'_, B>, app: &mut DbListTUI<T>) {
|
||||
let outer_chunks = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.constraints([Constraint::Percentage(95), Constraint::Percentage(5)].as_ref())
|
||||
.split(f.size());
|
||||
|
||||
// Columns
|
||||
{
|
||||
let inner_chunks = Layout::default()
|
||||
.direction(Direction::Horizontal)
|
||||
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)])
|
||||
.split(outer_chunks[0]);
|
||||
|
||||
let formatted_keys = app
|
||||
.entries
|
||||
.keys()
|
||||
.enumerate()
|
||||
.map(|(i, k)| ListItem::new(format!("[{}] - {k:?}", i + app.start)))
|
||||
.collect::<Vec<ListItem<'_>>>();
|
||||
|
||||
let key_list = List::new(formatted_keys)
|
||||
.block(Block::default().borders(Borders::ALL).title(format!(
|
||||
"Keys (Showing range [{}, {}] out of {} entries)",
|
||||
app.start,
|
||||
app.start + app.entries.len() - 1,
|
||||
app.total_entries
|
||||
)))
|
||||
.style(Style::default().fg(Color::White))
|
||||
.highlight_style(Style::default().fg(Color::Cyan).add_modifier(Modifier::ITALIC))
|
||||
.highlight_symbol("➜ ")
|
||||
.start_corner(Corner::TopLeft);
|
||||
f.render_stateful_widget(key_list, inner_chunks[0], &mut app.state);
|
||||
|
||||
let value_display = Paragraph::new(
|
||||
serde_json::to_string_pretty(
|
||||
&app.entries.values().collect::<Vec<_>>()[app.state.selected().unwrap_or(0)],
|
||||
)
|
||||
.unwrap_or(String::from("Error serializing value!")),
|
||||
)
|
||||
.block(Block::default().borders(Borders::ALL).title("Value (JSON)"))
|
||||
.wrap(Wrap { trim: false })
|
||||
.alignment(Alignment::Left);
|
||||
f.render_widget(value_display, inner_chunks[1]);
|
||||
}
|
||||
|
||||
// Footer
|
||||
let footer = Paragraph::new(
|
||||
CMDS.iter().map(|(k, v)| format!("[{k}] {v}")).collect::<Vec<_>>().join(" | "),
|
||||
)
|
||||
.block(Block::default().borders(Borders::ALL))
|
||||
.alignment(Alignment::Center)
|
||||
.style(Style::default().fg(Color::Cyan).add_modifier(Modifier::BOLD));
|
||||
f.render_widget(footer, outer_chunks[1]);
|
||||
}
|
||||
Reference in New Issue
Block a user