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#[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 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 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 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}