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::providers::{ProviderNodeTypes, StaticFileProvider};
15use reth_static_file_types::SegmentRangeInclusive;
16use std::{sync::Arc, time::Duration};
17
18#[derive(Parser, Debug)]
19pub struct Command {
21 #[arg(long, default_value_t = false)]
23 pub(crate) skip_consistency_checks: bool,
24
25 #[arg(long, default_value_t = false)]
27 detailed_sizes: bool,
28
29 #[arg(long, default_value_t = false)]
31 detailed_segments: bool,
32
33 #[arg(long, default_value_t = false)]
40 checksum: bool,
41}
42
43impl Command {
44 pub fn execute<N: CliNodeTypes<ChainSpec: EthereumHardforks>>(
46 self,
47 data_dir: ChainPath<DataDirPath>,
48 tool: &DbTool<NodeTypesWithDBAdapter<N, Arc<DatabaseEnv>>>,
49 ) -> eyre::Result<()> {
50 if self.checksum {
51 let checksum_report = self.checksum_report(tool)?;
52 println!("{checksum_report}");
53 println!("\n");
54 }
55
56 let static_files_stats_table = self.static_files_stats_table::<N::Primitives>(data_dir)?;
57 println!("{static_files_stats_table}");
58
59 println!("\n");
60
61 let db_stats_table = self.db_stats_table(tool)?;
62 println!("{db_stats_table}");
63
64 Ok(())
65 }
66
67 fn db_stats_table<N: NodeTypesWithDB<DB = Arc<DatabaseEnv>>>(
68 &self,
69 tool: &DbTool<N>,
70 ) -> eyre::Result<ComfyTable> {
71 let mut table = ComfyTable::new();
72 table.load_preset(comfy_table::presets::ASCII_MARKDOWN);
73 table.set_header([
74 "Table Name",
75 "# Entries",
76 "Branch Pages",
77 "Leaf Pages",
78 "Overflow Pages",
79 "Total Size",
80 ]);
81
82 tool.provider_factory.db_ref().view(|tx| {
83 let mut db_tables = Tables::ALL.iter().map(|table| table.name()).collect::<Vec<_>>();
84 db_tables.sort();
85 let mut total_size = 0;
86 for db_table in db_tables {
87 let table_db = tx.inner.open_db(Some(db_table)).wrap_err("Could not open db.")?;
88
89 let stats = tx
90 .inner
91 .db_stat(&table_db)
92 .wrap_err(format!("Could not find table: {db_table}"))?;
93
94 let page_size = stats.page_size() as usize;
98 let leaf_pages = stats.leaf_pages();
99 let branch_pages = stats.branch_pages();
100 let overflow_pages = stats.overflow_pages();
101 let num_pages = leaf_pages + branch_pages + overflow_pages;
102 let table_size = page_size * num_pages;
103
104 total_size += table_size;
105 let mut row = Row::new();
106 row.add_cell(Cell::new(db_table))
107 .add_cell(Cell::new(stats.entries()))
108 .add_cell(Cell::new(branch_pages))
109 .add_cell(Cell::new(leaf_pages))
110 .add_cell(Cell::new(overflow_pages))
111 .add_cell(Cell::new(human_bytes(table_size as f64)));
112 table.add_row(row);
113 }
114
115 let max_widths = table.column_max_content_widths();
116 let mut separator = Row::new();
117 for width in max_widths {
118 separator.add_cell(Cell::new("-".repeat(width as usize)));
119 }
120 table.add_row(separator);
121
122 let mut row = Row::new();
123 row.add_cell(Cell::new("Tables"))
124 .add_cell(Cell::new(""))
125 .add_cell(Cell::new(""))
126 .add_cell(Cell::new(""))
127 .add_cell(Cell::new(""))
128 .add_cell(Cell::new(human_bytes(total_size as f64)));
129 table.add_row(row);
130
131 let freelist = tx.inner.env().freelist()?;
132 let pagesize = tx.inner.db_stat(&mdbx::Database::freelist_db())?.page_size() as usize;
133 let freelist_size = freelist * pagesize;
134
135 let mut row = Row::new();
136 row.add_cell(Cell::new("Freelist"))
137 .add_cell(Cell::new(freelist))
138 .add_cell(Cell::new(""))
139 .add_cell(Cell::new(""))
140 .add_cell(Cell::new(""))
141 .add_cell(Cell::new(human_bytes(freelist_size as f64)));
142 table.add_row(row);
143
144 Ok::<(), eyre::Report>(())
145 })??;
146
147 Ok(table)
148 }
149
150 fn static_files_stats_table<N: NodePrimitives>(
151 &self,
152 data_dir: ChainPath<DataDirPath>,
153 ) -> eyre::Result<ComfyTable> {
154 let mut table = ComfyTable::new();
155 table.load_preset(comfy_table::presets::ASCII_MARKDOWN);
156
157 if self.detailed_sizes {
158 table.set_header([
159 "Segment",
160 "Block Range",
161 "Transaction Range",
162 "Shape (columns x rows)",
163 "Data Size",
164 "Index Size",
165 "Offsets Size",
166 "Config Size",
167 "Total Size",
168 ]);
169 } else {
170 table.set_header([
171 "Segment",
172 "Block Range",
173 "Transaction Range",
174 "Shape (columns x rows)",
175 "Size",
176 ]);
177 }
178
179 let static_files = iter_static_files(&data_dir.static_files())?;
180 let static_file_provider =
181 StaticFileProvider::<N>::read_only(data_dir.static_files(), false)?;
182
183 let mut total_data_size = 0;
184 let mut total_index_size = 0;
185 let mut total_offsets_size = 0;
186 let mut total_config_size = 0;
187
188 for (segment, ranges) in static_files.into_iter().sorted_by_key(|(segment, _)| *segment) {
189 let (
190 mut segment_columns,
191 mut segment_rows,
192 mut segment_data_size,
193 mut segment_index_size,
194 mut segment_offsets_size,
195 mut segment_config_size,
196 ) = (0, 0, 0, 0, 0, 0);
197
198 for (block_range, header) in &ranges {
199 let fixed_block_range =
200 static_file_provider.find_fixed_range(segment, block_range.start());
201 let jar_provider = static_file_provider
202 .get_segment_provider_for_range(segment, || Some(fixed_block_range), None)?
203 .ok_or_else(|| {
204 eyre::eyre!("Failed to get segment provider for segment: {}", segment)
205 })?;
206
207 let columns = jar_provider.columns();
208 let rows = jar_provider.rows();
209
210 let data_size = fs::metadata(jar_provider.data_path())
211 .map(|metadata| metadata.len())
212 .unwrap_or_default();
213 let index_size = fs::metadata(jar_provider.index_path())
214 .map(|metadata| metadata.len())
215 .unwrap_or_default();
216 let offsets_size = fs::metadata(jar_provider.offsets_path())
217 .map(|metadata| metadata.len())
218 .unwrap_or_default();
219 let config_size = fs::metadata(jar_provider.config_path())
220 .map(|metadata| metadata.len())
221 .unwrap_or_default();
222
223 if self.detailed_segments {
224 let mut row = Row::new();
225 row.add_cell(Cell::new(segment))
226 .add_cell(Cell::new(format!("{block_range}")))
227 .add_cell(Cell::new(
228 header.tx_range().map_or("N/A".to_string(), |range| format!("{range}")),
229 ))
230 .add_cell(Cell::new(format!("{columns} x {rows}")));
231 if self.detailed_sizes {
232 row.add_cell(Cell::new(human_bytes(data_size as f64)))
233 .add_cell(Cell::new(human_bytes(index_size as f64)))
234 .add_cell(Cell::new(human_bytes(offsets_size as f64)))
235 .add_cell(Cell::new(human_bytes(config_size as f64)));
236 }
237 row.add_cell(Cell::new(human_bytes(
238 (data_size + index_size + offsets_size + config_size) as f64,
239 )));
240 table.add_row(row);
241 } else {
242 if segment_columns > 0 {
243 assert_eq!(segment_columns, columns);
244 } else {
245 segment_columns = columns;
246 }
247 segment_rows += rows;
248 segment_data_size += data_size;
249 segment_index_size += index_size;
250 segment_offsets_size += offsets_size;
251 segment_config_size += config_size;
252 }
253
254 total_data_size += data_size;
255 total_index_size += index_size;
256 total_offsets_size += offsets_size;
257 total_config_size += config_size;
258
259 drop(jar_provider);
261
262 static_file_provider.remove_cached_provider(segment, fixed_block_range.end());
264 }
265
266 if !self.detailed_segments {
267 let first_ranges = ranges.first().expect("not empty list of ranges");
268 let last_ranges = ranges.last().expect("not empty list of ranges");
269
270 let block_range =
271 SegmentRangeInclusive::new(first_ranges.0.start(), last_ranges.0.end());
272
273 let tx_range = {
276 let start = ranges
277 .iter()
278 .find_map(|(_, header)| header.tx_range().map(|range| range.start()))
279 .unwrap_or_default();
280 let end = ranges
281 .iter()
282 .rev()
283 .find_map(|(_, header)| header.tx_range().map(|range| range.end()));
284 end.map(|end| SegmentRangeInclusive::new(start, end))
285 };
286
287 let mut row = Row::new();
288 row.add_cell(Cell::new(segment))
289 .add_cell(Cell::new(format!("{block_range}")))
290 .add_cell(Cell::new(
291 tx_range.map_or("N/A".to_string(), |tx_range| format!("{tx_range}")),
292 ))
293 .add_cell(Cell::new(format!("{segment_columns} x {segment_rows}")));
294 if self.detailed_sizes {
295 row.add_cell(Cell::new(human_bytes(segment_data_size as f64)))
296 .add_cell(Cell::new(human_bytes(segment_index_size as f64)))
297 .add_cell(Cell::new(human_bytes(segment_offsets_size as f64)))
298 .add_cell(Cell::new(human_bytes(segment_config_size as f64)));
299 }
300 row.add_cell(Cell::new(human_bytes(
301 (segment_data_size +
302 segment_index_size +
303 segment_offsets_size +
304 segment_config_size) as f64,
305 )));
306 table.add_row(row);
307 }
308 }
309
310 let max_widths = table.column_max_content_widths();
311 let mut separator = Row::new();
312 for width in max_widths {
313 separator.add_cell(Cell::new("-".repeat(width as usize)));
314 }
315 table.add_row(separator);
316
317 let mut row = Row::new();
318 row.add_cell(Cell::new("Total"))
319 .add_cell(Cell::new(""))
320 .add_cell(Cell::new(""))
321 .add_cell(Cell::new(""));
322 if self.detailed_sizes {
323 row.add_cell(Cell::new(human_bytes(total_data_size as f64)))
324 .add_cell(Cell::new(human_bytes(total_index_size as f64)))
325 .add_cell(Cell::new(human_bytes(total_offsets_size as f64)))
326 .add_cell(Cell::new(human_bytes(total_config_size as f64)));
327 }
328 row.add_cell(Cell::new(human_bytes(
329 (total_data_size + total_index_size + total_offsets_size + total_config_size) as f64,
330 )));
331 table.add_row(row);
332
333 Ok(table)
334 }
335
336 fn checksum_report<N: ProviderNodeTypes>(&self, tool: &DbTool<N>) -> eyre::Result<ComfyTable> {
337 let mut table = ComfyTable::new();
338 table.load_preset(comfy_table::presets::ASCII_MARKDOWN);
339 table.set_header(vec![Cell::new("Table"), Cell::new("Checksum"), Cell::new("Elapsed")]);
340
341 let db_tables = Tables::ALL;
342 let mut total_elapsed = Duration::default();
343
344 for &db_table in db_tables {
345 let (checksum, elapsed) = ChecksumViewer::new(tool).view_rt(db_table).unwrap();
346
347 total_elapsed += elapsed;
349
350 let mut row = Row::new();
352 row.add_cell(Cell::new(db_table));
353 row.add_cell(Cell::new(format!("{checksum:x}")));
354 row.add_cell(Cell::new(format!("{elapsed:?}")));
355 table.add_row(row);
356 }
357
358 let max_widths = table.column_max_content_widths();
360 let mut separator = Row::new();
361 for width in max_widths {
362 separator.add_cell(Cell::new("-".repeat(width as usize)));
363 }
364 table.add_row(separator);
365
366 let mut row = Row::new();
368 row.add_cell(Cell::new("Total elapsed"));
369 row.add_cell(Cell::new(""));
370 row.add_cell(Cell::new(format!("{total_elapsed:?}")));
371 table.add_row(row);
372
373 Ok(table)
374 }
375}