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 = if stats.total_seeks > 0 {
95        stats.total_seek_len / stats.total_seeks
96    } else {
97        0
98    };
99    println!("\t\tAvg seek len: {}", fmt_size(avg_seek, fmt));
100
101    // When no seeks occurred, the C reference sets total_clusters=1, min=0.
102    let (total_clusters, min_cluster, max_cluster, avg_cluster) =
103        if stats.min_cluster_size == u64::MAX {
104            (1u64, 0u64, stats.max_cluster_size, 0u64)
105        } else {
106            let avg = if stats.total_clusters > 0 {
107                stats.total_cluster_size / stats.total_clusters
108            } else {
109                0
110            };
111            (
112                stats.total_clusters,
113                stats.min_cluster_size,
114                stats.max_cluster_size,
115                avg,
116            )
117        };
118    println!("\tTotal clusters: {total_clusters}");
119    println!("\t\tAvg cluster size: {}", fmt_size(avg_cluster, fmt));
120    println!("\t\tMin cluster size: {}", fmt_size(min_cluster, fmt));
121    println!("\t\tMax cluster size: {}", fmt_size(max_cluster, fmt));
122
123    let spread = stats.highest_bytenr.saturating_sub(stats.lowest_bytenr);
124    println!("\tTotal disk spread: {}", fmt_size(spread, fmt));
125    println!("\tTotal read time: {elapsed_secs} s {elapsed_usecs} us");
126    println!("\tLevels: {}", stats.levels);
127    println!("\tTotal nodes: {}", stats.total_nodes);
128
129    for i in 0..stats.levels as usize {
130        let count = stats.node_counts.get(i).copied().unwrap_or(0);
131        if i == 0 {
132            println!("\t\tOn level {i}: {count:8}");
133        } else {
134            let child_count =
135                stats.node_counts.get(i - 1).copied().unwrap_or(0);
136            let fanout = if count > 0 { child_count / count } else { 0 };
137            println!("\t\tOn level {i}: {count:8}  (avg fanout {fanout})");
138        }
139    }
140}
141
142impl Runnable for TreeStatsCommand {
143    fn run(&self, _ctx: &RunContext) -> Result<()> {
144        let file = crate::util::open_path(&self.device)?;
145        let mut fs = reader::filesystem_open(file).with_context(|| {
146            format!("failed to open '{}'", self.device.display())
147        })?;
148
149        let size_fmt = if self.raw {
150            SizeFormat::Raw
151        } else {
152            SizeFormat::HumanIec
153        };
154
155        if let Some(ref tree_spec) = self.tree {
156            let tree_id = parse_tree_id(tree_spec)?;
157            let (root_logical, _) =
158                fs.tree_roots.get(&tree_id).copied().ok_or_else(|| {
159                    anyhow::anyhow!("tree {tree_id} not found in filesystem")
160                })?;
161
162            let name = tree_name(tree_id);
163            let start = Instant::now();
164            let stats = tree_stats_collect(&mut fs.reader, root_logical, true)
165                .with_context(|| format!("failed to walk {name}"))?;
166            let elapsed = start.elapsed();
167            print_stats(
168                &name,
169                &stats,
170                elapsed.as_secs(),
171                elapsed.subsec_micros(),
172                &size_fmt,
173            );
174        } else {
175            // Default: root, extent, csum, fs trees.
176            // The root tree is bootstrapped from the superblock, not a
177            // ROOT_ITEM, so its logical address comes from superblock.root.
178            let root_tree_logical = fs.superblock.root;
179            let default_trees: &[(u64, u64, bool)] = &[
180                (
181                    u64::from(raw::BTRFS_ROOT_TREE_OBJECTID),
182                    root_tree_logical,
183                    false,
184                ),
185                (
186                    u64::from(raw::BTRFS_EXTENT_TREE_OBJECTID),
187                    fs.tree_roots
188                        .get(&u64::from(raw::BTRFS_EXTENT_TREE_OBJECTID))
189                        .map_or(0, |&(l, _)| l),
190                    false,
191                ),
192                (
193                    u64::from(raw::BTRFS_CSUM_TREE_OBJECTID),
194                    fs.tree_roots
195                        .get(&u64::from(raw::BTRFS_CSUM_TREE_OBJECTID))
196                        .map_or(0, |&(l, _)| l),
197                    false,
198                ),
199                (
200                    u64::from(raw::BTRFS_FS_TREE_OBJECTID),
201                    fs.tree_roots
202                        .get(&u64::from(raw::BTRFS_FS_TREE_OBJECTID))
203                        .map_or(0, |&(l, _)| l),
204                    true,
205                ),
206            ];
207
208            for &(tree_id, root_logical, find_inline) in default_trees {
209                if root_logical == 0 {
210                    continue;
211                }
212                let name = tree_name(tree_id);
213                let start = Instant::now();
214                let stats = tree_stats_collect(
215                    &mut fs.reader,
216                    root_logical,
217                    find_inline,
218                )
219                .with_context(|| format!("failed to walk {name}"))?;
220                let elapsed = start.elapsed();
221                print_stats(
222                    &name,
223                    &stats,
224                    elapsed.as_secs(),
225                    elapsed.subsec_micros(),
226                    &size_fmt,
227                );
228            }
229        }
230
231        Ok(())
232    }
233}