Skip to main content

btrfs_cli/inspect/
tree_stats.rs

1use crate::{
2    Format, 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            Ok(raw::BTRFS_DATA_RELOC_TREE_OBJECTID as u64)
51        }
52        _ => s.parse::<u64>().with_context(|| {
53            format!("cannot parse tree id '{s}' (expected a name or number)")
54        }),
55    }
56}
57
58fn tree_name(id: u64) -> String {
59    match id as u32 {
60        x if x == raw::BTRFS_ROOT_TREE_OBJECTID => "root tree".to_string(),
61        x if x == raw::BTRFS_EXTENT_TREE_OBJECTID => "extent tree".to_string(),
62        x if x == raw::BTRFS_CHUNK_TREE_OBJECTID => "chunk tree".to_string(),
63        x if x == raw::BTRFS_DEV_TREE_OBJECTID => "dev tree".to_string(),
64        x if x == raw::BTRFS_FS_TREE_OBJECTID => "fs tree".to_string(),
65        x if x == raw::BTRFS_CSUM_TREE_OBJECTID => "csum tree".to_string(),
66        x if x == raw::BTRFS_QUOTA_TREE_OBJECTID => "quota tree".to_string(),
67        x if x == raw::BTRFS_UUID_TREE_OBJECTID => "uuid tree".to_string(),
68        x if x == raw::BTRFS_FREE_SPACE_TREE_OBJECTID => {
69            "free-space tree".to_string()
70        }
71        x if x as i32 == raw::BTRFS_DATA_RELOC_TREE_OBJECTID => {
72            "data-reloc tree".to_string()
73        }
74        _ => format!("tree {id}"),
75    }
76}
77
78fn print_stats(
79    name: &str,
80    stats: &TreeStats,
81    elapsed_secs: u64,
82    elapsed_usecs: u32,
83    fmt: &SizeFormat,
84) {
85    println!("Calculating size of {name}");
86    println!("\tTotal size: {}", fmt_size(stats.total_bytes, fmt));
87    println!("\t\tInline data: {}", fmt_size(stats.total_inline, fmt));
88    println!("\tTotal seeks: {}", stats.total_seeks);
89    println!("\t\tForward seeks: {}", stats.forward_seeks);
90    println!("\t\tBackward seeks: {}", stats.backward_seeks);
91    let avg_seek = if stats.total_seeks > 0 {
92        stats.total_seek_len / stats.total_seeks
93    } else {
94        0
95    };
96    println!("\t\tAvg seek len: {}", fmt_size(avg_seek, fmt));
97
98    // When no seeks occurred, the C reference sets total_clusters=1, min=0.
99    let (total_clusters, min_cluster, max_cluster, avg_cluster) =
100        if stats.min_cluster_size == u64::MAX {
101            (1u64, 0u64, stats.max_cluster_size, 0u64)
102        } else {
103            let avg = if stats.total_clusters > 0 {
104                stats.total_cluster_size / stats.total_clusters
105            } else {
106                0
107            };
108            (
109                stats.total_clusters,
110                stats.min_cluster_size,
111                stats.max_cluster_size,
112                avg,
113            )
114        };
115    println!("\tTotal clusters: {total_clusters}");
116    println!("\t\tAvg cluster size: {}", fmt_size(avg_cluster, fmt));
117    println!("\t\tMin cluster size: {}", fmt_size(min_cluster, fmt));
118    println!("\t\tMax cluster size: {}", fmt_size(max_cluster, fmt));
119
120    let spread = stats.highest_bytenr.saturating_sub(stats.lowest_bytenr);
121    println!("\tTotal disk spread: {}", fmt_size(spread, fmt));
122    println!("\tTotal read time: {elapsed_secs} s {elapsed_usecs} us");
123    println!("\tLevels: {}", stats.levels);
124    println!("\tTotal nodes: {}", stats.total_nodes);
125
126    for i in 0..stats.levels as usize {
127        let count = stats.node_counts.get(i).copied().unwrap_or(0);
128        if i == 0 {
129            println!("\t\tOn level {i}: {count:8}");
130        } else {
131            let child_count =
132                stats.node_counts.get(i - 1).copied().unwrap_or(0);
133            let fanout = if count > 0 { child_count / count } else { 0 };
134            println!("\t\tOn level {i}: {count:8}  (avg fanout {fanout})");
135        }
136    }
137}
138
139impl Runnable for TreeStatsCommand {
140    fn run(&self, _format: Format, _dry_run: bool) -> Result<()> {
141        let file = crate::util::open_path(&self.device)?;
142        let mut fs = reader::filesystem_open(file).with_context(|| {
143            format!("failed to open '{}'", self.device.display())
144        })?;
145
146        let size_fmt = if self.raw {
147            SizeFormat::Raw
148        } else {
149            SizeFormat::HumanIec
150        };
151
152        if let Some(ref tree_spec) = self.tree {
153            let tree_id = parse_tree_id(tree_spec)?;
154            let (root_logical, _) =
155                fs.tree_roots.get(&tree_id).copied().ok_or_else(|| {
156                    anyhow::anyhow!("tree {tree_id} not found in filesystem")
157                })?;
158
159            let name = tree_name(tree_id);
160            let start = Instant::now();
161            let stats = tree_stats_collect(&mut fs.reader, root_logical, true)
162                .with_context(|| format!("failed to walk {name}"))?;
163            let elapsed = start.elapsed();
164            print_stats(
165                &name,
166                &stats,
167                elapsed.as_secs(),
168                elapsed.subsec_micros(),
169                &size_fmt,
170            );
171        } else {
172            // Default: root, extent, csum, fs trees.
173            // The root tree is bootstrapped from the superblock, not a
174            // ROOT_ITEM, so its logical address comes from superblock.root.
175            let root_tree_logical = fs.superblock.root;
176            let default_trees: &[(u64, u64, bool)] = &[
177                (
178                    u64::from(raw::BTRFS_ROOT_TREE_OBJECTID),
179                    root_tree_logical,
180                    false,
181                ),
182                (
183                    u64::from(raw::BTRFS_EXTENT_TREE_OBJECTID),
184                    fs.tree_roots
185                        .get(&u64::from(raw::BTRFS_EXTENT_TREE_OBJECTID))
186                        .map_or(0, |&(l, _)| l),
187                    false,
188                ),
189                (
190                    u64::from(raw::BTRFS_CSUM_TREE_OBJECTID),
191                    fs.tree_roots
192                        .get(&u64::from(raw::BTRFS_CSUM_TREE_OBJECTID))
193                        .map_or(0, |&(l, _)| l),
194                    false,
195                ),
196                (
197                    u64::from(raw::BTRFS_FS_TREE_OBJECTID),
198                    fs.tree_roots
199                        .get(&u64::from(raw::BTRFS_FS_TREE_OBJECTID))
200                        .map_or(0, |&(l, _)| l),
201                    true,
202                ),
203            ];
204
205            for &(tree_id, root_logical, find_inline) in default_trees {
206                if root_logical == 0 {
207                    continue;
208                }
209                let name = tree_name(tree_id);
210                let start = Instant::now();
211                let stats = tree_stats_collect(
212                    &mut fs.reader,
213                    root_logical,
214                    find_inline,
215                )
216                .with_context(|| format!("failed to walk {name}"))?;
217                let elapsed = start.elapsed();
218                print_stats(
219                    &name,
220                    &stats,
221                    elapsed.as_secs(),
222                    elapsed.subsec_micros(),
223                    &size_fmt,
224                );
225            }
226        }
227
228        Ok(())
229    }
230}