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#[derive(Parser, Debug)]
22pub struct TreeStatsCommand {
23 device: PathBuf,
25
26 #[clap(short = 'b', long = "raw")]
28 raw: bool,
29
30 #[clap(short = 't', long = "tree")]
32 tree: Option<String>,
33}
34
35fn 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)] 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)] fn 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 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 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}