Skip to main content

btrfs_cli/inspect/
tree_stats.rs

1use crate::{
2    RunContext, Runnable,
3    util::{SizeFormat, fmt_size},
4};
5use anyhow::{Context, Result};
6use btrfs_disk::{
7    raw,
8    reader::{self, TreeStats, tree_stats_collect},
9};
10use clap::Parser;
11use std::{path::PathBuf, time::Instant};
12
13/// Print statistics about trees in a btrfs filesystem
14///
15/// Reads the filesystem image or block device directly (no mount required).
16/// By default prints statistics for the root, extent, checksum, and fs trees.
17/// Use -t to restrict to a single tree.
18///
19/// When the filesystem is mounted the numbers may be slightly inaccurate due
20/// to concurrent modifications.
21#[derive(Parser, Debug)]
22pub struct TreeStatsCommand {
23    /// Path to a btrfs block device or image file
24    device: PathBuf,
25
26    /// Print sizes in raw bytes instead of human-readable form
27    #[clap(short = 'b', long = "raw")]
28    raw: bool,
29
30    /// Only print stats for the given tree (name or numeric ID)
31    #[clap(short = 't', long = "tree")]
32    tree: Option<String>,
33}
34
35/// Map a tree name string or decimal integer to a tree object ID.
36fn parse_tree_id(s: &str) -> Result<u64> {
37    match s {
38        "root" => Ok(u64::from(raw::BTRFS_ROOT_TREE_OBJECTID)),
39        "extent" => Ok(u64::from(raw::BTRFS_EXTENT_TREE_OBJECTID)),
40        "chunk" => Ok(u64::from(raw::BTRFS_CHUNK_TREE_OBJECTID)),
41        "dev" => Ok(u64::from(raw::BTRFS_DEV_TREE_OBJECTID)),
42        "fs" => Ok(u64::from(raw::BTRFS_FS_TREE_OBJECTID)),
43        "csum" | "checksum" => Ok(u64::from(raw::BTRFS_CSUM_TREE_OBJECTID)),
44        "quota" => Ok(u64::from(raw::BTRFS_QUOTA_TREE_OBJECTID)),
45        "uuid" => Ok(u64::from(raw::BTRFS_UUID_TREE_OBJECTID)),
46        "free-space" | "free_space" => {
47            Ok(u64::from(raw::BTRFS_FREE_SPACE_TREE_OBJECTID))
48        }
49        "data-reloc" | "data_reloc" => {
50            #[allow(clippy::cast_sign_loss)] // known constant
51            Ok(raw::BTRFS_DATA_RELOC_TREE_OBJECTID as u64)
52        }
53        _ => s.parse::<u64>().with_context(|| {
54            format!("cannot parse tree id '{s}' (expected a name or number)")
55        }),
56    }
57}
58
59#[allow(clippy::cast_possible_truncation, clippy::cast_possible_wrap)]
60fn tree_name(id: u64) -> String {
61    match id as u32 {
62        x if x == raw::BTRFS_ROOT_TREE_OBJECTID => "root tree".to_string(),
63        x if x == raw::BTRFS_EXTENT_TREE_OBJECTID => "extent tree".to_string(),
64        x if x == raw::BTRFS_CHUNK_TREE_OBJECTID => "chunk tree".to_string(),
65        x if x == raw::BTRFS_DEV_TREE_OBJECTID => "dev tree".to_string(),
66        x if x == raw::BTRFS_FS_TREE_OBJECTID => "fs tree".to_string(),
67        x if x == raw::BTRFS_CSUM_TREE_OBJECTID => "csum tree".to_string(),
68        x if x == raw::BTRFS_QUOTA_TREE_OBJECTID => "quota tree".to_string(),
69        x if x == raw::BTRFS_UUID_TREE_OBJECTID => "uuid tree".to_string(),
70        x if x == raw::BTRFS_FREE_SPACE_TREE_OBJECTID => {
71            "free-space tree".to_string()
72        }
73        x if x as i32 == raw::BTRFS_DATA_RELOC_TREE_OBJECTID => {
74            "data-reloc tree".to_string()
75        }
76        _ => format!("tree {id}"),
77    }
78}
79
80#[allow(clippy::similar_names)] // elapsed_secs and elapsed_usecs are distinct
81fn print_stats(
82    name: &str,
83    stats: &TreeStats,
84    elapsed_secs: u64,
85    elapsed_usecs: u32,
86    fmt: &SizeFormat,
87) {
88    println!("Calculating size of {name}");
89    println!("\tTotal size: {}", fmt_size(stats.total_bytes, fmt));
90    println!("\t\tInline data: {}", fmt_size(stats.total_inline, fmt));
91    println!("\tTotal seeks: {}", stats.total_seeks);
92    println!("\t\tForward seeks: {}", stats.forward_seeks);
93    println!("\t\tBackward seeks: {}", stats.backward_seeks);
94    let avg_seek = stats
95        .total_seek_len
96        .checked_div(stats.total_seeks)
97        .unwrap_or(0);
98    println!("\t\tAvg seek len: {}", fmt_size(avg_seek, fmt));
99
100    // When no seeks occurred, the C reference sets total_clusters=1, min=0.
101    let (total_clusters, min_cluster, max_cluster, avg_cluster) =
102        if stats.min_cluster_size == u64::MAX {
103            (1u64, 0u64, stats.max_cluster_size, 0u64)
104        } else {
105            let avg = stats
106                .total_cluster_size
107                .checked_div(stats.total_clusters)
108                .unwrap_or(0);
109            (
110                stats.total_clusters,
111                stats.min_cluster_size,
112                stats.max_cluster_size,
113                avg,
114            )
115        };
116    println!("\tTotal clusters: {total_clusters}");
117    println!("\t\tAvg cluster size: {}", fmt_size(avg_cluster, fmt));
118    println!("\t\tMin cluster size: {}", fmt_size(min_cluster, fmt));
119    println!("\t\tMax cluster size: {}", fmt_size(max_cluster, fmt));
120
121    let spread = stats.highest_bytenr.saturating_sub(stats.lowest_bytenr);
122    println!("\tTotal disk spread: {}", fmt_size(spread, fmt));
123    println!("\tTotal read time: {elapsed_secs} s {elapsed_usecs} us");
124    println!("\tLevels: {}", stats.levels);
125    println!("\tTotal nodes: {}", stats.total_nodes);
126
127    for i in 0..stats.levels as usize {
128        let count = stats.node_counts.get(i).copied().unwrap_or(0);
129        if i == 0 {
130            println!("\t\tOn level {i}: {count:8}");
131        } else {
132            let child_count =
133                stats.node_counts.get(i - 1).copied().unwrap_or(0);
134            let fanout = child_count.checked_div(count).unwrap_or(0);
135            println!("\t\tOn level {i}: {count:8}  (avg fanout {fanout})");
136        }
137    }
138}
139
140impl Runnable for TreeStatsCommand {
141    fn run(&self, _ctx: &RunContext) -> Result<()> {
142        let file = crate::util::open_path(&self.device)?;
143        let mut fs = reader::filesystem_open(file).with_context(|| {
144            format!("failed to open '{}'", self.device.display())
145        })?;
146
147        let size_fmt = if self.raw {
148            SizeFormat::Raw
149        } else {
150            SizeFormat::HumanIec
151        };
152
153        if let Some(ref tree_spec) = self.tree {
154            let tree_id = parse_tree_id(tree_spec)?;
155            let (root_logical, _) =
156                fs.tree_roots.get(&tree_id).copied().ok_or_else(|| {
157                    anyhow::anyhow!("tree {tree_id} not found in filesystem")
158                })?;
159
160            let name = tree_name(tree_id);
161            let start = Instant::now();
162            let stats = tree_stats_collect(&mut fs.reader, root_logical, true)
163                .with_context(|| format!("failed to walk {name}"))?;
164            let elapsed = start.elapsed();
165            print_stats(
166                &name,
167                &stats,
168                elapsed.as_secs(),
169                elapsed.subsec_micros(),
170                &size_fmt,
171            );
172        } else {
173            // Default: root, extent, csum, fs trees.
174            // The root tree is bootstrapped from the superblock, not a
175            // ROOT_ITEM, so its logical address comes from superblock.root.
176            let root_tree_logical = fs.superblock.root;
177            let default_trees: &[(u64, u64, bool)] = &[
178                (
179                    u64::from(raw::BTRFS_ROOT_TREE_OBJECTID),
180                    root_tree_logical,
181                    false,
182                ),
183                (
184                    u64::from(raw::BTRFS_EXTENT_TREE_OBJECTID),
185                    fs.tree_roots
186                        .get(&u64::from(raw::BTRFS_EXTENT_TREE_OBJECTID))
187                        .map_or(0, |&(l, _)| l),
188                    false,
189                ),
190                (
191                    u64::from(raw::BTRFS_CSUM_TREE_OBJECTID),
192                    fs.tree_roots
193                        .get(&u64::from(raw::BTRFS_CSUM_TREE_OBJECTID))
194                        .map_or(0, |&(l, _)| l),
195                    false,
196                ),
197                (
198                    u64::from(raw::BTRFS_FS_TREE_OBJECTID),
199                    fs.tree_roots
200                        .get(&u64::from(raw::BTRFS_FS_TREE_OBJECTID))
201                        .map_or(0, |&(l, _)| l),
202                    true,
203                ),
204            ];
205
206            for &(tree_id, root_logical, find_inline) in default_trees {
207                if root_logical == 0 {
208                    continue;
209                }
210                let name = tree_name(tree_id);
211                let start = Instant::now();
212                let stats = tree_stats_collect(
213                    &mut fs.reader,
214                    root_logical,
215                    find_inline,
216                )
217                .with_context(|| format!("failed to walk {name}"))?;
218                let elapsed = start.elapsed();
219                print_stats(
220                    &name,
221                    &stats,
222                    elapsed.as_secs(),
223                    elapsed.subsec_micros(),
224                    &size_fmt,
225                );
226            }
227        }
228
229        Ok(())
230    }
231}