History

Improve traversal performance when hidden tips are used.

Now it will abort early if there are only hidden tips to be traversed, which cannot change the result anymore.

Sebastian Thiel committed 1 year ago

Commit 219655f1 parent 1b08fd92 files · +60 −25Browse files

Unified · Split

2 files
  1. gix-traverse/src/commit/simple.rs +59 −24
  2. gix-traverse/tests/traverse/commit/simple.rs +1 −1
gix-traverse/src/commit/simple.rs+59−24
@@ -78,13 +78,13 @@ pub enum Error { · 77 unmodified lines
7878 use Result as Either;
7979
8080 type QueueKey<T> = Either<T, Reverse<T>>;
81−type CommitDateQueue = gix_revwalk::PriorityQueue<QueueKey<SecondsSinceUnixEpoch>, ObjectId>;
81+type CommitDateQueue = gix_revwalk::PriorityQueue<QueueKey<SecondsSinceUnixEpoch>, (ObjectId, CommitState)>;
8282 type Candidates = VecDeque<crate::commit::Info>;
8383
8484 /// The state used and potentially shared by multiple graph traversals.
8585 #[derive(Clone)]
8686 pub(super) struct State {
87− next: VecDeque<ObjectId>,
87+ next: VecDeque<(ObjectId, CommitState)>,
8888 queue: CommitDateQueue,
8989 buf: Vec<u8>,
9090 seen: gix_revwalk::graph::IdMap<CommitState>,
@@ -184,9 +184,10 @@ mod init { · 93 unmodified lines
184184 }
185185 Sorting::ByCommitTime(order) | Sorting::ByCommitTimeCutoff { order, .. } => {
186186 let state = &mut self.state;
187− for commit_id in state.next.drain(..) {
187+ for (commit_id, commit_state) in state.next.drain(..) {
188188 add_to_queue(
189189 commit_id,
190+ commit_state,
190191 order,
191192 sorting.cutoff_time(),
192193 &mut state.queue,
@@ -226,11 +227,12 @@ mod init { · 33 unmodified lines
226227 // Assure we *start* traversing hidden variants of a commit first, give them a head-start.
227228 match self.sorting {
228229 Sorting::BreadthFirst => {
229− state.next.push_front(id_to_ignore);
230+ state.next.push_front((id_to_ignore, CommitState::Hidden));
230231 }
231232 Sorting::ByCommitTime(order) | Sorting::ByCommitTimeCutoff { order, .. } => {
232233 add_to_queue(
233234 id_to_ignore,
235+ CommitState::Hidden,
234236 order,
235237 self.sorting.cutoff_time(),
236238 &mut state.queue,
@@ -273,6 +275,7 @@ mod init { · 36 unmodified lines
273275
274276 fn add_to_queue(
275277 commit_id: ObjectId,
278+ commit_state: CommitState,
276279 order: CommitTimeOrder,
277280 cutoff_time: Option<SecondsSinceUnixEpoch>,
278281 queue: &mut CommitDateQueue,
@@ -284,11 +287,11 @@ mod init { · 5 unmodified lines
284287 let key = to_queue_key(time, order);
285288 match (cutoff_time, order) {
286289 (Some(cutoff_time), _) if time >= cutoff_time => {
287− queue.insert(key, commit_id);
290+ queue.insert(key, (commit_id, commit_state));
288291 }
289292 (Some(_), _) => {}
290293 (None, _) => {
291− queue.insert(key, commit_id);
294+ queue.insert(key, (commit_id, commit_state));
292295 }
293296 }
294297 Ok(())
@@ -339,10 +342,11 @@ mod init { · 44 unmodified lines
339342 state.clear();
340343 state.next.reserve(tips.size_hint().0);
341344 for tip in tips.map(Into::into) {
342− let seen = state.seen.insert(tip, CommitState::Interesting);
345+ let commit_state = CommitState::Interesting;
346+ let seen = state.seen.insert(tip, commit_state);
343347 // We know there can only be duplicate interesting ones.
344348 if seen.is_none() && predicate(&tip) {
345− state.next.push_back(tip);
349+ state.next.push_back((tip, commit_state));
346350 }
347351 }
348352 }
@@ -418,16 +422,23 @@ mod init { · 69 unmodified lines
418422 cutoff: Option<SecondsSinceUnixEpoch>,
419423 ) -> Option<Result<Info, Error>> {
420424 let state = &mut self.state;
425+ let next = &mut state.queue;
421426
422427 'skip_hidden: loop {
423− let (commit_time, oid) = match state.queue.pop()? {
428+ let (commit_time, (oid, _queued_commit_state)) = match next.pop()? {
424429 (Newest(t) | Oldest(Reverse(t)), o) => (t, o),
425430 };
426431 let mut parents: ParentIds = Default::default();
427− // TODO(perf): can avoid this lookup by storing state on `queue` respectively.
428− // ALSO: need to look ahead for early aborts, i.e. if there is only hidden left to traverse.
429− // Maybe this can be counted?
432+
433+ // Always use the state that is actually stored, as we may change the type as we go.
430434 let commit_state = *state.seen.get(&oid).expect("every commit we traverse has state added");
435+ if can_deplete_candidates_early(
436+ next.iter_unordered().map(|t| t.1),
437+ commit_state,
438+ state.candidates.as_ref(),
439+ ) {
440+ return None;
441+ }
431442 match super::super::find(self.cache.as_ref(), &self.objects, &oid, &mut state.buf) {
432443 Ok(Either::CachedCommit(commit)) => {
433444 if !collect_parents(&mut state.parent_ids, self.cache.as_ref(), commit.iter_parents()) {
@@ -443,7 +454,7 @@ mod init { · 9 unmodified lines
443454 &mut state.candidates,
444455 commit_state,
445456 &mut self.predicate,
446− &mut state.queue,
457+ next,
447458 order,
448459 cutoff,
449460 || parent_commit_time,
@@ -462,7 +473,7 @@ mod init { · 12 unmodified lines
462473 &mut state.candidates,
463474 commit_state,
464475 &mut self.predicate,
465− &mut state.queue,
476+ next,
466477 order,
467478 cutoff,
468479 || {
@@ -505,6 +516,28 @@ mod init { · 36 unmodified lines
505516 }
506517 }
507518
519+ /// Returns `true` if we have only hidden cursors queued for traversal, assuming that we don't see interesting ones ever again.
520+ ///
521+ /// `unqueued_commit_state` is the state of the commit that is currently being processed.
522+ fn can_deplete_candidates_early(
523+ mut queued_states: impl Iterator<Item = CommitState>,
524+ unqueued_commit_state: CommitState,
525+ candidates: Option<&Candidates>,
526+ ) -> bool {
527+ if candidates.is_none() {
528+ return false;
529+ }
530+ if unqueued_commit_state.is_interesting() {
531+ return false;
532+ }
533+
534+ let mut is_empty = true;
535+ queued_states.all(|state| {
536+ is_empty = false;
537+ state.is_hidden()
538+ }) && !is_empty
539+ }
540+
508541 /// Utilities
509542 impl<Find, Predicate> Simple<Find, Predicate>
510543 where
@@ -513,14 +546,16 @@ mod init { · 2 unmodified lines
513546 {
514547 fn next_by_topology(&mut self) -> Option<Result<Info, Error>> {
515548 let state = &mut self.state;
549+ let next = &mut state.next;
516550 'skip_hidden: loop {
517− let oid = state.next.pop_front()?;
551+ let (oid, _queued_commit_state) = next.pop_front()?;
518552 let mut parents: ParentIds = Default::default();
519− // TODO(perf): can avoid this lookup by storing state on `next` respectively.
520− // ALSO: need to look ahead for early aborts, i.e. if there is only hidden left to traverse.
521− // Maybe this can be counted?
522− let commit_state = *state.seen.get(&oid).expect("every commit we traverse has state added");
523553
554+ // Always use the state that is actually stored, as we may change the type as we go.
555+ let commit_state = *state.seen.get(&oid).expect("every commit we traverse has state added");
556+ if can_deplete_candidates_early(next.iter().map(|t| t.1), commit_state, state.candidates.as_ref()) {
557+ return None;
558+ }
524559 match super::super::find(self.cache.as_ref(), &self.objects, &oid, &mut state.buf) {
525560 Ok(Either::CachedCommit(commit)) => {
526561 if !collect_parents(&mut state.parent_ids, self.cache.as_ref(), commit.iter_parents()) {
@@ -537,7 +572,7 @@ mod init { · 10 unmodified lines
537572 &mut state.candidates,
538573 commit_state,
539574 &mut self.predicate,
540− &mut state.next,
575+ next,
541576 );
542577 if commit_state.is_interesting() && matches!(self.parents, Parents::First) {
543578 break;
@@ -556,7 +591,7 @@ mod init { · 12 unmodified lines
556591 &mut state.candidates,
557592 commit_state,
558593 &mut self.predicate,
559− &mut state.next,
594+ next,
560595 );
561596 if commit_state.is_interesting() && matches!(self.parents, Parents::First) {
562597 break;
@@ -608,7 +643,7 @@ mod init { · 45 unmodified lines
608643 candidates: &mut Option<Candidates>,
609644 commit_state: CommitState,
610645 predicate: &mut impl FnMut(&oid) -> bool,
611− next: &mut VecDeque<ObjectId>,
646+ next: &mut VecDeque<(ObjectId, CommitState)>,
612647 ) {
613648 let enqueue = match seen.entry(parent_id) {
614649 Entry::Occupied(mut e) => {
@@ -627,7 +662,7 @@ mod init { · 12 unmodified lines
627662 }
628663 };
629664 if enqueue {
630− next.push_back(parent_id);
665+ next.push_back((parent_id, commit_state));
631666 }
632667 }
633668
@@ -665,7 +700,7 @@ mod init { · 31 unmodified lines
665700 let key = to_queue_key(parent_commit_time, order);
666701 match cutoff {
667702 Some(cutoff_older_than) if parent_commit_time < cutoff_older_than => {}
668− Some(_) | None => queue.insert(key, parent_id),
703+ Some(_) | None => queue.insert(key, (parent_id, commit_state)),
669704 }
670705 }
671706 }
gix-traverse/tests/traverse/commit/simple.rs+1−1
@@ -133,9 +133,9 @@ mod hide { · 132 unmodified lines
133133
134134 fn all_sortings() -> impl Iterator<Item = Sorting> {
135135 [
136+ Sorting::BreadthFirst,
136137 Sorting::ByCommitTime(CommitTimeOrder::NewestFirst),
137138 Sorting::ByCommitTime(CommitTimeOrder::OldestFirst),
138− Sorting::BreadthFirst,
139139 ]
140140 .into_iter()
141141 }

Keyboard shortcuts

General

Show keyboard shortcuts
?
Focus the current search
/

Issues and pull requests

Open filters
f

Issues, pull requests and Inbox

Select next item
j↓
Select previous item
k↑
Open selected item
Enter
Dismiss filters or clear selection
Esc

Repository code

Go to file
t