Skip to main content

btrfs_cli/filesystem/
usage.rs

1use super::UnitMode;
2use crate::{Format, Runnable, util::human_bytes};
3use anyhow::{Context, Result};
4use btrfs_uapi::{
5    chunk::device_chunk_allocations,
6    device::device_info_all,
7    filesystem::filesystem_info,
8    space::{BlockGroupFlags, SpaceInfo, space_info},
9};
10use clap::Parser;
11use std::{
12    collections::{HashMap, HashSet},
13    fs::File,
14    os::unix::io::AsFd,
15    path::PathBuf,
16};
17
18/// Show detailed information about internal filesystem usage
19#[derive(Parser, Debug)]
20pub struct FilesystemUsageCommand {
21    #[clap(flatten)]
22    pub units: UnitMode,
23
24    /// Use base 1000 for human-readable sizes
25    #[clap(short = 'H', long)]
26    pub human_si: bool,
27
28    /// Show data in tabular format
29    #[clap(short = 'T', long)]
30    pub tabular: bool,
31
32    /// One or more mount points to show usage for
33    #[clap(required = true)]
34    pub paths: Vec<PathBuf>,
35}
36
37/// Number of raw-device copies per chunk for a given profile.
38/// Returns 0 for RAID5/6 which require chunk tree data to compute accurately.
39fn profile_ncopies(flags: BlockGroupFlags) -> u64 {
40    if flags.contains(BlockGroupFlags::RAID1C4) {
41        4
42    } else if flags.contains(BlockGroupFlags::RAID1C3) {
43        3
44    } else if flags.contains(BlockGroupFlags::RAID1)
45        || flags.contains(BlockGroupFlags::DUP)
46        || flags.contains(BlockGroupFlags::RAID10)
47    {
48        2
49    } else if flags.contains(BlockGroupFlags::RAID5)
50        || flags.contains(BlockGroupFlags::RAID6)
51    {
52        0
53    } else {
54        1
55    }
56}
57
58fn has_multiple_profiles(spaces: &[SpaceInfo]) -> bool {
59    let profile_mask = BlockGroupFlags::RAID0
60        | BlockGroupFlags::RAID1
61        | BlockGroupFlags::DUP
62        | BlockGroupFlags::RAID10
63        | BlockGroupFlags::RAID5
64        | BlockGroupFlags::RAID6
65        | BlockGroupFlags::RAID1C3
66        | BlockGroupFlags::RAID1C4
67        | BlockGroupFlags::SINGLE;
68
69    let profiles_for = |type_flag: BlockGroupFlags| {
70        spaces
71            .iter()
72            .filter(|s| {
73                s.flags.contains(type_flag)
74                    && !s.flags.contains(BlockGroupFlags::GLOBAL_RSV)
75            })
76            .map(|s| s.flags & profile_mask)
77            .collect::<HashSet<_>>()
78    };
79
80    profiles_for(BlockGroupFlags::DATA).len() > 1
81        || profiles_for(BlockGroupFlags::METADATA).len() > 1
82        || profiles_for(BlockGroupFlags::SYSTEM).len() > 1
83}
84
85impl Runnable for FilesystemUsageCommand {
86    fn run(&self, _format: Format, _dry_run: bool) -> Result<()> {
87        for (i, path) in self.paths.iter().enumerate() {
88            if i > 0 {
89                println!();
90            }
91            print_usage(path, self.tabular)?;
92        }
93        Ok(())
94    }
95}
96
97fn print_usage(path: &std::path::Path, _tabular: bool) -> Result<()> {
98    let file = File::open(path)
99        .with_context(|| format!("failed to open '{}'", path.display()))?;
100    let fd = file.as_fd();
101
102    let fs = filesystem_info(fd).with_context(|| {
103        format!("failed to get filesystem info for '{}'", path.display())
104    })?;
105    let devices = device_info_all(fd, &fs).with_context(|| {
106        format!("failed to get device info for '{}'", path.display())
107    })?;
108    let spaces = space_info(fd).with_context(|| {
109        format!("failed to get space info for '{}'", path.display())
110    })?;
111
112    // Per-device chunk allocations from the chunk tree.  This requires
113    // CAP_SYS_ADMIN; if it fails we degrade gracefully and note it below.
114    let chunk_allocs = device_chunk_allocations(fd).ok();
115
116    // Map devid -> path for display.
117    let devid_to_path: HashMap<u64, &str> =
118        devices.iter().map(|d| (d.devid, d.path.as_str())).collect();
119
120    let mut r_data_chunks: u64 = 0;
121    let mut r_data_used: u64 = 0;
122    let mut l_data_chunks: u64 = 0;
123    let mut r_meta_chunks: u64 = 0;
124    let mut r_meta_used: u64 = 0;
125    let mut l_meta_chunks: u64 = 0;
126    let mut r_sys_chunks: u64 = 0;
127    let mut r_sys_used: u64 = 0;
128    let mut l_global_reserve: u64 = 0;
129    let mut l_global_reserve_used: u64 = 0;
130    let mut max_ncopies: u64 = 1;
131
132    for s in &spaces {
133        if s.flags.contains(BlockGroupFlags::GLOBAL_RSV) {
134            l_global_reserve = s.total_bytes;
135            l_global_reserve_used = s.used_bytes;
136            continue;
137        }
138        let ncopies = profile_ncopies(s.flags);
139        if ncopies > max_ncopies {
140            max_ncopies = ncopies;
141        }
142        if s.flags.contains(BlockGroupFlags::DATA) {
143            r_data_chunks += s.total_bytes * ncopies;
144            r_data_used += s.used_bytes * ncopies;
145            l_data_chunks += s.total_bytes;
146        }
147        if s.flags.contains(BlockGroupFlags::METADATA) {
148            r_meta_chunks += s.total_bytes * ncopies;
149            r_meta_used += s.used_bytes * ncopies;
150            l_meta_chunks += s.total_bytes;
151        }
152        if s.flags.contains(BlockGroupFlags::SYSTEM) {
153            r_sys_chunks += s.total_bytes * ncopies;
154            r_sys_used += s.used_bytes * ncopies;
155        }
156    }
157
158    let r_total_size: u64 = devices.iter().map(|d| d.total_bytes).sum();
159    let r_total_chunks = r_data_chunks + r_meta_chunks + r_sys_chunks;
160    let r_total_used = r_data_used + r_meta_used + r_sys_used;
161    let r_total_unused = r_total_size.saturating_sub(r_total_chunks);
162
163    let r_total_missing: u64 = devices
164        .iter()
165        .filter(|d| std::fs::metadata(&d.path).is_err())
166        .map(|d| d.total_bytes)
167        .sum();
168
169    let data_ratio = if l_data_chunks > 0 {
170        r_data_chunks as f64 / l_data_chunks as f64
171    } else {
172        1.0
173    };
174    let meta_ratio = if l_meta_chunks > 0 {
175        r_meta_chunks as f64 / l_meta_chunks as f64
176    } else {
177        1.0
178    };
179    let max_data_ratio = max_ncopies as f64;
180
181    const MIN_UNALLOCATED_THRESH: u64 = 16 * 1024 * 1024;
182    let free_base = if data_ratio > 0.0 {
183        ((r_data_chunks.saturating_sub(r_data_used)) as f64 / data_ratio) as u64
184    } else {
185        0
186    };
187    let (free_estimated, free_min) = if r_total_unused >= MIN_UNALLOCATED_THRESH
188    {
189        (
190            free_base + (r_total_unused as f64 / data_ratio) as u64,
191            free_base + (r_total_unused as f64 / max_data_ratio) as u64,
192        )
193    } else {
194        (free_base, free_base)
195    };
196
197    let free_statfs = nix::sys::statfs::statfs(path)
198        .map(|st| st.blocks_available() as u64 * st.block_size() as u64)
199        .unwrap_or(0);
200
201    let multiple = has_multiple_profiles(&spaces);
202
203    println!("Overall:");
204    println!("    Device size:\t\t{:>10}", human_bytes(r_total_size));
205    println!(
206        "    Device allocated:\t\t{:>10}",
207        human_bytes(r_total_chunks)
208    );
209    println!(
210        "    Device unallocated:\t\t{:>10}",
211        human_bytes(r_total_unused)
212    );
213    println!(
214        "    Device missing:\t\t{:>10}",
215        human_bytes(r_total_missing)
216    );
217    println!("    Device slack:\t\t{:>10}", human_bytes(0));
218    println!("    Used:\t\t\t{:>10}", human_bytes(r_total_used));
219    println!(
220        "    Free (estimated):\t\t{:>10}\t(min: {})",
221        human_bytes(free_estimated),
222        human_bytes(free_min)
223    );
224    println!("    Free (statfs, df):\t\t{:>10}", human_bytes(free_statfs));
225    println!("    Data ratio:\t\t\t{:>10.2}", data_ratio);
226    println!("    Metadata ratio:\t\t{:>10.2}", meta_ratio);
227    println!(
228        "    Global reserve:\t\t{:>10}\t(used: {})",
229        human_bytes(l_global_reserve),
230        human_bytes(l_global_reserve_used)
231    );
232    println!(
233        "    Multiple profiles:\t\t{:>10}",
234        if multiple { "yes" } else { "no" }
235    );
236
237    if chunk_allocs.is_none() {
238        eprintln!(
239            "NOTE: per-device usage breakdown unavailable \
240             (chunk tree requires CAP_SYS_ADMIN)"
241        );
242    }
243
244    for s in &spaces {
245        if s.flags.contains(BlockGroupFlags::GLOBAL_RSV) {
246            continue;
247        }
248        let pct = if s.total_bytes > 0 {
249            100.0 * s.used_bytes as f64 / s.total_bytes as f64
250        } else {
251            0.0
252        };
253        println!(
254            "\n{},{}: Size:{}, Used:{} ({:.2}%)",
255            s.flags.type_name(),
256            s.flags.profile_name(),
257            human_bytes(s.total_bytes),
258            human_bytes(s.used_bytes),
259            pct
260        );
261
262        // Per-device lines: one row per device that holds stripes for this
263        // exact profile.  Sorted by devid for stable output.
264        if let Some(allocs) = &chunk_allocs {
265            let mut profile_allocs: Vec<_> =
266                allocs.iter().filter(|a| a.flags == s.flags).collect();
267            profile_allocs.sort_by_key(|a| a.devid);
268
269            for alloc in profile_allocs {
270                let path = devid_to_path
271                    .get(&alloc.devid)
272                    .copied()
273                    .unwrap_or("<unknown>");
274                println!("   {}\t\t{:>10}", path, human_bytes(alloc.bytes));
275            }
276        }
277    }
278
279    println!("\nUnallocated:");
280    for dev in &devices {
281        let unallocated = dev.total_bytes.saturating_sub(dev.bytes_used);
282        println!("   {}\t{:>10}", dev.path, human_bytes(unallocated));
283    }
284
285    Ok(())
286}