revision: use priority queue in limit_list()

limit_list() maintains a date-sorted work queue of commits using a linked list with commit_list_insert_by_date() for insertion. Each insertion walks the list to find the right position — O(n) per insert. In repositories with merge-heavy histories, the symmetric difference can contain thousands of commits, making this O(n) insertion the dominant cost. Replace the sorted linked list with a prio_queue (binary heap). This gives O(log n) insertion and O(log n) extraction instead of O(n) insertion and O(1) extraction, which is a net win when the queue is large. The still_interesting() and everybody_uninteresting() helpers are updated to scan the prio_queue's contiguous array instead of walking a linked list. process_parents() already accepts both a commit_list and a prio_queue parameter, so the change in limit_list() simply switches which one is passed. Benchmark: git rev-list --left-right --count HEAD~N...HEAD Repository: 2.3M commits, merge-heavy DAG (monorepo) Best of 5 runs, times in seconds: commits in symmetric diff baseline patched speedup -------------- -------- ------- ------- 10 0.01 0.01 1.0x 50 0.01 0.01 1.0x 3751 21.23 8.49 2.5x 4524 21.70 8.29 2.6x 10130 20.10 6.65 3.0x No change for small traversals; 2.5-3.0x faster when the queue grows to thousands of commits. Signed-off-by: Kristofer Karlsson <krka@spotify.com> Signed-off-by: Junio C Hamano <gitster@pobox.com>

Kristofer Karlsson committed May 14, 2026 at 16:51 UTC ef8d51a8a3e1c57201aa2c116ad27b0db580123a
1 file changed +21 -17
revision.c
+21 -17
@@ -473,10 +473,10 @@ static struct commit *handle_commit(struct rev_info *revs,
473 die("%s is unknown object", name);
474 }
475
476 -static int everybody_uninteresting(struct commit_list *orig,
476 +static int everybody_uninteresting(struct prio_queue *orig,
477 struct commit **interesting_cache)
478 {
479 - struct commit_list *list = orig;
479 + size_t i;
480
481 if (*interesting_cache) {
482 struct commit *commit = *interesting_cache;
@@ -484,9 +484,8 @@ static int everybody_uninteresting(struct commit_list *orig,
484 return 0;
485 }
486
487 - while (list) {
488 - struct commit *commit = list->item;
489 - list = list->next;
487 + for (i = 0; i < orig->nr; i++) {
488 + struct commit *commit = orig->array[i].data;
489 if (commit->object.flags & UNINTERESTING)
490 continue;
491
@@ -1300,20 +1299,17 @@ static void cherry_pick_list(struct commit_list *list, struct rev_info *revs)
1299 /* How many extra uninteresting commits we want to see.. */
1300 #define SLOP 5
1301
1303 -static int still_interesting(struct commit_list *src, timestamp_t date, int slop,
1302 +static int still_interesting(struct prio_queue *src, timestamp_t date, int slop,
1303 struct commit **interesting_cache)
1304 {
1305 /*
1307 - * No source list at all? We're definitely done..
1306 + * Since src is sorted by date, it is enough to peek at the
1307 + * first entry to compare dates. No entry at all means done.
1308 */
1309 - if (!src)
1309 + struct commit *commit = prio_queue_peek(src);
1310 + if (!commit)
1311 return 0;
1311 -
1312 - /*
1313 - * Does the destination list contain entries with a date
1314 - * before the source list? Definitely _not_ done.
1315 - */
1316 - if (date <= src->item->date)
1312 + if (date <= commit->date)
1313 return SLOP;
1314
1315 /*
@@ -1451,6 +1447,7 @@ static int limit_list(struct rev_info *revs)
1447 struct commit_list *newlist = NULL;
1448 struct commit_list **p = &newlist;
1449 struct commit *interesting_cache = NULL;
1450 + struct prio_queue queue = { .compare = compare_commits_by_commit_date };
1451
1452 if (revs->ancestry_path_implicit_bottoms) {
1453 collect_bottom_commits(original_list,
@@ -1461,6 +1458,11 @@ static int limit_list(struct rev_info *revs)
1458
1459 while (original_list) {
1460 struct commit *commit = pop_commit(&original_list);
1461 + prio_queue_put(&queue, commit);
1462 + }
1463 +
1464 + while (queue.nr) {
1465 + struct commit *commit = prio_queue_get(&queue);
1466 struct object *obj = &commit->object;
1467
1468 if (commit == interesting_cache)
@@ -1468,11 +1470,13 @@ static int limit_list(struct rev_info *revs)
1470
1471 if (revs->max_age != -1 && (commit->date < revs->max_age))
1472 obj->flags |= UNINTERESTING;
1471 - if (process_parents(revs, commit, &original_list, NULL) < 0)
1473 + if (process_parents(revs, commit, NULL, &queue) < 0) {
1474 + clear_prio_queue(&queue);
1475 return -1;
1476 + }
1477 if (obj->flags & UNINTERESTING) {
1478 mark_parents_uninteresting(revs, commit);
1475 - slop = still_interesting(original_list, date, slop, &interesting_cache);
1479 + slop = still_interesting(&queue, date, slop, &interesting_cache);
1480 if (slop)
1481 continue;
1482 break;
@@ -1509,7 +1513,7 @@ static int limit_list(struct rev_info *revs)
1513 }
1514 }
1515
1512 - commit_list_free(original_list);
1516 + clear_prio_queue(&queue);
1517 revs->commits = newlist;
1518 return 0;
1519 }