History

feat!: index-backed tree traversal with a form of work-stealing. (#851)

When delta-trees are unbalanced, in pathological cases it's possible that that one thread ends up with more than half of the work. In this case it's required that it manages to spawn its own threads to parallelize the work it has.

Sebastian Thiel committed 3 years ago

Commit d22dd8f1 parent add5ea811 files · +441 −202Browse files

Unified · Split

11 files
  1. gix-pack/Cargo.toml +1 −1
  2. gix-pack/src/bundle/write/mod.rs +13 −16
  3. gix-pack/src/cache/delta/traverse/mod.rs +34 −26
  4. gix-pack/src/cache/delta/traverse/resolve.rs +314 −51
  5. gix-pack/src/cache/delta/traverse/util.rs +7 −5
  6. gix-pack/src/index/traverse/mod.rs +18 −22
  7. gix-pack/src/index/traverse/with_index.rs +16 −20
  8. gix-pack/src/index/traverse/with_lookup.rs +8 −12
  9. gix-pack/src/index/verify.rs +5 −8
  10. gix-pack/src/index/write/mod.rs +9 −14
  11. gix-pack/tests/pack/index.rs +16 −27
gix-pack/Cargo.toml+1−1
@@ -5,7 +5,7 @@ repository = "https://github.com/Byron/gitoxide" · 4 unmodified lines
55 authors = ["Sebastian Thiel <sebastian.thiel@icloud.com>"]
66 license = "MIT/Apache-2.0"
77 description = "Implements git packs and related data structures"
8−edition = "2018"
8+edition = "2021"
99 include = ["src/**/*", "CHANGELOG.md"]
1010 rust-version = "1.64"
1111 autotests = false
gix-pack/src/bundle/write/mod.rs+13−16
@@ -62,17 +62,14 @@ impl crate::Bundle { · 61 unmodified lines
6262 /// * the resulting pack may be empty, that is, contains zero objects in some situations. This is a valid reply by a server and should
6363 /// be accounted for.
6464 /// - Empty packs always have the same name and not handling this case will result in at most one superfluous pack.
65− pub fn write_to_directory<P>(
65+ pub fn write_to_directory(
6666 pack: impl io::BufRead,
6767 directory: Option<impl AsRef<Path>>,
68− mut progress: P,
68+ mut progress: impl Progress,
6969 should_interrupt: &AtomicBool,
7070 thin_pack_base_object_lookup_fn: Option<ThinPackLookupFn>,
7171 options: Options,
72− ) -> Result<Outcome, Error>
73− where
74− P: Progress,
75− {
72+ ) -> Result<Outcome, Error> {
7673 let mut read_progress = progress.add_child_with_id("read pack", ProgressId::ReadPackBytes.into());
7774 read_progress.init(None, progress::bytes());
7875 let pack = progress::Read {
@@ -354,20 +351,20 @@ impl crate::Bundle { · 275 unmodified lines
354351 }
355352 }
356353
354+fn resolve_entry(range: data::EntryRange, mapped_file: &memmap2::Mmap) -> Option<&[u8]> {
355+ mapped_file.get(range.start as usize..range.end as usize)
356+}
357+
357358 fn new_pack_file_resolver(
358359 data_file: SharedTempFile,
359−) -> io::Result<impl Fn(data::EntryRange, &mut Vec<u8>) -> Option<()> + Send + Clone> {
360+) -> io::Result<(
361+ impl Fn(data::EntryRange, &memmap2::Mmap) -> Option<&[u8]> + Send + Clone,
362+ memmap2::Mmap,
363+)> {
360364 let mut guard = data_file.lock();
361365 guard.flush()?;
362− let mapped_file = Arc::new(crate::mmap::read_only(
363− &guard.get_mut().with_mut(|f| f.path().to_owned())?,
364− )?);
365− let pack_data_lookup = move |range: std::ops::Range<u64>, out: &mut Vec<u8>| -> Option<()> {
366− mapped_file
367− .get(range.start as usize..range.end as usize)
368− .map(|pack_entry| out.copy_from_slice(pack_entry))
369− };
370− Ok(pack_data_lookup)
366+ let mapped_file = crate::mmap::read_only(&guard.get_mut().with_mut(|f| f.path().to_owned())?)?;
367+ Ok((resolve_entry, mapped_file))
371368 }
372369
373370 struct WriteOutcome {
gix-pack/src/cache/delta/traverse/mod.rs+34−26
@@ -1,9 +1,10 @@
11 use std::sync::atomic::{AtomicBool, Ordering};
22
3+use gix_features::threading::{Mutable, OwnShared};
34 use gix_features::{
45 parallel::in_parallel_with_slice,
56 progress::{self, Progress},
6− threading::{lock, Mutable, OwnShared},
7+ threading,
78 };
89
910 use crate::{
@@ -36,18 +37,18 @@ pub enum Error { · 26 unmodified lines
3637 /// The base's offset which was from a resolved ref-delta that didn't actually get added to the tree
3738 base_pack_offset: crate::data::Offset,
3839 },
40+ #[error("Failed to spawn thread when switching to work-stealing mode")]
41+ SpawnThread(#[from] std::io::Error),
3942 }
4043
4144 /// Additional context passed to the `inspect_object(…)` function of the [`Tree::traverse()`] method.
42−pub struct Context<'a, S> {
45+pub struct Context<'a> {
4346 /// The pack entry describing the object
4447 pub entry: &'a crate::data::Entry,
4548 /// The offset at which `entry` ends in the pack, useful to learn about the exact range of `entry` within the pack.
4649 pub entry_end: u64,
4750 /// The decompressed object itself, ready to be decoded.
4851 pub decompressed: &'a [u8],
49− /// Custom state known to the function
50− pub state: &'a mut S,
5152 /// The depth at which this object resides in the delta-tree. It represents the amount of base objects, with 0 indicating
5253 /// an 'undeltified' object, and higher values indicating delta objects with the given amount of bases.
5354 pub level: u16,
@@ -89,79 +90,86 @@ where · 35 unmodified lines
8990 /// operation as well.
9091 /// * `pack_entries_end` marks one-past-the-last byte of the last entry in the pack, as the last entries size would otherwise
9192 /// be unknown as it's not part of the index file.
92− /// * `new_thread_state() -> State` is a function to create state to be used in each thread, invoked once per thread.
9393 /// * `inspect_object(node_data: &mut T, progress: Progress, context: Context<ThreadLocal State>) -> Result<(), CustomError>` is a function
9494 /// running for each thread receiving fully decoded objects along with contextual information, which either succeeds with `Ok(())`
9595 /// or returns a `CustomError`.
96− /// Note that `node_data` can be modified to allow storing maintaining computation results on a per-object basis.
96+ /// Note that `node_data` can be modified to allow storing maintaining computation results on a per-object basis. It should contain
97+ /// its own mutable per-thread data as required.
9798 ///
9899 /// This method returns a vector of all tree items, along with their potentially modified custom node data.
99100 ///
100101 /// _Note_ that this method consumed the Tree to assure safe parallel traversal with mutation support.
101− pub fn traverse<F, P1, P2, MBFN, S, E>(
102+ pub fn traverse<F, P1, P2, MBFN, E, R>(
102103 mut self,
103104 resolve: F,
105+ resolve_data: &R,
104106 pack_entries_end: u64,
105− new_thread_state: impl Fn() -> S + Send + Clone,
106107 inspect_object: MBFN,
107108 Options {
108109 thread_limit,
109− object_progress,
110+ mut object_progress,
110111 mut size_progress,
111112 should_interrupt,
112113 object_hash,
113114 }: Options<'_, P1, P2>,
114115 ) -> Result<Outcome<T>, Error>
115116 where
116− F: for<'r> Fn(EntryRange, &'r mut Vec<u8>) -> Option<()> + Send + Clone,
117+ F: for<'r> FnMut(EntryRange, &'r R) -> Option<&'r [u8]> + Send + Clone,
118+ R: Send + Sync,
117119 P1: Progress,
118120 P2: Progress,
119− MBFN: Fn(&mut T, &mut <P1 as Progress>::SubProgress, Context<'_, S>) -> Result<(), E> + Send + Clone,
121+ MBFN: FnMut(&mut T, &<P1 as Progress>::SubProgress, Context<'_>) -> Result<(), E> + Send + Clone,
120122 E: std::error::Error + Send + Sync + 'static,
121123 {
122124 self.set_pack_entries_end_and_resolve_ref_offsets(pack_entries_end)?;
123− let object_progress = OwnShared::new(Mutable::new(object_progress));
124125
125126 let num_objects = self.num_items();
126127 let object_counter = {
127− let mut progress = lock(&object_progress);
128+ let progress = &mut object_progress;
128129 progress.init(Some(num_objects), progress::count("objects"));
129130 progress.counter()
130131 };
131132 size_progress.init(None, progress::bytes());
132133 let size_counter = size_progress.counter();
133134 let child_items = self.child_items.as_mut_slice();
135+ let object_progress = OwnShared::new(Mutable::new(object_progress));
134136
135137 let start = std::time::Instant::now();
136138 in_parallel_with_slice(
137139 &mut self.root_items,
138140 thread_limit,
139141 {
140− let object_progress = object_progress.clone();
141142 let child_items = ItemSliceSend(std::ptr::slice_from_raw_parts_mut(
142143 child_items.as_mut_ptr(),
143144 child_items.len(),
144145 ));
145− move |thread_index| {
146− (
147− Vec::<u8>::with_capacity(4096),
148− lock(&object_progress)
149− .add_child_with_id(format!("thread {thread_index}"), gix_features::progress::UNKNOWN),
150− new_thread_state(),
151− resolve.clone(),
152− inspect_object.clone(),
153− child_items.clone(),
154− )
146+ {
147+ let object_progress = object_progress.clone();
148+ move |thread_index| {
149+ let _ = &child_items;
150+ resolve::State {
151+ bytes_buf: Vec::<u8>::with_capacity(4096),
152+ delta_bytes: Vec::<u8>::with_capacity(4096),
153+ fully_resolved_delta_bytes: Vec::<u8>::with_capacity(4096),
154+ progress: threading::lock(&object_progress)
155+ .add_child(format!("gix-pack.delta-traverse.inspect-object.{thread_index}")),
156+ resolve: resolve.clone(),
157+ modify_base: inspect_object.clone(),
158+ child_items: child_items.clone(),
159+ }
160+ }
155161 }
156162 },
157163 {
158− move |node, state, _threads_left, should_interrupt| {
164+ move |node, state, threads_left, should_interrupt| {
159165 resolve::deltas(
160166 object_counter.clone(),
161167 size_counter.clone(),
162168 node,
163169 state,
170+ resolve_data,
164171 object_hash.len_in_bytes(),
172+ threads_left,
165173 should_interrupt,
166174 )
167175 }
@@ -170,7 +178,7 @@ where · 2 unmodified lines
170178 |_| (),
171179 )?;
172180
173− lock(&object_progress).show_throughput(start);
181+ threading::lock(&object_progress).show_throughput(start);
174182 size_progress.show_throughput(start);
175183
176184 Ok(Outcome {
gix-pack/src/cache/delta/traverse/resolve.rs+314−51
@@ -1,7 +1,8 @@
1−use std::sync::atomic::AtomicBool;
2−use std::{cell::RefCell, collections::BTreeMap, sync::atomic::Ordering};
1+use std::sync::atomic::{AtomicBool, AtomicIsize};
2+use std::{collections::BTreeMap, sync::atomic::Ordering};
33
4−use gix_features::{progress::Progress, zlib};
4+use gix_features::progress::Progress;
5+use gix_features::{threading, zlib};
56
67 use crate::{
78 cache::delta::{
@@ -11,49 +12,59 @@ use crate::{ · 3 unmodified lines
1112 },
1213 Item,
1314 },
15+ data,
1416 data::EntryRange,
1517 };
1618
19+pub(crate) struct State<P, F, MBFN, T: Send> {
20+ pub bytes_buf: Vec<u8>,
21+ pub delta_bytes: Vec<u8>,
22+ pub fully_resolved_delta_bytes: Vec<u8>,
23+ pub progress: P,
24+ pub resolve: F,
25+ pub modify_base: MBFN,
26+ pub child_items: ItemSliceSend<Item<T>>,
27+}
28+
1729 #[allow(clippy::too_many_arguments)]
18−pub(crate) fn deltas<T, F, P, MBFN, S, E>(
30+pub(crate) fn deltas<T, F, MBFN, E, R, P>(
1931 object_counter: Option<gix_features::progress::StepShared>,
2032 size_counter: Option<gix_features::progress::StepShared>,
2133 node: &mut Item<T>,
22− (bytes_buf, ref mut progress, state, resolve, modify_base, child_items): &mut (
23− Vec<u8>,
24− P,
25− S,
26− F,
27− MBFN,
28− ItemSliceSend<Item<T>>,
29− ),
34+ State {
35+ bytes_buf,
36+ delta_bytes,
37+ fully_resolved_delta_bytes,
38+ progress,
39+ resolve,
40+ modify_base,
41+ child_items,
42+ }: &mut State<P, F, MBFN, T>,
43+ resolve_data: &R,
3044 hash_len: usize,
45+ threads_left: &AtomicIsize,
3146 should_interrupt: &AtomicBool,
3247 ) -> Result<(), Error>
3348 where
3449 T: Send,
35− F: for<'r> Fn(EntryRange, &'r mut Vec<u8>) -> Option<()>,
50+ R: Send + Sync,
3651 P: Progress,
37− MBFN: Fn(&mut T, &mut P, Context<'_, S>) -> Result<(), E>,
52+ F: for<'r> FnMut(EntryRange, &'r R) -> Option<&'r [u8]> + Send + Clone,
53+ MBFN: FnMut(&mut T, &P, Context<'_>) -> Result<(), E> + Send + Clone,
3854 E: std::error::Error + Send + Sync + 'static,
3955 {
4056 let mut decompressed_bytes_by_pack_offset = BTreeMap::new();
41− let bytes_buf = RefCell::new(bytes_buf);
42− let decompress_from_resolver = |slice: EntryRange| -> Result<(crate::data::Entry, u64, Vec<u8>), Error> {
43− let mut bytes_buf = bytes_buf.borrow_mut();
44− bytes_buf.resize((slice.end - slice.start) as usize, 0);
45− resolve(slice.clone(), &mut bytes_buf).ok_or(Error::ResolveFailed {
57+ let mut decompress_from_resolver = |slice: EntryRange, out: &mut Vec<u8>| -> Result<(data::Entry, u64), Error> {
58+ let bytes = resolve(slice.clone(), resolve_data).ok_or(Error::ResolveFailed {
4659 pack_offset: slice.start,
4760 })?;
48− let entry = crate::data::Entry::from_bytes(&bytes_buf, slice.start, hash_len);
49− let compressed = &bytes_buf[entry.header_size()..];
61+ let entry = data::Entry::from_bytes(bytes, slice.start, hash_len);
62+ let compressed = &bytes[entry.header_size()..];
5063 let decompressed_len = entry.decompressed_size as usize;
51− Ok((entry, slice.end, decompress_all_at_once(compressed, decompressed_len)?))
64+ decompress_all_at_once(compressed, decompressed_len, out)?;
65+ Ok((entry, slice.end))
5266 };
5367
54− // Traverse the tree breadth first and loose the data produced for the base as it won't be needed anymore.
55− progress.init(None, gix_features::progress::count_with_decimals("objects", 2));
56−
5768 // each node is a base, and its children always start out as deltas which become a base after applying them.
5869 // These will be pushed onto our stack until all are processed
5970 let root_level = 0;
@@ -61,19 +72,26 @@ where · 1 unmodified line
6172 root_level,
6273 Node {
6374 item: node,
64− child_items: child_items.0,
75+ child_items: child_items.clone(),
6576 },
6677 )];
6778 while let Some((level, mut base)) = nodes.pop() {
79+ let storage;
6880 if should_interrupt.load(Ordering::Relaxed) {
6981 return Err(Error::Interrupted);
7082 }
7183 let (base_entry, entry_end, base_bytes) = if level == root_level {
72− decompress_from_resolver(base.entry_slice())?
84+ let (a, b) = decompress_from_resolver(base.entry_slice(), bytes_buf)?;
85+ (a, b, &*bytes_buf)
7386 } else {
74− decompressed_bytes_by_pack_offset
87+ if !bytes_buf.is_empty() {
88+ *bytes_buf = Vec::new();
89+ }
90+ let (a, b, v) = decompressed_bytes_by_pack_offset
7591 .remove(&base.offset())
76− .expect("we store the resolved delta buffer when done")
92+ .expect("we store the resolved delta buffer when done");
93+ storage = v;
94+ (a, b, &storage)
7795 };
7896
7997 // anything done here must be repeated further down for leaf-nodes.
@@ -86,8 +104,7 @@ where · 6 unmodified lines
86104 Context {
87105 entry: &base_entry,
88106 entry_end,
89− decompressed: &base_bytes,
90− state,
107+ decompressed: base_bytes,
91108 level,
92109 },
93110 )
@@ -99,20 +116,19 @@ where · 5 unmodified lines
99116 }
100117
101118 for mut child in base.into_child_iter() {
102− let (mut child_entry, entry_end, delta_bytes) = decompress_from_resolver(child.entry_slice())?;
103− let (base_size, consumed) = crate::data::delta::decode_header_size(&delta_bytes);
119+ let (mut child_entry, entry_end) = decompress_from_resolver(child.entry_slice(), delta_bytes)?;
120+ let (base_size, consumed) = data::delta::decode_header_size(delta_bytes);
104121 let mut header_ofs = consumed;
105122 assert_eq!(
106123 base_bytes.len(),
107124 base_size as usize,
108− "recorded base size in delta does not match"
125+ "recorded base size in delta does match the actual one"
109126 );
110− let (result_size, consumed) = crate::data::delta::decode_header_size(&delta_bytes[consumed..]);
127+ let (result_size, consumed) = data::delta::decode_header_size(&delta_bytes[consumed..]);
111128 header_ofs += consumed;
112129
113− let mut fully_resolved_delta_bytes = bytes_buf.borrow_mut();
114− fully_resolved_delta_bytes.resize(result_size as usize, 0);
115− crate::data::delta::apply(&base_bytes, &mut fully_resolved_delta_bytes, &delta_bytes[header_ofs..]);
130+ resize_vec(fully_resolved_delta_bytes, result_size as usize);
131+ data::delta::apply(base_bytes, fully_resolved_delta_bytes, &delta_bytes[header_ofs..]);
116132
117133 // FIXME: this actually invalidates the "pack_offset()" computation, which is not obvious to consumers
118134 // at all
@@ -120,7 +136,7 @@ where · 1 unmodified line
120136 if child.has_children() {
121137 decompressed_bytes_by_pack_offset.insert(
122138 child.offset(),
123− (child_entry, entry_end, fully_resolved_delta_bytes.to_owned()),
139+ (child_entry, entry_end, std::mem::take(fully_resolved_delta_bytes)),
124140 );
125141 nodes.push((level + 1, child));
126142 } else {
@@ -130,8 +146,7 @@ where · 3 unmodified lines
130146 Context {
131147 entry: &child_entry,
132148 entry_end,
133− decompressed: &fully_resolved_delta_bytes,
134− state,
149+ decompressed: fully_resolved_delta_bytes,
135150 level: level + 1,
136151 },
137152 )
@@ -142,25 +157,273 @@ where · 4 unmodified lines
142157 .map(|c| c.fetch_add(base_bytes.len(), Ordering::SeqCst));
143158 }
144159 }
160+
161+ // After the first round, see if we can use additional threads, and if so we enter multi-threaded mode.
162+ // In it we will keep using new threads as they become available while using this thread for coordination.
163+ // We optimize for a low memory footprint as we are likely to get here if long delta-chains with large objects are involved.
164+ // Try to avoid going into threaded mode if there isn't more than one unit of work anyway.
165+ if nodes.len() > 1 {
166+ if let Ok(initial_threads) =
167+ threads_left.fetch_update(Ordering::SeqCst, Ordering::SeqCst, |threads_available| {
168+ (threads_available > 0).then_some(0)
169+ })
170+ {
171+ // Assure no memory is held here.
172+ *bytes_buf = Vec::new();
173+ *delta_bytes = Vec::new();
174+ *fully_resolved_delta_bytes = Vec::new();
175+ return deltas_mt(
176+ initial_threads,
177+ decompressed_bytes_by_pack_offset,
178+ object_counter,
179+ size_counter,
180+ progress,
181+ nodes,
182+ resolve.clone(),
183+ resolve_data,
184+ modify_base.clone(),
185+ hash_len,
186+ threads_left,
187+ should_interrupt,
188+ );
189+ }
190+ }
145191 }
146192
147193 Ok(())
148194 }
149195
150−fn decompress_all_at_once(b: &[u8], decompressed_len: usize) -> Result<Vec<u8>, Error> {
151− let mut out = Vec::with_capacity(decompressed_len);
152− // SAFETY:
153− // 1. we have reserved `decompressed_len`
154− // 2. zlib is going to write all of `decompressed_len`. On error, none of the the buffer is made available to the user.
155− #[allow(unsafe_code, clippy::uninit_vec)]
156− unsafe {
157− out.set_len(decompressed_len);
196+/// * `initial_threads` is the threads we may spawn, not accounting for our own thread which is still considered used by the parent
197+/// system. Since this thread will take a controlling function, we may spawn one more than that. In threaded mode, we will finish
198+/// all remaining work.
199+#[allow(clippy::too_many_arguments)]
200+pub(crate) fn deltas_mt<T, F, MBFN, E, R, P>(
201+ mut threads_to_create: isize,
202+ decompressed_bytes_by_pack_offset: BTreeMap<u64, (data::Entry, u64, Vec<u8>)>,
203+ object_counter: Option<gix_features::progress::StepShared>,
204+ size_counter: Option<gix_features::progress::StepShared>,
205+ progress: &P,
206+ nodes: Vec<(u16, Node<'_, T>)>,
207+ resolve: F,
208+ resolve_data: &R,
209+ modify_base: MBFN,
210+ hash_len: usize,
211+ threads_left: &AtomicIsize,
212+ should_interrupt: &AtomicBool,
213+) -> Result<(), Error>
214+where
215+ T: Send,
216+ R: Send + Sync,
217+ P: Progress,
218+ F: for<'r> FnMut(EntryRange, &'r R) -> Option<&'r [u8]> + Send + Clone,
219+ MBFN: FnMut(&mut T, &P, Context<'_>) -> Result<(), E> + Send + Clone,
220+ E: std::error::Error + Send + Sync + 'static,
221+{
222+ let nodes = gix_features::threading::Mutable::new(nodes);
223+ let decompressed_bytes_by_pack_offset = gix_features::threading::Mutable::new(decompressed_bytes_by_pack_offset);
224+ threads_to_create += 1; // ourselves
225+ let mut returned_ourselves = false;
226+
227+ gix_features::parallel::threads(|s| -> Result<(), Error> {
228+ let mut threads = Vec::new();
229+ let poll_interval = std::time::Duration::from_millis(100);
230+ loop {
231+ for tid in 0..threads_to_create {
232+ let thread = gix_features::parallel::build_thread()
233+ .name(format!("gix-pack.traverse_deltas.{tid}"))
234+ .spawn_scoped(s, {
235+ let nodes = &nodes;
236+ let decompressed_bytes_by_pack_offset = &decompressed_bytes_by_pack_offset;
237+ let mut resolve = resolve.clone();
238+ let mut modify_base = modify_base.clone();
239+ let object_counter = object_counter.as_ref();
240+ let size_counter = size_counter.as_ref();
241+
242+ move || -> Result<(), Error> {
243+ let mut bytes_buf = Vec::new();
244+ let mut delta_bytes = Vec::new();
245+ let mut fully_resolved_delta_bytes = Vec::new();
246+ let mut decompress_from_resolver =
247+ |slice: EntryRange, out: &mut Vec<u8>| -> Result<(data::Entry, u64), Error> {
248+ let bytes = resolve(slice.clone(), resolve_data).ok_or(Error::ResolveFailed {
249+ pack_offset: slice.start,
250+ })?;
251+ let entry = data::Entry::from_bytes(bytes, slice.start, hash_len);
252+ let compressed = &bytes[entry.header_size()..];
253+ let decompressed_len = entry.decompressed_size as usize;
254+ decompress_all_at_once(compressed, decompressed_len, out)?;
255+ Ok((entry, slice.end))
256+ };
257+
258+ loop {
259+ let (level, mut base) = match threading::lock(nodes).pop() {
260+ Some(v) => v,
261+ None => break,
262+ };
263+ if should_interrupt.load(Ordering::Relaxed) {
264+ return Err(Error::Interrupted);
265+ }
266+ let storage;
267+ let (base_entry, entry_end, base_bytes) = if level == 0 {
268+ let (a, b) = decompress_from_resolver(base.entry_slice(), &mut bytes_buf)?;
269+ (a, b, &*bytes_buf)
270+ } else {
271+ if !bytes_buf.is_empty() {
272+ bytes_buf = Vec::new();
273+ }
274+ let (a, b, v) = threading::lock(decompressed_bytes_by_pack_offset)
275+ .remove(&base.offset())
276+ .expect("we store the resolved delta buffer when done");
277+ storage = v;
278+ (a, b, storage.as_slice())
279+ };
280+
281+ // anything done here must be repeated further down for leaf-nodes.
282+ // This way we avoid retaining their decompressed memory longer than needed (they have no children,
283+ // thus their memory can be released right away, using 18% less peak memory on the linux kernel).
284+ {
285+ modify_base(
286+ base.data(),
287+ progress,
288+ Context {
289+ entry: &base_entry,
290+ entry_end,
291+ decompressed: base_bytes,
292+ level,
293+ },
294+ )
295+ .map_err(|err| Box::new(err) as Box<dyn std::error::Error + Send + Sync>)?;
296+ object_counter.as_ref().map(|c| c.fetch_add(1, Ordering::SeqCst));
297+ size_counter
298+ .as_ref()
299+ .map(|c| c.fetch_add(base_bytes.len(), Ordering::SeqCst));
300+ }
301+
302+ for mut child in base.into_child_iter() {
303+ let (mut child_entry, entry_end) =
304+ decompress_from_resolver(child.entry_slice(), &mut delta_bytes)?;
305+ let (base_size, consumed) = data::delta::decode_header_size(&delta_bytes);
306+ let mut header_ofs = consumed;
307+ assert_eq!(
308+ base_bytes.len(),
309+ base_size as usize,
310+ "recorded base size in delta does match the actual one"
311+ );
312+ let (result_size, consumed) =
313+ data::delta::decode_header_size(&delta_bytes[consumed..]);
314+ header_ofs += consumed;
315+
316+ fully_resolved_delta_bytes.resize(result_size as usize, 0);
317+ data::delta::apply(
318+ base_bytes,
319+ &mut fully_resolved_delta_bytes,
320+ &delta_bytes[header_ofs..],
321+ );
322+
323+ // FIXME: this actually invalidates the "pack_offset()" computation, which is not obvious to consumers
324+ // at all
325+ child_entry.header = base_entry.header; // assign the actual object type, instead of 'delta'
326+ if child.has_children() {
327+ threading::lock(decompressed_bytes_by_pack_offset).insert(
328+ child.offset(),
329+ (child_entry, entry_end, std::mem::take(&mut fully_resolved_delta_bytes)),
330+ );
331+ threading::lock(nodes).push((level + 1, child));
332+ } else {
333+ modify_base(
334+ child.data(),
335+ progress,
336+ Context {
337+ entry: &child_entry,
338+ entry_end,
339+ decompressed: &fully_resolved_delta_bytes,
340+ level: level + 1,
341+ },
342+ )
343+ .map_err(|err| Box::new(err) as Box<dyn std::error::Error + Send + Sync>)?;
344+ object_counter.as_ref().map(|c| c.fetch_add(1, Ordering::SeqCst));
345+ size_counter
346+ .as_ref()
347+ .map(|c| c.fetch_add(base_bytes.len(), Ordering::SeqCst));
348+ }
349+ }
350+ }
351+ Ok(())
352+ }
353+ })?;
354+ threads.push(thread);
355+ }
356+ if threads_left
357+ .fetch_update(Ordering::SeqCst, Ordering::SeqCst, |threads_available: isize| {
358+ (threads_available > 0).then(|| {
359+ threads_to_create = threads_available.min(threading::lock(&nodes).len() as isize);
360+ threads_available - threads_to_create
361+ })
362+ })
363+ .is_err()
364+ {
365+ threads_to_create = 0;
366+ }
367+
368+ // What we really want to do is either wait for one of our threads to go down
369+ // or for another scheduled thread to become available. Unfortunately we can't do that,
370+ // but may instead find a good way to set the polling interval instead of hard-coding it.
371+ std::thread::sleep(poll_interval);
372+ // Get out of threads are already starving or they would be starving soon as no work is left.
373+ if threads.iter().any(|t| t.is_finished()) {
374+ let mut running_threads = Vec::new();
375+ for thread in threads.drain(..) {
376+ if thread.is_finished() {
377+ match thread.join() {
378+ Ok(Err(err)) => return Err(err),
379+ Ok(Ok(())) => {
380+ if !returned_ourselves {
381+ returned_ourselves = true;
382+ } else {
383+ threads_left.fetch_add(1, Ordering::SeqCst);
384+ }
385+ }
386+ Err(err) => {
387+ std::panic::resume_unwind(err);
388+ }
389+ }
390+ } else {
391+ running_threads.push(thread);
392+ }
393+ }
394+ if running_threads.is_empty() && threading::lock(&nodes).is_empty() {
395+ break;
396+ }
397+ threads = running_threads;
398+ }
399+ }
400+
401+ Ok(())
402+ })
403+}
404+
405+fn resize_vec(v: &mut Vec<u8>, new_len: usize) {
406+ if new_len > v.len() {
407+ v.reserve_exact(new_len.saturating_sub(v.capacity()) + (v.capacity() - v.len()));
408+ // SAFETY:
409+ // 1. we have reserved enough capacity to fit `new_len`
410+ // 2. the caller is trusted to write into `v` to completely fill `new_len`.
411+ #[allow(unsafe_code, clippy::uninit_vec)]
412+ unsafe {
413+ v.set_len(new_len);
414+ }
415+ } else {
416+ v.truncate(new_len)
158417 }
418+}
419+
420+fn decompress_all_at_once(b: &[u8], decompressed_len: usize, out: &mut Vec<u8>) -> Result<(), Error> {
421+ resize_vec(out, decompressed_len);
159422 zlib::Inflate::default()
160− .once(b, &mut out)
423+ .once(b, out)
161424 .map_err(|err| Error::ZlibInflate {
162425 source: err,
163426 message: "Failed to decompress entry",
164427 })?;
165− Ok(out)
428+ Ok(())
166429 }
gix-pack/src/cache/delta/traverse/util.rs+7−5
@@ -4,6 +4,8 @@ pub struct ItemSliceSend<T>(pub *mut [T]) · 3 unmodified lines
44 where
55 T: Send;
66
7+/// SAFETY: This would be unsafe if this would ever be abused, but it's used internally and only in a way that assure that the pointers
8+/// don't violate aliasing rules.
79 impl<T> Clone for ItemSliceSend<T>
810 where
911 T: Send,
@@ -18,12 +20,12 @@ where · 8 unmodified lines
1820 unsafe impl<T> Send for ItemSliceSend<T> where T: Send {}
1921
2022 /// An item returned by `iter_root_chunks`, allowing access to the `data` stored alongside nodes in a [`Tree`].
21−pub struct Node<'a, T> {
23+pub struct Node<'a, T: Send> {
2224 pub item: &'a mut Item<T>,
23− pub child_items: *mut [Item<T>],
25+ pub child_items: ItemSliceSend<Item<T>>,
2426 }
2527
26−impl<'a, T> Node<'a, T> {
28+impl<'a, T: Send> Node<'a, T> {
2729 /// Returns the offset into the pack at which the `Node`s data is located.
2830 pub fn offset(&self) -> u64 {
2931 self.item.offset
@@ -55,8 +57,8 @@ impl<'a, T> Node<'a, T> { · 25 unmodified lines
5557 // SAFETY: The resulting mutable pointer cannot be yielded by any other node.
5658 #[allow(unsafe_code)]
5759 Node {
58− item: unsafe { &mut *(children as *mut Item<T>).add(index as usize) },
59− child_items: children,
60+ item: &mut unsafe { &mut *children.0 }[index as usize],
61+ child_items: children.clone(),
6062 }
6163 })
6264 }
gix-pack/src/index/traverse/mod.rs+18−22
@@ -1,6 +1,9 @@
11 use std::sync::atomic::AtomicBool;
22
3−use gix_features::{parallel, progress::Progress};
3+use gix_features::{
4+ parallel,
5+ progress::{Progress, RawProgress},
6+};
47
58 use crate::index;
69
@@ -79,7 +82,7 @@ impl index::File { · 72 unmodified lines
7982 pack: &crate::data::File,
8083 progress: P,
8184 should_interrupt: &AtomicBool,
82− new_processor: impl Fn() -> Processor + Send + Clone,
85+ processor: Processor,
8386 Options {
8487 traversal,
8588 thread_limit,
@@ -91,17 +94,12 @@ impl index::File { · 5 unmodified lines
9194 P: Progress,
9295 C: crate::cache::DecodeEntry,
9396 E: std::error::Error + Send + Sync + 'static,
94− Processor: FnMut(
95− gix_object::Kind,
96− &[u8],
97− &index::Entry,
98− &mut <P::SubProgress as Progress>::SubProgress,
99− ) -> Result<(), E>,
97+ Processor: FnMut(gix_object::Kind, &[u8], &index::Entry, &dyn RawProgress) -> Result<(), E> + Send + Clone,
10098 F: Fn() -> C + Send + Clone,
10199 {
102100 match traversal {
103101 Algorithm::Lookup => self.traverse_with_lookup(
104− new_processor,
102+ processor,
105103 pack,
106104 progress,
107105 should_interrupt,
@@ -113,10 +111,10 @@ impl index::File { · 5 unmodified lines
113111 ),
114112 Algorithm::DeltaTreeLookup => self.traverse_with_index(
115113 pack,
116− new_processor,
114+ processor,
117115 progress,
118116 should_interrupt,
119− crate::index::traverse::with_index::Options { check, thread_limit },
117+ with_index::Options { check, thread_limit },
120118 ),
121119 }
122120 }
@@ -151,19 +149,18 @@ impl index::File { · 28 unmodified lines
151149 }
152150
153151 #[allow(clippy::too_many_arguments)]
154− fn decode_and_process_entry<C, P, E>(
152+ fn decode_and_process_entry<C, E>(
155153 &self,
156154 check: SafetyCheck,
157155 pack: &crate::data::File,
158156 cache: &mut C,
159157 buf: &mut Vec<u8>,
160− progress: &mut P,
161− index_entry: &crate::index::Entry,
162− processor: &mut impl FnMut(gix_object::Kind, &[u8], &index::Entry, &mut P) -> Result<(), E>,
158+ progress: &mut dyn RawProgress,
159+ index_entry: &index::Entry,
160+ processor: &mut impl FnMut(gix_object::Kind, &[u8], &index::Entry, &dyn RawProgress) -> Result<(), E>,
163161 ) -> Result<crate::data::decode::entry::Outcome, Error<E>>
164162 where
165163 C: crate::cache::DecodeEntry,
166− P: Progress,
167164 E: std::error::Error + Send + Sync + 'static,
168165 {
169166 let pack_entry = pack.entry(index_entry.pack_offset);
@@ -192,9 +189,9 @@ impl index::File { · 22 unmodified lines
192189 check,
193190 object_kind,
194191 buf,
195− progress,
196192 index_entry,
197193 || pack.entry_crc32(index_entry.pack_offset, entry_len),
194+ progress,
198195 processor,
199196 )?;
200197 Ok(entry_stats)
@@ -202,17 +199,16 @@ impl index::File { · 1 unmodified line
202199 }
203200
204201 #[allow(clippy::too_many_arguments)]
205−fn process_entry<P, E>(
202+fn process_entry<E>(
206203 check: SafetyCheck,
207204 object_kind: gix_object::Kind,
208205 decompressed: &[u8],
209− progress: &mut P,
210− index_entry: &crate::index::Entry,
206+ index_entry: &index::Entry,
211207 pack_entry_crc32: impl FnOnce() -> u32,
212− processor: &mut impl FnMut(gix_object::Kind, &[u8], &index::Entry, &mut P) -> Result<(), E>,
208+ progress: &dyn RawProgress,
209+ processor: &mut impl FnMut(gix_object::Kind, &[u8], &index::Entry, &dyn RawProgress) -> Result<(), E>,
213210 ) -> Result<(), Error<E>>
214211 where
215− P: Progress,
216212 E: std::error::Error + Send + Sync + 'static,
217213 {
218214 if check.object_checksum() {
gix-pack/src/index/traverse/with_index.rs+16−20
@@ -59,19 +59,16 @@ impl index::File { · 58 unmodified lines
5959 pub fn traverse_with_index<P, Processor, E>(
6060 &self,
6161 pack: &crate::data::File,
62− new_processor: impl Fn() -> Processor + Send + Clone,
62+ mut processor: Processor,
6363 mut progress: P,
6464 should_interrupt: &AtomicBool,
6565 Options { check, thread_limit }: Options,
6666 ) -> Result<Outcome<P>, Error<E>>
6767 where
6868 P: Progress,
69− Processor: FnMut(
70− gix_object::Kind,
71− &[u8],
72− &index::Entry,
73− &mut <P::SubProgress as Progress>::SubProgress,
74− ) -> Result<(), E>,
69+ Processor: FnMut(gix_object::Kind, &[u8], &index::Entry, &dyn gix_features::progress::RawProgress) -> Result<(), E>
70+ + Send
71+ + Clone,
7572 E: std::error::Error + Send + Sync + 'static,
7673 {
7774 let (verify_result, traversal_result) = parallel::join(
@@ -113,18 +110,17 @@ impl index::File { · 35 unmodified lines
113110 self.object_hash,
114111 )?;
115112 let mut outcome = digest_statistics(tree.traverse(
116− |slice, out| pack.entry_slice(slice).map(|entry| out.copy_from_slice(entry)),
113+ |slice, pack| pack.entry_slice(slice),
114+ pack,
117115 pack.pack_end() as u64,
118− new_processor,
119− |data,
120− progress,
121− traverse::Context {
122− entry: pack_entry,
123− entry_end,
124− decompressed: bytes,
125− state: ref mut processor,
126− level,
127− }| {
116+ move |data,
117+ progress,
118+ traverse::Context {
119+ entry: pack_entry,
120+ entry_end,
121+ decompressed: bytes,
122+ level,
123+ }| {
128124 let object_kind = pack_entry.header.as_kind().expect("non-delta object");
129125 data.level = level;
130126 data.decompressed_size = pack_entry.decompressed_size;
@@ -135,7 +131,6 @@ impl index::File { · 4 unmodified lines
135131 check,
136132 object_kind,
137133 bytes,
138− progress,
139134 &data.index_entry,
140135 || {
141136 // TODO: Fix this - we overwrite the header of 'data' which also changes the computed entry size,
@@ -146,7 +141,8 @@ impl index::File { · 4 unmodified lines
146141 .expect("slice pointing into the pack (by now data is verified)"),
147142 )
148143 },
149− processor,
144+ progress,
145+ &mut processor,
150146 );
151147 match result {
152148 Err(err @ Error::PackDecode { .. }) if !check.fatal_decode_error() => {
gix-pack/src/index/traverse/with_lookup.rs+8−12
@@ -67,7 +67,7 @@ impl index::File { · 66 unmodified lines
6767 /// For more details, see the documentation on the [`traverse()`][index::File::traverse()] method.
6868 pub fn traverse_with_lookup<P, C, Processor, E, F>(
6969 &self,
70− new_processor: impl Fn() -> Processor + Send + Clone,
70+ mut processor: Processor,
7171 pack: &data::File,
7272 mut progress: P,
7373 should_interrupt: &AtomicBool,
@@ -81,12 +81,9 @@ impl index::File { · 7 unmodified lines
8181 P: Progress,
8282 C: crate::cache::DecodeEntry,
8383 E: std::error::Error + Send + Sync + 'static,
84− Processor: FnMut(
85− gix_object::Kind,
86− &[u8],
87− &index::Entry,
88− &mut <P::SubProgress as Progress>::SubProgress,
89− ) -> Result<(), E>,
84+ Processor: FnMut(gix_object::Kind, &[u8], &index::Entry, &dyn gix_features::progress::RawProgress) -> Result<(), E>
85+ + Send
86+ + Clone,
9087 F: Fn() -> C + Send + Clone,
9188 {
9289 let (verify_result, traversal_result) = parallel::join(
@@ -133,7 +130,6 @@ impl index::File { · 40 unmodified lines
133130 move |index| {
134131 (
135132 make_pack_lookup_cache(),
136− new_processor(),
137133 Vec::with_capacity(2048), // decode buffer
138134 lock(&reduce_progress)
139135 .add_child_with_id(format!("thread {index}"), gix_features::progress::UNKNOWN), // per thread progress
@@ -146,9 +142,9 @@ impl index::File { · 6 unmodified lines
146142 input_chunks,
147143 thread_limit,
148144 state_per_thread,
149− |entries: &[index::Entry],
150− (cache, ref mut processor, buf, progress)|
151− -> Result<Vec<data::decode::entry::Outcome>, Error<_>> {
145+ move |entries: &[index::Entry],
146+ (cache, buf, progress)|
147+ -> Result<Vec<data::decode::entry::Outcome>, Error<_>> {
152148 progress.init(
153149 Some(entries.len()),
154150 gix_features::progress::count_with_decimals("objects", 2),
@@ -163,7 +159,7 @@ impl index::File { · 8 unmodified lines
163159 buf,
164160 progress,
165161 index_entry,
166− processor,
162+ &mut processor,
167163 );
168164 progress.inc();
169165 let stat = match result {
gix-pack/src/index/verify.rs+5−8
@@ -198,7 +198,7 @@ impl index::File { · 197 unmodified lines
198198 pack,
199199 progress,
200200 should_interrupt,
201− || {
201+ {
202202 let mut encode_buf = Vec::with_capacity(2048);
203203 move |kind, data, index_entry, progress| {
204204 Self::verify_entry(verify_mode, &mut encode_buf, kind, data, index_entry, progress)
@@ -231,17 +231,14 @@ impl index::File { · 26 unmodified lines
231231 }
232232
233233 #[allow(clippy::too_many_arguments)]
234− fn verify_entry<P>(
234+ fn verify_entry(
235235 verify_mode: Mode,
236236 encode_buf: &mut Vec<u8>,
237237 object_kind: gix_object::Kind,
238238 buf: &[u8],
239239 index_entry: &index::Entry,
240− progress: &mut P,
241− ) -> Result<(), integrity::Error>
242− where
243− P: Progress,
244− {
240+ progress: &dyn gix_features::progress::RawProgress,
241+ ) -> Result<(), integrity::Error> {
245242 if let Mode::HashCrc32Decode | Mode::HashCrc32DecodeEncode = verify_mode {
246243 use gix_object::Kind::*;
247244 match object_kind {
@@ -260,7 +257,7 @@ impl index::File { · 12 unmodified lines
260257 .expect("writing to a memory buffer never fails");
261258 if encode_buf.as_slice() != buf {
262259 let mut should_return_error = true;
263− if let gix_object::Kind::Tree = object_kind {
260+ if let Tree = object_kind {
264261 if buf.as_bstr().find(b"100664").is_some() || buf.as_bstr().find(b"100640").is_some() {
265262 progress.info(format!("Tree object {} would be cleaned up during re-serialization, replacing mode '100664|100640' with '100644'", index_entry.oid));
266263 should_return_error = false
gix-pack/src/index/write/mod.rs+9−14
@@ -83,20 +83,22 @@ impl crate::index::File { · 82 unmodified lines
8383 /// It should return `None` if the entry cannot be resolved from the pack that produced the `entries` iterator, causing
8484 /// the write operation to fail.
8585 #[allow(clippy::too_many_arguments)]
86− pub fn write_data_iter_to_stream<F, F2>(
86+ pub fn write_data_iter_to_stream<F, F2, R, P>(
8787 version: crate::index::Version,
8888 make_resolver: F,
8989 entries: impl Iterator<Item = Result<crate::data::input::Entry, crate::data::input::Error>>,
9090 thread_limit: Option<usize>,
91− mut root_progress: impl Progress,
91+ mut root_progress: P,
9292 out: impl io::Write,
9393 should_interrupt: &AtomicBool,
9494 object_hash: gix_hash::Kind,
9595 pack_version: crate::data::Version,
9696 ) -> Result<Outcome, Error>
9797 where
98− F: FnOnce() -> io::Result<F2>,
99− F2: for<'r> Fn(crate::data::EntryRange, &'r mut Vec<u8>) -> Option<()> + Send + Clone,
98+ F: FnOnce() -> io::Result<(F2, R)>,
99+ R: Send + Sync,
100+ F2: for<'r> Fn(crate::data::EntryRange, &'r R) -> Option<&'r [u8]> + Send + Clone,
101+ P: Progress,
100102 {
101103 if version != crate::index::Version::default() {
102104 return Err(Error::Unsupported(version));
@@ -180,12 +182,12 @@ impl crate::index::File { · 77 unmodified lines
180182
181183 root_progress.inc();
182184
183− let resolver = make_resolver()?;
185+ let (resolver, pack) = make_resolver()?;
184186 let sorted_pack_offsets_by_oid = {
185187 let traverse::Outcome { roots, children } = tree.traverse(
186188 resolver,
189+ &pack,
187190 pack_entries_end,
188− || (),
189191 |data,
190192 _progress,
191193 traverse::Context {
@@ -250,14 +252,7 @@ impl crate::index::File { · 58 unmodified lines
250252 }
251253
252254 fn modify_base(entry: &mut TreeEntry, pack_entry: &crate::data::Entry, decompressed: &[u8], hash: gix_hash::Kind) {
253− fn compute_hash(kind: gix_object::Kind, bytes: &[u8], object_hash: gix_hash::Kind) -> gix_hash::ObjectId {
254− let mut hasher = gix_features::hash::hasher(object_hash);
255− hasher.update(&gix_object::encode::loose_header(kind, bytes.len()));
256− hasher.update(bytes);
257− gix_hash::ObjectId::from(hasher.digest())
258− }
259−
260255 let object_kind = pack_entry.header.as_kind().expect("base object as source of iteration");
261− let id = compute_hash(object_kind, decompressed, hash);
256+ let id = gix_object::compute_hash(hash, object_kind, decompressed);
262257 entry.id = id;
263258 }
gix-pack/tests/pack/index.rs+16−27
@@ -115,28 +115,25 @@ mod version { · 114 unmodified lines
115115
116116 use gix_features::progress;
117117 use gix_odb::pack;
118− use gix_pack::{
119− data::{input, EntryRange},
120− index,
121− };
118+ use gix_pack::{data::input, index};
122119
123120 use crate::{
124121 fixture_path,
125122 pack::{INDEX_V2, V2_PACKS_AND_INDICES},
126123 };
127124
125+ fn slice_map(entry: gix_pack::data::EntryRange, map: &memmap2::Mmap) -> Option<&[u8]> {
126+ map.get(entry.start as usize..entry.end as usize)
127+ }
128+
128129 #[test]
129130 fn write_to_stream() -> Result<(), Box<dyn std::error::Error>> {
130− fn assert_index_write<F>(
131+ fn assert_index_write(
131132 mode: &input::Mode,
132133 compressed: &input::EntryDataMode,
133134 index_path: &&str,
134135 data_path: &&str,
135− resolve: F,
136− ) -> Result<(), Box<dyn std::error::Error>>
137− where
138− F: Fn(pack::data::EntryRange, &mut Vec<u8>) -> Option<()> + Send + Clone,
139− {
136+ ) -> Result<(), Box<dyn std::error::Error>> {
140137 let pack_iter = pack::data::input::BytesToEntriesIter::new_from_header(
141138 io::BufReader::new(fs::File::open(fixture_path(data_path))?),
142139 *mode,
@@ -150,7 +147,11 @@ mod version { · 7 unmodified lines
150147 let pack_version = pack_iter.version();
151148 let outcome = pack::index::File::write_data_iter_to_stream(
152149 desired_kind,
153− move || Ok(resolve),
150+ || {
151+ let file = std::fs::File::open(fixture_path(data_path))?;
152+ let map = unsafe { memmap2::Mmap::map(&file)? };
153+ Ok((slice_map, map))
154+ },
154155 pack_iter,
155156 None,
156157 progress::Discard,
@@ -217,17 +218,7 @@ mod version { · 60 unmodified lines
217218 for mode in &[input::Mode::AsIs, input::Mode::Verify, input::Mode::Restore] {
218219 for compressed in &[input::EntryDataMode::Crc32, input::EntryDataMode::KeepAndCrc32] {
219220 for (index_path, data_path) in V2_PACKS_AND_INDICES {
220− let resolve = {
221− let buf = gix_features::threading::OwnShared::new({
222− let file = std::fs::File::open(fixture_path(data_path))?;
223− unsafe { memmap2::Mmap::map(&file)? }
224− });
225− move |entry: EntryRange, out: &mut Vec<u8>| {
226− buf.get(entry.start as usize..entry.end as usize)
227− .map(|slice| out.copy_from_slice(slice))
228− }
229− };
230− assert_index_write(mode, compressed, index_path, data_path, resolve)?;
221+ assert_index_write(mode, compressed, index_path, data_path)?;
231222 }
232223 }
233224 }
@@ -259,11 +250,9 @@ fn traverse_with_index_and_forward_ref_deltas() { · 25 unmodified lines
259250 let _it_should_work = index
260251 .traverse_with_index(
261252 &data,
262− || {
263− |_, _, _, _| {
264− count.fetch_add(1, Ordering::SeqCst);
265− Ok::<_, std::io::Error>(())
266− }
253+ |_, _, _, _| {
254+ count.fetch_add(1, Ordering::SeqCst);
255+ Ok::<_, std::io::Error>(())
267256 },
268257 progress::Discard,
269258 &AtomicBool::new(false),