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 = 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 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 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}