Skip to main content

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::{
15    providers::{ProviderNodeTypes, StaticFileProvider},
16    RocksDBProviderFactory,
17};
18use reth_static_file_types::SegmentRangeInclusive;
19use std::time::Duration;
20
21#[derive(Parser, Debug)]
22/// The arguments for the `reth db stats` command
23pub struct Command {
24    /// Skip consistency checks for static files.
25    #[arg(long, default_value_t = false)]
26    pub(crate) skip_consistency_checks: bool,
27
28    /// Show only the total size for static files.
29    #[arg(long, default_value_t = false)]
30    detailed_sizes: bool,
31
32    /// Show detailed information per static file segment.
33    #[arg(long, default_value_t = false)]
34    detailed_segments: bool,
35
36    /// Show a checksum of each table in the database.
37    ///
38    /// WARNING: this option will take a long time to run, as it needs to traverse and hash the
39    /// entire database.
40    ///
41    /// For individual table checksums, use the `reth db checksum` command.
42    #[arg(long, default_value_t = false)]
43    checksum: bool,
44}
45
46impl Command {
47    /// Execute `db stats` command
48    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                // Defaults to 16KB right now but we should
103                // re-evaluate depending on the DB we end up using
104                // (e.g. REDB does not have these options as configurable intentionally)
105                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                // Manually drop provider, otherwise removal from cache will deadlock.
333                drop(jar_provider);
334
335                // Removes from cache, since if we have many files, it may hit ulimit limits
336                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                // Transaction ranges can be empty, so we need to find the first and last which are
347                // not.
348                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            // increment duration for final report
421            total_elapsed += elapsed;
422
423            // add rows containing checksums to the table
424            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        // add a separator for the final report
432        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        // add the final report
440        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}