Skip to main content

gix_pack/data/output/count/objects/
mod.rs

1use std::{cell::RefCell, sync::atomic::AtomicBool};
2
3use gix_features::parallel;
4use gix_hash::ObjectId;
5
6use crate::data::output;
7
8pub(in crate::data::output::count::objects_impl) mod reduce;
9mod util;
10
11mod types;
12pub use types::{Error, ObjectExpansion, Options, Outcome};
13
14mod tree;
15
16/// Generate [`Count`][output::Count]s from input `objects` with object expansion based on [`options`][Options]
17/// to learn which objects would constitute a pack. This step is required to know exactly how many objects would
18/// be in a pack while keeping data around to minimize database object access.
19///
20/// A [`Count`][output::Count] object maintains enough state to greatly accelerate future access of packed objects.
21///
22/// * `db` - the object store to use for accessing objects.
23/// * `objects_ids`
24///   * A list of objects IDs to add to the pack. Duplication checks are performed so no object is ever added to a pack twice.
25///   * Objects may be expanded based on the provided [`options`][Options]
26/// * `objects`
27///   * count the amount of objects we encounter
28/// * `should_interrupt`
29///  * A flag that is set to true if the operation should stop
30/// * `options`
31///   * more configuration
32pub fn objects<Find>(
33    db: Find,
34    objects_ids: Box<dyn Iterator<Item = Result<ObjectId, Box<dyn std::error::Error + Send + Sync + 'static>>> + Send>,
35    objects: &dyn gix_features::progress::Count,
36    should_interrupt: &AtomicBool,
37    Options {
38        thread_limit,
39        input_object_expansion,
40        chunk_size,
41    }: Options,
42) -> Result<(Vec<output::Count>, Outcome), Error>
43where
44    Find: crate::Find + Send + Clone,
45{
46    let lower_bound = objects_ids.size_hint().0;
47    let (chunk_size, thread_limit, _) = parallel::optimize_chunk_size_and_thread_limit(
48        chunk_size,
49        if lower_bound == 0 { None } else { Some(lower_bound) },
50        thread_limit,
51        None,
52    );
53    let chunks = gix_features::iter::Chunks {
54        inner: objects_ids,
55        size: chunk_size,
56    };
57    let seen_objs = gix_hashtable::sync::ObjectIdMap::default();
58    let objects = objects.counter();
59
60    parallel::in_parallel(
61        chunks,
62        thread_limit,
63        {
64            move |_| {
65                (
66                    Vec::new(), // object data buffer
67                    Vec::new(), // object data buffer 2 to hold two objects at a time
68                    objects.clone(),
69                )
70            }
71        },
72        {
73            let seen_objs = &seen_objs;
74            move |oids: Vec<_>, (buf1, buf2, objects)| {
75                expand::this(
76                    &db,
77                    input_object_expansion,
78                    seen_objs,
79                    &mut oids.into_iter(),
80                    buf1,
81                    buf2,
82                    objects,
83                    should_interrupt,
84                    true, /*allow pack lookups*/
85                )
86            }
87        },
88        reduce::Statistics::new(),
89    )
90}
91
92/// Like [`objects()`] but using a single thread only to mostly save on the otherwise required overhead.
93pub fn objects_unthreaded(
94    db: &dyn crate::Find,
95    object_ids: &mut dyn Iterator<Item = Result<ObjectId, Box<dyn std::error::Error + Send + Sync + 'static>>>,
96    objects: &dyn gix_features::progress::Count,
97    should_interrupt: &AtomicBool,
98    input_object_expansion: ObjectExpansion,
99) -> Result<(Vec<output::Count>, Outcome), Error> {
100    let seen_objs = RefCell::new(gix_hashtable::HashSet::default());
101
102    let (mut buf1, mut buf2) = (Vec::new(), Vec::new());
103    expand::this(
104        db,
105        input_object_expansion,
106        &seen_objs,
107        object_ids,
108        &mut buf1,
109        &mut buf2,
110        &objects.counter(),
111        should_interrupt,
112        false, /*allow pack lookups*/
113    )
114}
115
116mod expand {
117    use std::{
118        cell::RefCell,
119        sync::atomic::{AtomicBool, Ordering},
120    };
121
122    use gix_hash::{ObjectId, oid};
123    use gix_object::{CommitRefIter, Data, TagRefIter};
124
125    use super::{
126        tree,
127        types::{Error, ObjectExpansion, Outcome},
128        util,
129    };
130    use crate::{
131        FindExt,
132        data::{output, output::count::PackLocation},
133    };
134
135    #[expect(clippy::too_many_arguments)]
136    pub fn this(
137        db: &dyn crate::Find,
138        input_object_expansion: ObjectExpansion,
139        seen_objs: &impl util::InsertImmutable,
140        oids: &mut dyn Iterator<Item = Result<ObjectId, Box<dyn std::error::Error + Send + Sync + 'static>>>,
141        buf1: &mut Vec<u8>,
142        buf2: &mut Vec<u8>,
143        objects: &gix_features::progress::AtomicStep,
144        should_interrupt: &AtomicBool,
145        allow_pack_lookups: bool,
146    ) -> Result<(Vec<output::Count>, Outcome), Error> {
147        use ObjectExpansion::*;
148
149        let mut out = Vec::new();
150        let mut tree_traversal_state = gix_traverse::tree::breadthfirst::State::default();
151        let mut tree_diff_state = gix_diff::tree::State::default();
152        let mut parent_commit_ids = Vec::new();
153        let mut traverse_delegate = tree::traverse::AllUnseen::new(seen_objs);
154        let mut changes_delegate = tree::changes::AllNew::new(seen_objs);
155        let mut outcome = Outcome::default();
156
157        let stats = &mut outcome;
158        for id in oids {
159            if should_interrupt.load(Ordering::Relaxed) {
160                return Err(Error::Interrupted);
161            }
162
163            let id = id.map_err(Error::InputIteration)?;
164            let (obj, location) = db.find(&id, buf1)?;
165            stats.input_objects += 1;
166            match input_object_expansion {
167                TreeAdditionsComparedToAncestor => {
168                    use gix_object::Kind::*;
169                    let mut obj = obj;
170                    let mut location = location;
171                    let mut id = id.to_owned();
172
173                    loop {
174                        push_obj_count_unique(&mut out, seen_objs, &id, location, objects, stats, false);
175                        match obj.kind {
176                            Tree | Blob => break,
177                            Tag => {
178                                id = TagRefIter::from_bytes(obj.data, obj.object_hash)
179                                    .target_id()
180                                    .expect("every tag has a target");
181                                let tmp = db.find(&id, buf1)?;
182
183                                obj = tmp.0;
184                                location = tmp.1;
185
186                                stats.expanded_objects += 1;
187                                continue;
188                            }
189                            Commit => {
190                                let current_tree_iter = {
191                                    let mut commit_iter = CommitRefIter::from_bytes(obj.data, obj.object_hash);
192                                    let tree_id = commit_iter.tree_id().expect("every commit has a tree");
193                                    parent_commit_ids.clear();
194                                    for token in commit_iter {
195                                        match token {
196                                            Ok(gix_object::commit::ref_iter::Token::Parent { id }) => {
197                                                parent_commit_ids.push(id);
198                                            }
199                                            Ok(_) => break,
200                                            Err(err) => return Err(Error::CommitDecode(err)),
201                                        }
202                                    }
203                                    let (obj, location) = db.find(&tree_id, buf1)?;
204                                    push_obj_count_unique(
205                                        &mut out, seen_objs, &tree_id, location, objects, stats, true,
206                                    );
207                                    gix_object::TreeRefIter::from_bytes(obj.data, obj.object_hash)
208                                };
209
210                                let objects_ref = if parent_commit_ids.is_empty() {
211                                    traverse_delegate.clear();
212                                    let objects = ExpandedCountingObjects::new(db, out, objects);
213                                    gix_traverse::tree::breadthfirst(
214                                        current_tree_iter,
215                                        &mut tree_traversal_state,
216                                        &objects,
217                                        &mut traverse_delegate,
218                                    )
219                                    .map_err(Error::TreeTraverse)?;
220                                    out = objects.dissolve(stats);
221                                    &traverse_delegate.non_trees
222                                } else {
223                                    changes_delegate.clear();
224                                    for commit_id in &parent_commit_ids {
225                                        let parent_tree_id = {
226                                            let (parent_commit_obj, location) = db.find(commit_id, buf2)?;
227
228                                            push_obj_count_unique(
229                                                &mut out, seen_objs, commit_id, location, objects, stats, true,
230                                            );
231                                            CommitRefIter::from_bytes(
232                                                parent_commit_obj.data,
233                                                parent_commit_obj.object_hash,
234                                            )
235                                            .tree_id()
236                                            .expect("every commit has a tree")
237                                        };
238                                        let parent_tree = {
239                                            let (parent_tree_obj, location) = db.find(&parent_tree_id, buf2)?;
240                                            push_obj_count_unique(
241                                                &mut out,
242                                                seen_objs,
243                                                &parent_tree_id,
244                                                location,
245                                                objects,
246                                                stats,
247                                                true,
248                                            );
249                                            gix_object::TreeRefIter::from_bytes(
250                                                parent_tree_obj.data,
251                                                parent_tree_obj.object_hash,
252                                            )
253                                        };
254
255                                        let objects = CountingObjects::new(db);
256                                        gix_diff::tree(
257                                            parent_tree,
258                                            current_tree_iter,
259                                            &mut tree_diff_state,
260                                            &objects,
261                                            &mut changes_delegate,
262                                        )
263                                        .map_err(Error::TreeChanges)?;
264                                        stats.decoded_objects += objects.into_count();
265                                    }
266                                    &changes_delegate.objects
267                                };
268                                for id in objects_ref.iter() {
269                                    out.push(id_to_count(db, buf2, id, objects, stats, allow_pack_lookups));
270                                }
271                                break;
272                            }
273                        }
274                    }
275                }
276                TreeContents => {
277                    use gix_object::Kind::*;
278                    let mut id = id;
279                    let mut obj = (obj, location);
280                    loop {
281                        push_obj_count_unique(&mut out, seen_objs, &id, obj.1.clone(), objects, stats, false);
282                        match obj.0.kind {
283                            Tree => {
284                                traverse_delegate.clear();
285                                {
286                                    let objects = ExpandedCountingObjects::new(db, out, objects);
287                                    gix_traverse::tree::breadthfirst(
288                                        gix_object::TreeRefIter::from_bytes(obj.0.data, obj.0.object_hash),
289                                        &mut tree_traversal_state,
290                                        &objects,
291                                        &mut traverse_delegate,
292                                    )
293                                    .map_err(Error::TreeTraverse)?;
294                                    out = objects.dissolve(stats);
295                                }
296                                for id in &traverse_delegate.non_trees {
297                                    out.push(id_to_count(db, buf1, id, objects, stats, allow_pack_lookups));
298                                }
299                                break;
300                            }
301                            Commit => {
302                                id = CommitRefIter::from_bytes(obj.0.data, obj.0.object_hash)
303                                    .tree_id()
304                                    .expect("every commit has a tree");
305                                stats.expanded_objects += 1;
306                                obj = db.find(&id, buf1)?;
307                                continue;
308                            }
309                            Blob => break,
310                            Tag => {
311                                id = TagRefIter::from_bytes(obj.0.data, obj.0.object_hash)
312                                    .target_id()
313                                    .expect("every tag has a target");
314                                stats.expanded_objects += 1;
315                                obj = db.find(&id, buf1)?;
316                                continue;
317                            }
318                        }
319                    }
320                }
321                AsIs => push_obj_count_unique(&mut out, seen_objs, &id, location, objects, stats, false),
322            }
323        }
324        outcome.total_objects = out.len();
325        Ok((out, outcome))
326    }
327
328    #[inline]
329    fn push_obj_count_unique(
330        out: &mut Vec<output::Count>,
331        all_seen: &impl util::InsertImmutable,
332        id: &oid,
333        location: Option<crate::data::entry::Location>,
334        objects: &gix_features::progress::AtomicStep,
335        statistics: &mut Outcome,
336        count_expanded: bool,
337    ) {
338        let inserted = all_seen.insert(id.to_owned());
339        if inserted {
340            objects.fetch_add(1, Ordering::Relaxed);
341            statistics.decoded_objects += 1;
342            if count_expanded {
343                statistics.expanded_objects += 1;
344            }
345            out.push(output::Count::from_data(id, location));
346        }
347    }
348
349    #[inline]
350    fn id_to_count(
351        db: &dyn crate::Find,
352        buf: &mut Vec<u8>,
353        id: &oid,
354        objects: &gix_features::progress::AtomicStep,
355        statistics: &mut Outcome,
356        allow_pack_lookups: bool,
357    ) -> output::Count {
358        objects.fetch_add(1, Ordering::Relaxed);
359        statistics.expanded_objects += 1;
360        output::Count {
361            id: id.to_owned(),
362            entry_pack_location: if allow_pack_lookups {
363                PackLocation::LookedUp(db.location_by_oid(id, buf))
364            } else {
365                PackLocation::NotLookedUp
366            },
367        }
368    }
369
370    struct CountingObjects<'a> {
371        decoded_objects: std::cell::RefCell<usize>,
372        objects: &'a dyn crate::Find,
373    }
374
375    impl<'a> CountingObjects<'a> {
376        fn new(objects: &'a dyn crate::Find) -> Self {
377            Self {
378                decoded_objects: Default::default(),
379                objects,
380            }
381        }
382
383        fn into_count(self) -> usize {
384            self.decoded_objects.into_inner()
385        }
386    }
387
388    impl gix_object::Find for CountingObjects<'_> {
389        fn try_find<'a>(&self, id: &oid, buffer: &'a mut Vec<u8>) -> Result<Option<Data<'a>>, gix_object::find::Error> {
390            let res = Ok(self.objects.try_find(id, buffer)?.map(|t| t.0));
391            *self.decoded_objects.borrow_mut() += 1;
392            res
393        }
394    }
395
396    struct ExpandedCountingObjects<'a> {
397        decoded_objects: std::cell::RefCell<usize>,
398        expanded_objects: std::cell::RefCell<usize>,
399        out: std::cell::RefCell<Vec<output::Count>>,
400        objects_count: &'a gix_features::progress::AtomicStep,
401        objects: &'a dyn crate::Find,
402    }
403
404    impl<'a> ExpandedCountingObjects<'a> {
405        fn new(
406            objects: &'a dyn crate::Find,
407            out: Vec<output::Count>,
408            objects_count: &'a gix_features::progress::AtomicStep,
409        ) -> Self {
410            Self {
411                decoded_objects: Default::default(),
412                expanded_objects: Default::default(),
413                out: RefCell::new(out),
414                objects_count,
415                objects,
416            }
417        }
418
419        fn dissolve(self, stats: &mut Outcome) -> Vec<output::Count> {
420            stats.decoded_objects += self.decoded_objects.into_inner();
421            stats.expanded_objects += self.expanded_objects.into_inner();
422            self.out.into_inner()
423        }
424    }
425
426    impl gix_object::Find for ExpandedCountingObjects<'_> {
427        fn try_find<'a>(&self, id: &oid, buffer: &'a mut Vec<u8>) -> Result<Option<Data<'a>>, gix_object::find::Error> {
428            let maybe_obj = self.objects.try_find(id, buffer)?;
429            *self.decoded_objects.borrow_mut() += 1;
430            match maybe_obj {
431                None => Ok(None),
432                Some((obj, location)) => {
433                    self.objects_count.fetch_add(1, Ordering::Relaxed);
434                    *self.expanded_objects.borrow_mut() += 1;
435                    self.out.borrow_mut().push(output::Count::from_data(id, location));
436                    Ok(Some(obj))
437                }
438            }
439        }
440    }
441}