1use std::sync::atomic::{AtomicUsize, Ordering};
2
3use anyhow::{Result, bail};
4
5const DEFAULT_THREADS_PER_CPU: usize = 4;
6const DEFAULT_MAX_THREADS: usize = 16;
8const ENV_MAX_THREADS: usize = 32;
10
11static NUM_THREADS: AtomicUsize = AtomicUsize::new(0);
13
14pub fn set_num_threads(n: usize) {
17 NUM_THREADS.store(n, Ordering::Relaxed);
18}
19
20pub(crate) fn get_num_threads() -> Option<usize> {
22 match NUM_THREADS.load(Ordering::Relaxed) {
23 0 => None,
24 n => Some(n),
25 }
26}
27
28#[derive(Debug, Clone, Copy)]
30enum ThreadSource {
31 Override,
32 Environment,
33 Default,
34}
35
36impl std::fmt::Display for ThreadSource {
37 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
38 f.write_str(match self {
39 Self::Override => "override",
40 Self::Environment => "environment",
41 Self::Default => "default",
42 })
43 }
44}
45
46const KB: u64 = 1_024;
47const MB: u64 = 1_024 * 1_024;
48const GB: u64 = 1_024 * 1_024 * 1_024;
49const TB: u64 = 1_024 * 1_024 * 1_024 * 1_024;
50
51pub fn format_size(bytes: u64) -> String {
54 if bytes >= TB {
55 format!("{:.1} TB", bytes as f64 / TB as f64)
56 } else if bytes >= GB {
57 format!("{:.1} GB", bytes as f64 / GB as f64)
58 } else if bytes >= MB {
59 format!("{:.1} MB", bytes as f64 / MB as f64)
60 } else if bytes >= KB {
61 format!("{:.1} KB", bytes as f64 / KB as f64)
62 } else {
63 format!("{bytes} B")
64 }
65}
66
67pub fn parse_size(size: &str) -> Result<u64> {
69 let s = size.trim();
70
71 if s == "0" {
72 return Ok(0);
73 }
74
75 let num_end = s
76 .find(|c: char| !c.is_ascii_digit() && c != '.')
77 .unwrap_or(s.len());
78 let (num_str, unit_str) = s.split_at(num_end);
79 let num_str = num_str.trim();
80 let unit_str = unit_str.trim().to_ascii_uppercase();
81
82 if num_str.is_empty() {
83 bail!("Invalid size string: {s:?} (no number found)");
84 }
85
86 let num: f64 = num_str
87 .parse()
88 .map_err(|_| anyhow::anyhow!("Invalid number in size string: {num_str:?}"))?;
89
90 let multiplier = match unit_str.as_str() {
91 "" | "B" => 1u64,
92 "K" | "KB" | "KIB" => KB,
93 "M" | "MB" | "MIB" => MB,
94 "G" | "GB" | "GIB" => GB,
95 "T" | "TB" | "TIB" => TB,
96 _ => bail!("Unknown size unit: {unit_str:?}"),
97 };
98
99 Ok((num * multiplier as f64) as u64)
100}
101
102pub fn get_threadpool(work_items: usize) -> Result<rayon::ThreadPool> {
111 debug_assert_ne!(
112 work_items, 0,
113 "the thread pool should not be instantiated when there are no work items to process"
114 );
115 let work_limit = work_items.max(1);
116 let available_cpus = std::thread::available_parallelism()
117 .map(|n| n.get())
118 .unwrap_or(1)
119 .max(1);
120
121 let override_threads = get_num_threads();
122 let environment_threads = std::env::var("DVS_NUM_THREADS")
123 .ok()
124 .and_then(|v| v.parse::<usize>().ok())
125 .filter(|&n| n > 0);
126
127 let num_threads = match (override_threads, environment_threads) {
128 (Some(n), _) | (None, Some(n)) => n.min(ENV_MAX_THREADS).min(work_limit),
129 (None, None) => available_cpus
130 .saturating_mul(DEFAULT_THREADS_PER_CPU)
131 .min(DEFAULT_MAX_THREADS)
132 .min(work_limit),
133 };
134
135 let source = if override_threads.is_some() {
136 ThreadSource::Override
137 } else if environment_threads.is_some() {
138 ThreadSource::Environment
139 } else {
140 ThreadSource::Default
141 };
142
143 log::debug!("thread pool: {num_threads} threads (source: {source}, work_items={work_items})",);
144
145 let pool = rayon::ThreadPoolBuilder::new()
146 .num_threads(num_threads)
147 .build()?;
148 Ok(pool)
149}
150
151#[cfg(test)]
152mod tests {
153 use super::*;
154 #[test]
155 fn parse_size_basic() {
156 assert_eq!(parse_size("0").unwrap(), 0);
157 assert_eq!(parse_size("500MB").unwrap(), 500 * MB);
158 assert_eq!(parse_size("500 MB").unwrap(), 500 * MB);
159 assert_eq!(parse_size("1GB").unwrap(), GB);
160 assert_eq!(parse_size("1 gb").unwrap(), GB);
161 assert_eq!(parse_size("2TB").unwrap(), 2 * TB);
162 assert_eq!(parse_size("100KB").unwrap(), 100 * KB);
163 assert_eq!(parse_size("1024B").unwrap(), 1024);
164 assert_eq!(parse_size("1.5GB").unwrap(), (1.5 * GB as f64) as u64);
165 assert!(parse_size("").is_err());
166 assert!(parse_size("MB").is_err());
167 assert!(parse_size("500XB").is_err());
168 }
169}