1use crate::{common::CliNodeTypes, db::checksum::ChecksumViewer};
2use clap::Parser;
3use comfy_table::{Cell, Row, Table as ComfyTable};
4use eyre::WrapErr;
5use human_bytes::human_bytes;
6use itertools::Itertools;
7use reth_chainspec::EthereumHardforks;
8use reth_db::{mdbx, static_file::iter_static_files, DatabaseEnv};
9use reth_db_api::{database::Database, TableViewer, Tables};
10use reth_db_common::DbTool;
11use reth_fs_util as fs;
12use reth_node_builder::{NodePrimitives, NodeTypesWithDB, NodeTypesWithDBAdapter};
13use reth_node_core::dirs::{ChainPath, DataDirPath};
14use reth_provider::{
15 providers::{ProviderNodeTypes, StaticFileProvider},
16 RocksDBProviderFactory,
17};
18use reth_static_file_types::SegmentRangeInclusive;
19use std::time::Duration;
20
21#[derive(Parser, Debug)]
22pub struct Command {
24 #[arg(long, default_value_t = false)]
26 pub(crate) skip_consistency_checks: bool,
27
28 #[arg(long, default_value_t = false)]
30 detailed_sizes: bool,
31
32 #[arg(long, default_value_t = false)]
34 detailed_segments: bool,
35
36 #[arg(long, default_value_t = false)]
43 checksum: bool,
44}
45
46impl Command {
47 pub fn execute<N: CliNodeTypes<ChainSpec: EthereumHardforks>>(
49 self,
50 data_dir: ChainPath<DataDirPath>,
51 tool: &DbTool<NodeTypesWithDBAdapter<N, DatabaseEnv>>,
52 ) -> eyre::Result<()> {
53 if self.checksum {
54 let checksum_report = self.checksum_report(tool)?;
55 println!("{checksum_report}");
56 println!("\n");
57 }
58
59 let static_files_stats_table = self.static_files_stats_table::<N::Primitives>(data_dir)?;
60 println!("{static_files_stats_table}");
61
62 println!("\n");
63
64 let db_stats_table = self.db_stats_table(tool)?;
65 println!("{db_stats_table}");
66
67 println!("\n");
68
69 let rocksdb_stats_table = self.rocksdb_stats_table(tool);
70 println!("{rocksdb_stats_table}");
71
72 Ok(())
73 }
74
75 fn db_stats_table<N: NodeTypesWithDB<DB = DatabaseEnv>>(
76 &self,
77 tool: &DbTool<N>,
78 ) -> eyre::Result<ComfyTable> {
79 let mut table = ComfyTable::new();
80 table.load_preset(comfy_table::presets::ASCII_MARKDOWN);
81 table.set_header([
82 "Table Name",
83 "# Entries",
84 "Branch Pages",
85 "Leaf Pages",
86 "Overflow Pages",
87 "Total Size",
88 ]);
89
90 tool.provider_factory.db_ref().view(|tx| {
91 let mut db_tables = Tables::ALL.iter().map(|table| table.name()).collect::<Vec<_>>();
92 db_tables.sort();
93 let mut total_size = 0;
94 for db_table in db_tables {
95 let table_db = tx.inner().open_db(Some(db_table)).wrap_err("Could not open db.")?;
96
97 let stats = tx
98 .inner()
99 .db_stat(table_db.dbi())
100 .wrap_err(format!("Could not find table: {db_table}"))?;
101
102 let page_size = stats.page_size() as usize;
106 let leaf_pages = stats.leaf_pages();
107 let branch_pages = stats.branch_pages();
108 let overflow_pages = stats.overflow_pages();
109 let num_pages = leaf_pages + branch_pages + overflow_pages;
110 let table_size = page_size * num_pages;
111
112 total_size += table_size;
113 let mut row = Row::new();
114 row.add_cell(Cell::new(db_table))
115 .add_cell(Cell::new(stats.entries()))
116 .add_cell(Cell::new(branch_pages))
117 .add_cell(Cell::new(leaf_pages))
118 .add_cell(Cell::new(overflow_pages))
119 .add_cell(Cell::new(human_bytes(table_size as f64)));
120 table.add_row(row);
121 }
122
123 let max_widths = table.column_max_content_widths();
124 let mut separator = Row::new();
125 for width in max_widths {
126 separator.add_cell(Cell::new("-".repeat(width as usize)));
127 }
128 table.add_row(separator);
129
130 let mut row = Row::new();
131 row.add_cell(Cell::new("Tables"))
132 .add_cell(Cell::new(""))
133 .add_cell(Cell::new(""))
134 .add_cell(Cell::new(""))
135 .add_cell(Cell::new(""))
136 .add_cell(Cell::new(human_bytes(total_size as f64)));
137 table.add_row(row);
138
139 let freelist = tx.inner().env().freelist()?;
140 let pagesize =
141 tx.inner().db_stat(mdbx::Database::freelist_db().dbi())?.page_size() as usize;
142 let freelist_size = freelist * pagesize;
143
144 let mut row = Row::new();
145 row.add_cell(Cell::new("Freelist"))
146 .add_cell(Cell::new(freelist))
147 .add_cell(Cell::new(""))
148 .add_cell(Cell::new(""))
149 .add_cell(Cell::new(""))
150 .add_cell(Cell::new(human_bytes(freelist_size as f64)));
151 table.add_row(row);
152
153 Ok::<(), eyre::Report>(())
154 })??;
155
156 Ok(table)
157 }
158
159 fn rocksdb_stats_table<N: NodeTypesWithDB>(&self, tool: &DbTool<N>) -> ComfyTable {
160 let mut table = ComfyTable::new();
161 table.load_preset(comfy_table::presets::ASCII_MARKDOWN);
162 table.set_header([
163 "RocksDB Table Name",
164 "# Entries",
165 "SST Size",
166 "Memtable Size",
167 "Total Size",
168 "Pending Compaction",
169 ]);
170
171 let stats = tool.provider_factory.rocksdb_provider().table_stats();
172 let mut total_sst: u64 = 0;
173 let mut total_memtable: u64 = 0;
174 let mut total_size: u64 = 0;
175 let mut total_pending: u64 = 0;
176
177 for stat in &stats {
178 total_sst += stat.sst_size_bytes;
179 total_memtable += stat.memtable_size_bytes;
180 total_size += stat.estimated_size_bytes;
181 total_pending += stat.pending_compaction_bytes;
182 let mut row = Row::new();
183 row.add_cell(Cell::new(&stat.name))
184 .add_cell(Cell::new(stat.estimated_num_keys))
185 .add_cell(Cell::new(human_bytes(stat.sst_size_bytes as f64)))
186 .add_cell(Cell::new(human_bytes(stat.memtable_size_bytes as f64)))
187 .add_cell(Cell::new(human_bytes(stat.estimated_size_bytes as f64)))
188 .add_cell(Cell::new(human_bytes(stat.pending_compaction_bytes as f64)));
189 table.add_row(row);
190 }
191
192 if !stats.is_empty() {
193 let max_widths = table.column_max_content_widths();
194 let mut separator = Row::new();
195 for width in max_widths {
196 separator.add_cell(Cell::new("-".repeat(width as usize)));
197 }
198 table.add_row(separator);
199
200 let mut row = Row::new();
201 row.add_cell(Cell::new("RocksDB Total"))
202 .add_cell(Cell::new(""))
203 .add_cell(Cell::new(human_bytes(total_sst as f64)))
204 .add_cell(Cell::new(human_bytes(total_memtable as f64)))
205 .add_cell(Cell::new(human_bytes(total_size as f64)))
206 .add_cell(Cell::new(human_bytes(total_pending as f64)));
207 table.add_row(row);
208
209 let wal_size = tool.provider_factory.rocksdb_provider().wal_size_bytes();
210 let mut row = Row::new();
211 row.add_cell(Cell::new("WAL"))
212 .add_cell(Cell::new(""))
213 .add_cell(Cell::new(""))
214 .add_cell(Cell::new(""))
215 .add_cell(Cell::new(human_bytes(wal_size as f64)))
216 .add_cell(Cell::new(""));
217 table.add_row(row);
218 }
219
220 table
221 }
222
223 fn static_files_stats_table<N: NodePrimitives>(
224 &self,
225 data_dir: ChainPath<DataDirPath>,
226 ) -> eyre::Result<ComfyTable> {
227 let mut table = ComfyTable::new();
228 table.load_preset(comfy_table::presets::ASCII_MARKDOWN);
229
230 if self.detailed_sizes {
231 table.set_header([
232 "Segment",
233 "Block Range",
234 "Transaction Range",
235 "Shape (columns x rows)",
236 "Data Size",
237 "Index Size",
238 "Offsets Size",
239 "Config Size",
240 "Total Size",
241 ]);
242 } else {
243 table.set_header([
244 "Segment",
245 "Block Range",
246 "Transaction Range",
247 "Shape (columns x rows)",
248 "Size",
249 ]);
250 }
251
252 let static_files = iter_static_files(&data_dir.static_files())?;
253 let static_file_provider =
254 StaticFileProvider::<N>::read_only(data_dir.static_files(), false)?;
255
256 let mut total_data_size = 0;
257 let mut total_index_size = 0;
258 let mut total_offsets_size = 0;
259 let mut total_config_size = 0;
260
261 for (segment, ranges) in static_files.into_iter().sorted_by_key(|(segment, _)| *segment) {
262 let (
263 mut segment_columns,
264 mut segment_rows,
265 mut segment_data_size,
266 mut segment_index_size,
267 mut segment_offsets_size,
268 mut segment_config_size,
269 ) = (0, 0, 0, 0, 0, 0);
270
271 for (block_range, header) in &ranges {
272 let fixed_block_range =
273 static_file_provider.find_fixed_range(segment, block_range.start());
274 let jar_provider = static_file_provider
275 .get_segment_provider_for_range(segment, || Some(fixed_block_range), None)?
276 .ok_or_else(|| {
277 eyre::eyre!("Failed to get segment provider for segment: {}", segment)
278 })?;
279
280 let columns = jar_provider.columns();
281 let rows = jar_provider.rows();
282
283 let data_size = fs::metadata(jar_provider.data_path())
284 .map(|metadata| metadata.len())
285 .unwrap_or_default();
286 let index_size = fs::metadata(jar_provider.index_path())
287 .map(|metadata| metadata.len())
288 .unwrap_or_default();
289 let offsets_size = fs::metadata(jar_provider.offsets_path())
290 .map(|metadata| metadata.len())
291 .unwrap_or_default();
292 let config_size = fs::metadata(jar_provider.config_path())
293 .map(|metadata| metadata.len())
294 .unwrap_or_default();
295
296 if self.detailed_segments {
297 let mut row = Row::new();
298 row.add_cell(Cell::new(segment))
299 .add_cell(Cell::new(format!("{block_range}")))
300 .add_cell(Cell::new(
301 header.tx_range().map_or("N/A".to_string(), |range| format!("{range}")),
302 ))
303 .add_cell(Cell::new(format!("{columns} x {rows}")));
304 if self.detailed_sizes {
305 row.add_cell(Cell::new(human_bytes(data_size as f64)))
306 .add_cell(Cell::new(human_bytes(index_size as f64)))
307 .add_cell(Cell::new(human_bytes(offsets_size as f64)))
308 .add_cell(Cell::new(human_bytes(config_size as f64)));
309 }
310 row.add_cell(Cell::new(human_bytes(
311 (data_size + index_size + offsets_size + config_size) as f64,
312 )));
313 table.add_row(row);
314 } else {
315 if segment_columns > 0 {
316 assert_eq!(segment_columns, columns);
317 } else {
318 segment_columns = columns;
319 }
320 segment_rows += rows;
321 segment_data_size += data_size;
322 segment_index_size += index_size;
323 segment_offsets_size += offsets_size;
324 segment_config_size += config_size;
325 }
326
327 total_data_size += data_size;
328 total_index_size += index_size;
329 total_offsets_size += offsets_size;
330 total_config_size += config_size;
331
332 drop(jar_provider);
334
335 static_file_provider.remove_cached_provider(segment, fixed_block_range.end());
337 }
338
339 if !self.detailed_segments {
340 let first_ranges = ranges.first().expect("not empty list of ranges");
341 let last_ranges = ranges.last().expect("not empty list of ranges");
342
343 let block_range =
344 SegmentRangeInclusive::new(first_ranges.0.start(), last_ranges.0.end());
345
346 let tx_range = {
349 let start = ranges
350 .iter()
351 .find_map(|(_, header)| header.tx_range().map(|range| range.start()))
352 .unwrap_or_default();
353 let end = ranges
354 .iter()
355 .rev()
356 .find_map(|(_, header)| header.tx_range().map(|range| range.end()));
357 end.map(|end| SegmentRangeInclusive::new(start, end))
358 };
359
360 let mut row = Row::new();
361 row.add_cell(Cell::new(segment))
362 .add_cell(Cell::new(format!("{block_range}")))
363 .add_cell(Cell::new(
364 tx_range.map_or("N/A".to_string(), |tx_range| format!("{tx_range}")),
365 ))
366 .add_cell(Cell::new(format!("{segment_columns} x {segment_rows}")));
367 if self.detailed_sizes {
368 row.add_cell(Cell::new(human_bytes(segment_data_size as f64)))
369 .add_cell(Cell::new(human_bytes(segment_index_size as f64)))
370 .add_cell(Cell::new(human_bytes(segment_offsets_size as f64)))
371 .add_cell(Cell::new(human_bytes(segment_config_size as f64)));
372 }
373 row.add_cell(Cell::new(human_bytes(
374 (segment_data_size +
375 segment_index_size +
376 segment_offsets_size +
377 segment_config_size) as f64,
378 )));
379 table.add_row(row);
380 }
381 }
382
383 let max_widths = table.column_max_content_widths();
384 let mut separator = Row::new();
385 for width in max_widths {
386 separator.add_cell(Cell::new("-".repeat(width as usize)));
387 }
388 table.add_row(separator);
389
390 let mut row = Row::new();
391 row.add_cell(Cell::new("Total"))
392 .add_cell(Cell::new(""))
393 .add_cell(Cell::new(""))
394 .add_cell(Cell::new(""));
395 if self.detailed_sizes {
396 row.add_cell(Cell::new(human_bytes(total_data_size as f64)))
397 .add_cell(Cell::new(human_bytes(total_index_size as f64)))
398 .add_cell(Cell::new(human_bytes(total_offsets_size as f64)))
399 .add_cell(Cell::new(human_bytes(total_config_size as f64)));
400 }
401 row.add_cell(Cell::new(human_bytes(
402 (total_data_size + total_index_size + total_offsets_size + total_config_size) as f64,
403 )));
404 table.add_row(row);
405
406 Ok(table)
407 }
408
409 fn checksum_report<N: ProviderNodeTypes>(&self, tool: &DbTool<N>) -> eyre::Result<ComfyTable> {
410 let mut table = ComfyTable::new();
411 table.load_preset(comfy_table::presets::ASCII_MARKDOWN);
412 table.set_header(vec![Cell::new("Table"), Cell::new("Checksum"), Cell::new("Elapsed")]);
413
414 let db_tables = Tables::ALL;
415 let mut total_elapsed = Duration::default();
416
417 for &db_table in db_tables {
418 let (checksum, elapsed) = ChecksumViewer::new(tool).view_rt(db_table).unwrap();
419
420 total_elapsed += elapsed;
422
423 let mut row = Row::new();
425 row.add_cell(Cell::new(db_table));
426 row.add_cell(Cell::new(format!("{checksum:x}")));
427 row.add_cell(Cell::new(format!("{elapsed:?}")));
428 table.add_row(row);
429 }
430
431 let max_widths = table.column_max_content_widths();
433 let mut separator = Row::new();
434 for width in max_widths {
435 separator.add_cell(Cell::new("-".repeat(width as usize)));
436 }
437 table.add_row(separator);
438
439 let mut row = Row::new();
441 row.add_cell(Cell::new("Total elapsed"));
442 row.add_cell(Cell::new(""));
443 row.add_cell(Cell::new(format!("{total_elapsed:?}")));
444 table.add_row(row);
445
446 Ok(table)
447 }
448}