reth_cli_commands/db/
stats.rs

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)]
19/// The arguments for the `reth db stats` command
20pub struct Command {
21    /// Skip consistency checks for static files.
22    #[arg(long, default_value_t = false)]
23    pub(crate) skip_consistency_checks: bool,
24
25    /// Show only the total size for static files.
26    #[arg(long, default_value_t = false)]
27    detailed_sizes: bool,
28
29    /// Show detailed information per static file segment.
30    #[arg(long, default_value_t = false)]
31    detailed_segments: bool,
32
33    /// Show a checksum of each table in the database.
34    ///
35    /// WARNING: this option will take a long time to run, as it needs to traverse and hash the
36    /// entire database.
37    ///
38    /// For individual table checksums, use the `reth db checksum` command.
39    #[arg(long, default_value_t = false)]
40    checksum: bool,
41}
42
43impl Command {
44    /// Execute `db stats` command
45    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                // Defaults to 16KB right now but we should
95                // re-evaluate depending on the DB we end up using
96                // (e.g. REDB does not have these options as configurable intentionally)
97                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                // Manually drop provider, otherwise removal from cache will deadlock.
260                drop(jar_provider);
261
262                // Removes from cache, since if we have many files, it may hit ulimit limits
263                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                // Transaction ranges can be empty, so we need to find the first and last which are
274                // not.
275                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            // increment duration for final report
348            total_elapsed += elapsed;
349
350            // add rows containing checksums to the table
351            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        // add a separator for the final report
359        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        // add the final report
367        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}