1use std::convert::Infallible;
4use std::num::NonZeroUsize;
5
6use crate::objects::{HashAlgo, ObjectId, ObjectKind};
7use crate::parallel::{
8 parallel_worthwhile as parallel_worthwhile_opts, try_par_map, try_par_map_force,
9 ParallelOptions,
10};
11
12pub use crate::parallel::index_parallelism_from_config;
13pub use crate::parallel::{ParallelHashError, Parallelism};
14
15use super::hash_object;
16use crate::error::Error;
17
18pub const PAR_HASH_MIN_ITEMS: usize = 32;
20pub const PAR_HASH_MIN_TOTAL_BYTES: usize = 256 * 1024;
22
23#[must_use]
25pub fn parallel_hash_worthwhile(item_count: usize, total_bytes: usize, threads: usize) -> bool {
26 if threads <= 1 {
27 return false;
28 }
29 parallel_worthwhile_opts(
30 item_count,
31 total_bytes,
32 NonZeroUsize::new(threads).unwrap_or(NonZeroUsize::MIN),
33 PAR_HASH_MIN_ITEMS,
34 PAR_HASH_MIN_TOTAL_BYTES,
35 )
36}
37
38fn hash_parallel_options(threads: NonZeroUsize) -> ParallelOptions {
39 ParallelOptions {
40 threads,
41 min_items: PAR_HASH_MIN_ITEMS,
42 min_total_bytes: Some(PAR_HASH_MIN_TOTAL_BYTES),
43 signed_entry_threshold: None,
44 }
45}
46
47pub fn hash_objects_parallel(
54 algo: HashAlgo,
55 items: &[(ObjectKind, &[u8])],
56 threads: NonZeroUsize,
57) -> Result<Vec<ObjectId>, ParallelHashError<Error>> {
58 let total_bytes: usize = items.iter().map(|(_, data)| data.len()).sum();
59 try_par_hash_with(items, threads, total_bytes, |(kind, data)| {
60 hash_object(algo, *kind, data)
61 })
62}
63
64#[must_use]
66pub fn par_hash_with<T, R, F>(
67 items: &[T],
68 threads: NonZeroUsize,
69 total_bytes: usize,
70 f: F,
71) -> Vec<R>
72where
73 T: Sync,
74 R: Send,
75 F: Fn(&T) -> R + Sync,
76{
77 try_par_hash_with::<T, R, Infallible, _>(items, threads, total_bytes, |item| Ok(f(item)))
78 .unwrap_or_else(|e| match e {
79 ParallelHashError::Task(infallible) => match infallible {},
80 })
81}
82
83pub fn try_par_hash_with<T, R, E, F>(
85 items: &[T],
86 threads: NonZeroUsize,
87 total_bytes: usize,
88 f: F,
89) -> Result<Vec<R>, ParallelHashError<E>>
90where
91 T: Sync,
92 R: Send,
93 E: Send + 'static,
94 F: Fn(&T) -> Result<R, E> + Sync,
95{
96 try_par_map(items, hash_parallel_options(threads), total_bytes, f)
97}
98
99pub fn try_par_hash_with_force<T, R, E, F>(
101 items: &[T],
102 threads: NonZeroUsize,
103 f: F,
104) -> Result<Vec<R>, ParallelHashError<E>>
105where
106 T: Sync,
107 R: Send,
108 E: Send + 'static,
109 F: Fn(&T) -> Result<R, E> + Sync,
110{
111 try_par_map_force(items, hash_parallel_options(threads), f)
112}
113
114#[cfg(test)]
115mod tests {
116 use super::*;
117 use crate::objects::{HashAlgo, ObjectKind};
118 use std::panic::{catch_unwind, AssertUnwindSafe};
119 use std::sync::atomic::{AtomicUsize, Ordering as AtomicOrdering};
120 use std::sync::Arc;
121
122 fn threads(n: usize) -> NonZeroUsize {
123 NonZeroUsize::new(n).expect("non-zero threads")
124 }
125
126 fn serial_ids(algo: HashAlgo, items: &[(ObjectKind, &[u8])]) -> Vec<ObjectId> {
127 items
128 .iter()
129 .map(|(kind, data)| hash_object(algo, *kind, data).expect("serial hash"))
130 .collect()
131 }
132
133 struct DropCounter {
134 dropped: Arc<AtomicUsize>,
135 }
136
137 impl Drop for DropCounter {
138 fn drop(&mut self) {
139 self.dropped.fetch_add(1, AtomicOrdering::SeqCst);
140 }
141 }
142
143 #[test]
144 fn parallel_matches_serial_counts_and_threads() {
145 let large = if cfg!(miri) { 64 } else { 10_000 };
147 let sizes = [0_usize, 1, 7, large];
148 let thread_counts = [1_usize, 2, 8];
149 let blob = vec![0xAB_u8; 512];
150 for &n in &sizes {
151 let items: Vec<(ObjectKind, &[u8])> = (0..n)
152 .map(|_| (ObjectKind::Blob, blob.as_slice()))
153 .collect();
154 let expect = serial_ids(HashAlgo::Sha1, &items);
155 for &t in &thread_counts {
156 let got =
157 hash_objects_parallel(HashAlgo::Sha1, &items, threads(t)).expect("parallel");
158 assert_eq!(got, expect, "n={n} threads={t}");
159 }
160 }
161 }
162
163 #[test]
164 fn parallel_sha256_matches_serial() {
165 let blobs: Vec<[u8; 64]> = (0..500).map(|i| [i as u8; 64]).collect();
166 let items: Vec<(ObjectKind, &[u8])> = blobs
167 .iter()
168 .map(|b| (ObjectKind::Blob, b.as_slice()))
169 .collect();
170 let expect = serial_ids(HashAlgo::Sha256, &items);
171 let got = hash_objects_parallel(HashAlgo::Sha256, &items, threads(4)).expect("parallel");
172 assert_eq!(got, expect);
173 }
174
175 #[test]
176 fn par_hash_with_preserves_order() {
177 let items: Vec<usize> = (0..256).collect();
178 let got = par_hash_with(&items, threads(4), items.len(), |&x| x * 2);
179 let expect: Vec<_> = items.iter().map(|x| x * 2).collect();
180 assert_eq!(got, expect);
181 }
182
183 #[test]
184 fn try_par_hash_surfaces_task_error_without_deadlock() {
185 let items: Vec<i32> = (0..128).collect();
186 let err = try_par_hash_with(&items, threads(4), items.len(), |&x| {
187 if x == 64 {
188 Err("boom")
189 } else {
190 Ok(x)
191 }
192 })
193 .unwrap_err();
194 assert_eq!(err, ParallelHashError::Task("boom"));
195 }
196
197 #[test]
198 fn try_par_hash_drops_partial_results_on_task_error() {
199 let items: Vec<i32> = (0..128).collect();
200 let made = Arc::new(AtomicUsize::new(0));
201 let dropped = Arc::new(AtomicUsize::new(0));
202 let err = try_par_hash_with::<_, _, (), _>(&items, threads(8), items.len(), |&x| {
203 if x == 127 {
204 while made.load(AtomicOrdering::Acquire) < 127 {
205 std::hint::spin_loop();
206 }
207 return Err(());
208 }
209 made.fetch_add(1, AtomicOrdering::SeqCst);
210 Ok(DropCounter {
211 dropped: Arc::clone(&dropped),
212 })
213 });
214 assert!(matches!(err, Err(ParallelHashError::Task(()))));
215 assert_eq!(made.load(AtomicOrdering::SeqCst), 127);
216 assert_eq!(dropped.load(AtomicOrdering::SeqCst), 127);
217 }
218
219 #[test]
220 fn try_par_hash_concurrent_errors_are_raced_without_ub() {
221 let items: Vec<i32> = (0..512).collect();
222 let err = try_par_hash_with(&items, threads(8), items.len(), |&x| {
223 if x % 7 == 0 {
224 Err(x)
225 } else {
226 Ok(x)
227 }
228 })
229 .unwrap_err();
230 match err {
231 ParallelHashError::Task(code) => assert_eq!(code % 7, 0),
232 }
233 }
234
235 #[test]
236 fn par_hash_panic_propagates_and_drops_partials() {
237 let items: Vec<i32> = (0..128).collect();
238 let made = Arc::new(AtomicUsize::new(0));
239 let dropped = Arc::new(AtomicUsize::new(0));
240 let result = catch_unwind(AssertUnwindSafe(|| {
241 let _ = try_par_hash_with::<_, _, std::convert::Infallible, _>(
242 &items,
243 threads(8),
244 items.len(),
245 |&x| {
246 if x == 127 {
247 while made.load(AtomicOrdering::Acquire) < 127 {
248 std::hint::spin_loop();
249 }
250 panic!("fail");
251 }
252 made.fetch_add(1, AtomicOrdering::SeqCst);
253 Ok(DropCounter {
254 dropped: Arc::clone(&dropped),
255 })
256 },
257 );
258 }));
259 assert!(result.is_err());
260 assert_eq!(made.load(AtomicOrdering::SeqCst), 127);
261 assert_eq!(dropped.load(AtomicOrdering::SeqCst), 127);
262 }
263
264 #[test]
265 fn parallelism_resolve_clamps_to_available() {
266 let avail = std::thread::available_parallelism()
267 .map(NonZeroUsize::get)
268 .unwrap_or(1);
269 let p = Parallelism::resolve(Some(usize::MAX));
270 assert_eq!(p.threads().get(), avail);
271 let auto = Parallelism::resolve(None);
272 assert_eq!(auto.threads().get(), avail);
273 let zero = Parallelism::resolve(Some(0));
274 assert_eq!(zero.threads().get(), avail);
275 }
276
277 #[test]
278 fn serial_fallback_below_threshold() {
279 let items: Vec<i32> = (0..4).collect();
280 let counter = Arc::new(AtomicUsize::new(0));
281 let c = Arc::clone(&counter);
282 let _ = par_hash_with(&items, threads(8), 0, move |&x| {
283 c.fetch_add(1, AtomicOrdering::Relaxed);
284 x
285 });
286 assert_eq!(counter.load(AtomicOrdering::Relaxed), 4);
287 }
288}