Skip to content

Commit 219655f

Browse files
committed
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.
1 parent 1b08fd9 commit 219655f

File tree

2 files changed

+60
-25
lines changed

2 files changed

+60
-25
lines changed

gix-traverse/src/commit/simple.rs

Lines changed: 59 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -78,13 +78,13 @@ pub enum Error {
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 {
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 {
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 {
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 {
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 {
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 {
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 {
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 {
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 {
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 {
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 {
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 {
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 {
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 {
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 {
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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -133,9 +133,9 @@ mod hide {
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
}

0 commit comments

Comments
 (0)