Skip to main content

gix_pack/data/output/entry/
iter_from_counts.rs

1pub(crate) mod function {
2    use std::{cmp::Ordering, sync::Arc};
3
4    use gix_features::{
5        parallel,
6        parallel::SequenceId,
7        progress::{
8            Progress,
9            prodash::{Count, DynNestedProgress},
10        },
11    };
12
13    use super::{Error, Mode, Options, Outcome, ProgressId, reduce, util};
14    use crate::data::output;
15
16    /// Given a known list of object `counts`, calculate entries ready to be put into a data pack.
17    ///
18    /// This allows objects to be written quite soon without having to wait for the entire pack to be built in memory.
19    /// A chunk of objects is held in memory and compressed using DEFLATE, and serve the output of this iterator.
20    /// That way slow writers will naturally apply back pressure, and communicate to the implementation that more time can be
21    /// spent compressing objects.
22    ///
23    /// * `counts`
24    ///   * A list of previously counted objects to add to the pack. Duplication checks are not performed, no object is expected to be duplicated.
25    /// * `progress`
26    ///   * a way to obtain progress information
27    /// * `options`
28    ///   * more configuration
29    ///
30    /// _Returns_ the checksum of the pack
31    ///
32    /// ## Discussion
33    ///
34    /// ### Advantages
35    ///
36    /// * Begins writing immediately and supports back-pressure.
37    /// * Abstract over object databases and how input is provided.
38    ///
39    /// ### Disadvantages
40    ///
41    /// * ~~currently there is no way to easily write the pack index, even though the state here is uniquely positioned to do
42    ///   so with minimal overhead (especially compared to `gix index-from-pack`)~~ Probably works now by chaining Iterators
43    ///   or keeping enough state to write a pack and then generate an index with recorded data.
44    ///
45    pub fn iter_from_counts<Find>(
46        mut counts: Vec<output::Count>,
47        db: Find,
48        mut progress: Box<dyn DynNestedProgress + 'static>,
49        Options {
50            version,
51            mode,
52            allow_thin_pack,
53            thread_limit,
54            chunk_size,
55            compression,
56        }: Options,
57    ) -> impl Iterator<Item = Result<(SequenceId, Vec<output::Entry>), Error>>
58    + parallel::reduce::Finalize<Reduce = reduce::Statistics<Error>>
59    where
60        Find: crate::Find + Send + Clone + 'static,
61    {
62        assert!(
63            matches!(version, crate::data::Version::V2),
64            "currently we can only write version 2"
65        );
66        let (chunk_size, thread_limit, _) =
67            parallel::optimize_chunk_size_and_thread_limit(chunk_size, Some(counts.len()), thread_limit, None);
68        {
69            let progress = Arc::new(parking_lot::Mutex::new(
70                progress.add_child_with_id("resolving".into(), ProgressId::ResolveCounts.into()),
71            ));
72            progress.lock().init(None, gix_features::progress::count("counts"));
73            let enough_counts_present = counts.len() > 4_000;
74            let start = std::time::Instant::now();
75            parallel::in_parallel_if(
76                || enough_counts_present,
77                counts.chunks_mut(chunk_size),
78                thread_limit,
79                |_n| Vec::<u8>::new(),
80                {
81                    let progress = Arc::clone(&progress);
82                    let db = db.clone();
83                    move |chunk, buf| {
84                        let chunk_size = chunk.len();
85                        for count in chunk {
86                            use crate::data::output::count::PackLocation::*;
87                            match count.entry_pack_location {
88                                LookedUp(_) => continue,
89                                NotLookedUp => count.entry_pack_location = LookedUp(db.location_by_oid(&count.id, buf)),
90                            }
91                        }
92                        progress.lock().inc_by(chunk_size);
93                        Ok::<_, ()>(())
94                    }
95                },
96                parallel::reduce::IdentityWithResult::<(), ()>::default(),
97            )
98            .expect("infallible - we ignore none-existing objects");
99            progress.lock().show_throughput(start);
100        }
101        let counts_range_by_pack_id = match mode {
102            Mode::PackCopyAndBaseObjects => {
103                let mut progress = progress.add_child_with_id("sorting".into(), ProgressId::SortEntries.into());
104                progress.init(Some(counts.len()), gix_features::progress::count("counts"));
105                let start = std::time::Instant::now();
106
107                use crate::data::output::count::PackLocation::*;
108                counts.sort_by(|lhs, rhs| match (&lhs.entry_pack_location, &rhs.entry_pack_location) {
109                    (LookedUp(None), LookedUp(None)) => Ordering::Equal,
110                    (LookedUp(Some(_)), LookedUp(None)) => Ordering::Greater,
111                    (LookedUp(None), LookedUp(Some(_))) => Ordering::Less,
112                    (LookedUp(Some(lhs)), LookedUp(Some(rhs))) => lhs
113                        .pack_id
114                        .cmp(&rhs.pack_id)
115                        .then(lhs.pack_offset.cmp(&rhs.pack_offset)),
116                    (_, _) => unreachable!("counts were resolved beforehand"),
117                });
118
119                let mut index: Vec<(u32, std::ops::Range<usize>)> = Vec::new();
120                let mut chunks_pack_start = counts.partition_point(|e| e.entry_pack_location.is_none());
121                let mut slice = &counts[chunks_pack_start..];
122                while !slice.is_empty() {
123                    let current_pack_id = slice[0].entry_pack_location.as_ref().expect("packed object").pack_id;
124                    let pack_end = slice.partition_point(|e| {
125                        e.entry_pack_location.as_ref().expect("packed object").pack_id == current_pack_id
126                    });
127                    index.push((current_pack_id, chunks_pack_start..chunks_pack_start + pack_end));
128                    slice = &slice[pack_end..];
129                    chunks_pack_start += pack_end;
130                }
131
132                progress.set(counts.len());
133                progress.show_throughput(start);
134
135                index
136            }
137        };
138
139        let counts = Arc::new(counts);
140        let progress = Arc::new(parking_lot::Mutex::new(progress));
141        let chunks = util::ChunkRanges::new(chunk_size, counts.len());
142
143        parallel::reduce::Stepwise::new(
144            chunks.enumerate(),
145            thread_limit,
146            {
147                let progress = Arc::clone(&progress);
148                move |n| {
149                    (
150                        Vec::new(), // object data buffer
151                        progress
152                            .lock()
153                            .add_child_with_id(format!("thread {n}"), gix_features::progress::UNKNOWN),
154                    )
155                }
156            },
157            {
158                let counts = Arc::clone(&counts);
159                move |(chunk_id, chunk_range): (SequenceId, std::ops::Range<usize>), (buf, progress)| {
160                    let mut out = Vec::new();
161                    let chunk = &counts[chunk_range];
162                    let mut stats = Outcome::default();
163                    let mut pack_offsets_to_id = None;
164                    progress.init(Some(chunk.len()), gix_features::progress::count("objects"));
165
166                    for count in chunk.iter() {
167                        out.push(match count
168                            .entry_pack_location
169                            .as_ref()
170                            .and_then(|l| db.entry_by_location(l).map(|pe| (l, pe)))
171                        {
172                            Some((location, pack_entry)) => {
173                                if let Some((cached_pack_id, _)) = &pack_offsets_to_id {
174                                    if *cached_pack_id != location.pack_id {
175                                        pack_offsets_to_id = None;
176                                    }
177                                }
178                                let pack_range = counts_range_by_pack_id[counts_range_by_pack_id
179                                    .binary_search_by_key(&location.pack_id, |e| e.0)
180                                    .expect("pack-id always present")]
181                                .1
182                                .clone();
183                                let base_index_offset = pack_range.start;
184                                let counts_in_pack = &counts[pack_range];
185                                let entry = output::Entry::from_pack_entry(
186                                    pack_entry,
187                                    count,
188                                    counts_in_pack,
189                                    base_index_offset,
190                                    allow_thin_pack.then_some({
191                                        |pack_id, base_offset| {
192                                            let (cached_pack_id, cache) = pack_offsets_to_id.get_or_insert_with(|| {
193                                                db.pack_offsets_and_oid(pack_id)
194                                                    .map(|mut v| {
195                                                        v.sort_by_key(|e| e.0);
196                                                        (pack_id, v)
197                                                    })
198                                                    .expect("pack used for counts is still available")
199                                            });
200                                            debug_assert_eq!(*cached_pack_id, pack_id);
201                                            stats.ref_delta_objects += 1;
202                                            cache
203                                                .binary_search_by_key(&base_offset, |e| e.0)
204                                                .ok()
205                                                .map(|idx| cache[idx].1)
206                                        }
207                                    }),
208                                    version,
209                                );
210                                match entry {
211                                    Some(entry) => {
212                                        stats.objects_copied_from_pack += 1;
213                                        entry
214                                    }
215                                    None => match db.try_find(&count.id, buf).map_err(Error::Find)? {
216                                        Some((obj, _location)) => {
217                                            stats.decoded_and_recompressed_objects += 1;
218                                            output::Entry::from_data(count, &obj, compression)
219                                        }
220                                        None => {
221                                            stats.missing_objects += 1;
222                                            Ok(output::Entry::invalid())
223                                        }
224                                    },
225                                }
226                            }
227                            None => match db.try_find(&count.id, buf).map_err(Error::Find)? {
228                                Some((obj, _location)) => {
229                                    stats.decoded_and_recompressed_objects += 1;
230                                    output::Entry::from_data(count, &obj, compression)
231                                }
232                                None => {
233                                    stats.missing_objects += 1;
234                                    Ok(output::Entry::invalid())
235                                }
236                            },
237                        }?);
238                        progress.inc();
239                    }
240                    Ok((chunk_id, out, stats))
241                }
242            },
243            reduce::Statistics::default(),
244        )
245    }
246}
247
248mod util {
249    #[derive(Clone)]
250    pub struct ChunkRanges {
251        cursor: usize,
252        size: usize,
253        len: usize,
254    }
255
256    impl ChunkRanges {
257        pub fn new(size: usize, total: usize) -> Self {
258            ChunkRanges {
259                cursor: 0,
260                size,
261                len: total,
262            }
263        }
264    }
265
266    impl Iterator for ChunkRanges {
267        type Item = std::ops::Range<usize>;
268
269        fn next(&mut self) -> Option<Self::Item> {
270            if self.cursor >= self.len {
271                None
272            } else {
273                let upper = (self.cursor + self.size).min(self.len);
274                let range = self.cursor..upper;
275                self.cursor = upper;
276                Some(range)
277            }
278        }
279    }
280}
281
282mod reduce {
283    use std::marker::PhantomData;
284
285    use gix_features::{parallel, parallel::SequenceId};
286
287    use super::Outcome;
288    use crate::data::output;
289
290    pub struct Statistics<E> {
291        total: Outcome,
292        _err: PhantomData<E>,
293    }
294
295    impl<E> Default for Statistics<E> {
296        fn default() -> Self {
297            Statistics {
298                total: Default::default(),
299                _err: PhantomData,
300            }
301        }
302    }
303
304    impl<Error> parallel::Reduce for Statistics<Error> {
305        type Input = Result<(SequenceId, Vec<output::Entry>, Outcome), Error>;
306        type FeedProduce = (SequenceId, Vec<output::Entry>);
307        type Output = Outcome;
308        type Error = Error;
309
310        fn feed(&mut self, item: Self::Input) -> Result<Self::FeedProduce, Self::Error> {
311            item.map(|(cid, entries, stats)| {
312                self.total.aggregate(stats);
313                (cid, entries)
314            })
315        }
316
317        fn finalize(self) -> Result<Self::Output, Self::Error> {
318            Ok(self.total)
319        }
320    }
321}
322
323mod types {
324    use crate::data::output::entry;
325
326    /// Information gathered during the run of [`iter_from_counts()`][crate::data::output::entry::iter_from_counts()].
327    #[derive(Default, PartialEq, Eq, Debug, Hash, Ord, PartialOrd, Clone, Copy)]
328    #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
329    pub struct Outcome {
330        /// The amount of fully decoded objects. These are the most expensive as they are fully decoded.
331        pub decoded_and_recompressed_objects: usize,
332        /// The amount of objects that could not be located despite them being mentioned during iteration
333        pub missing_objects: usize,
334        /// The amount of base or delta objects that could be copied directly from the pack. These are cheapest as they
335        /// only cost a memory copy for the most part.
336        pub objects_copied_from_pack: usize,
337        /// The amount of objects that ref to their base as ref-delta, an indication for a thin back being created.
338        pub ref_delta_objects: usize,
339    }
340
341    impl Outcome {
342        pub(in crate::data::output::entry) fn aggregate(
343            &mut self,
344            Outcome {
345                decoded_and_recompressed_objects: decoded_objects,
346                missing_objects,
347                objects_copied_from_pack,
348                ref_delta_objects,
349            }: Self,
350        ) {
351            self.decoded_and_recompressed_objects += decoded_objects;
352            self.missing_objects += missing_objects;
353            self.objects_copied_from_pack += objects_copied_from_pack;
354            self.ref_delta_objects += ref_delta_objects;
355        }
356    }
357
358    /// The way the iterator operates.
359    #[derive(PartialEq, Eq, Debug, Hash, Ord, PartialOrd, Clone, Copy)]
360    #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
361    pub enum Mode {
362        /// Copy base objects and deltas from packs, while non-packed objects will be treated as base objects
363        /// (i.e. without trying to delta compress them). This is a fast way of obtaining a back while benefiting
364        /// from existing pack compression and spending the smallest possible time on compressing unpacked objects at
365        /// the cost of bandwidth.
366        PackCopyAndBaseObjects,
367    }
368
369    /// Configuration options for the pack generation functions provided in [`iter_from_counts()`][crate::data::output::entry::iter_from_counts()].
370    #[derive(PartialEq, Eq, Debug, Hash, Ord, PartialOrd, Clone, Copy)]
371    #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
372    pub struct Options {
373        /// The amount of threads to use at most when resolving the pack. If `None`, all logical cores are used.
374        pub thread_limit: Option<usize>,
375        /// The algorithm to produce a pack
376        pub mode: Mode,
377        /// If set, the resulting back can have deltas that refer to an object which is not in the pack. This can happen
378        /// if the initial counted objects do not contain an object that an existing packed delta refers to, for example, because
379        /// it wasn't part of the iteration, for instance when the iteration was performed on tree deltas or only a part of the
380        /// commit graph. Please note that thin packs are not valid packs at rest, thus they are only valid for packs in transit.
381        ///
382        /// If set to false, delta objects will be decompressed and recompressed as base objects.
383        pub allow_thin_pack: bool,
384        /// The amount of objects per chunk or unit of work to be sent to threads for processing
385        /// TODO: could this become the window size?
386        pub chunk_size: usize,
387        /// The pack data version to produce for each entry
388        pub version: crate::data::Version,
389        /// The compression level to use for objects that are not copied from an existing pack,
390        /// but deflated from their object data.
391        ///
392        /// Defaults to [`Compression::DEFAULT`](gix_zlib::Compression::DEFAULT), which is
393        /// also the default that `git` uses when writing packs, unless configured otherwise
394        /// with `pack.compression`.
395        pub compression: gix_zlib::Compression,
396    }
397
398    impl Default for Options {
399        fn default() -> Self {
400            Options {
401                thread_limit: None,
402                mode: Mode::PackCopyAndBaseObjects,
403                allow_thin_pack: false,
404                chunk_size: 10,
405                version: Default::default(),
406                compression: gix_zlib::Compression::DEFAULT,
407            }
408        }
409    }
410
411    /// The error returned by the pack generation function [`iter_from_counts()`][crate::data::output::entry::iter_from_counts()].
412    #[derive(Debug, thiserror::Error)]
413    #[expect(missing_docs)]
414    pub enum Error {
415        #[error(transparent)]
416        Find(gix_object::find::Error),
417        #[error(transparent)]
418        NewEntry(#[from] entry::Error),
419    }
420
421    /// The progress ids used in [`write_to_directory()`][crate::Bundle::write_to_directory()].
422    ///
423    /// Use this information to selectively extract the progress of interest in case the parent application has custom visualization.
424    #[derive(Debug, Copy, Clone)]
425    pub enum ProgressId {
426        /// The amount of [`Count`][crate::data::output::Count] objects which are resolved to their pack location.
427        ResolveCounts,
428        /// Layout pack entries for placement into a pack (by pack-id and by offset).
429        SortEntries,
430    }
431
432    impl From<ProgressId> for gix_features::progress::Id {
433        fn from(v: ProgressId) -> Self {
434            match v {
435                ProgressId::ResolveCounts => *b"ECRC",
436                ProgressId::SortEntries => *b"ECSE",
437            }
438        }
439    }
440}
441pub use types::{Error, Mode, Options, Outcome, ProgressId};