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