Re: [PATCH v7 07/10] commit-reach: introduce struct paint_state with per-side counters
Elijah Newren <[email protected]>
| Newsgroups | org.kernel.vger.git |
|---|---|
| Message-ID | <CABPp-BFqghtx4p_Nqx+AWpU7SVn3mXOZGDQ0yoN-ZYQgXmZC=g@mail.gmail.com> |
On Thu, Aug 6, 2026 at 4:05 AM Kristofer Karlsson via GitGitGadget <[email protected]> wrote: > > From: Kristofer Karlsson <[email protected]> > > Add a paint_state struct for use by paint_down_to_common() that > wraps a prio_queue with per-side commit counters. Each non-stale > queued commit occupies exactly one counter bucket based on its > paint flags: PARENT1-only, PARENT2-only, or both sides (a pending > merge-base candidate). > > The counters are maintained by paint_count_update() which adjusts > the appropriate bucket by a signed delta. An exhaustive switch on > the paint+stale bits documents all valid flag combinations in one > place. > > Convert paint_down_to_common() to use paint_state. The loop now > drains the queue via paint_queue_get() which returns NULL when all > counters reach zero, replacing the old pointer-based termination > (max_nonstale). Ooh, I like this setup for what comes later; it sets the stage perfectly for the key insight behind the optimization. Very nice. > This is equivalent behavior -- both conditions > detect that no non-stale entries remain. > > paint_queue_get() uses a "pop first" form: it dequeues a commit, > then checks the counters. This means the loop exits one iteration > earlier than the old code in some topologies (the popped stale > commit is never processed), so a few step counts drop by one. > > The existing nonstale_queue is left in place for ahead_behind(), > though nonstale_queue_put_dedup() and nonstale_queue_get_dedup() > became unused and are removed. became -> become > > Signed-off-by: Kristofer Karlsson <[email protected]> > --- > .../technical/paint-down-to-common.adoc | 9 +- > commit-reach.c | 103 +++++++++++++----- > t/t6600-test-reach.sh | 6 +- > 3 files changed, 82 insertions(+), 36 deletions(-) > > diff --git a/Documentation/technical/paint-down-to-common.adoc b/Documentation/technical/paint-down-to-common.adoc > index cea0cc2f91..37fa6f93c1 100644 > --- a/Documentation/technical/paint-down-to-common.adoc > +++ b/Documentation/technical/paint-down-to-common.adoc > @@ -103,15 +103,12 @@ re-enqueued is bounded by the number of flag transitions. > Termination > ----------- > > -The walk uses a `nonstale_queue` wrapper around `prio_queue` that > -tracks `max_nonstale`: the lowest-priority non-stale commit enqueued > -so far. Once that commit is dequeued, every remaining entry is known > -to be STALE and the loop terminates. Specifically, the main loop > +The walk tracks the number of commits of each type in the queue > +(PARENT1-only, PARENT2-only, pending merge-base). The main loop > ends when one of the following conditions holds: > > 1. The queue is empty. > - 2. `max_nonstale` has been dequeued, meaning the queue only contains > - STALE entries. > + 2. The queue contains only stale entries. > 3. Generation cutoff: the dequeued commit's generation is below > a caller-supplied `min_generation` threshold. > 4. Single result: the caller only needs one merge base, one has > diff --git a/commit-reach.c b/commit-reach.c > index d59e76a2e2..a62b5e4624 100644 > --- a/commit-reach.c > +++ b/commit-reach.c > @@ -79,21 +79,73 @@ static void clear_nonstale_queue(struct nonstale_queue *queue) > queue->max_nonstale = NULL; > } > > -static void nonstale_queue_put_dedup(struct nonstale_queue *queue, > - struct commit *c) > +/* > + * Priority queue with per-side commit counters for paint_down_to_common(). > + * Each non-stale queued commit occupies exactly one bucket: PARENT1-only, > + * PARENT2-only, or both (a pending merge-base candidate). > + */ > +struct paint_state { > + struct prio_queue queue; > + size_t parent1_count; > + size_t parent2_count; > + size_t mb_candidate_count; > + int gen_ordered; > +}; > + > +static void paint_count_update(struct paint_state *state, > + unsigned flags, int delta) > { > - if (c->object.flags & ENQUEUED) > - return; > - c->object.flags |= ENQUEUED; > - nonstale_queue_put(queue, c); > + switch (flags & (PARENT1 | PARENT2 | STALE)) { > + case PARENT1: > + state->parent1_count += delta; > + break; > + > + case PARENT2: > + state->parent2_count += delta; > + break; > + > + case PARENT1 | PARENT2: > + state->mb_candidate_count += delta; > + break; > + > + case PARENT1 | PARENT2 | STALE: > + break; > + > + default: > + BUG("unexpected paint state"); So, if anyone tries to refactor and adds a nonsense flag combination, e.g. PARENT1 | STALE, this will trip. Good. > + } > +} > + > +static void paint_queue_put(struct paint_state *state, > + struct commit *c, unsigned add_flags) > +{ > + unsigned old_flags = c->object.flags; > + c->object.flags |= add_flags; > + > + if (old_flags & ENQUEUED) { > + paint_count_update(state, old_flags, -1); > + paint_count_update(state, c->object.flags, 1); If this object was already in the queue, remove the old counters for it (e.g. PARENT1), and add the new union counters for it (e.g. PARENT1 | PARENT2). Good. > + } else { > + c->object.flags |= ENQUEUED; > + prio_queue_put(&state->queue, c); > + paint_count_update(state, c->object.flags, 1); ...and if it wasn't, put it in the queue and add the counters for it. Also good. > + } > } > > -static struct commit *nonstale_queue_get_dedup(struct nonstale_queue *queue) > +static struct commit *paint_queue_get(struct paint_state *state) > { > - struct commit *commit = nonstale_queue_get(queue); > + struct commit *commit = prio_queue_get(&state->queue); > + > + if (!commit) > + return NULL; > + > + commit->object.flags &= ~ENQUEUED; > + > + if (!state->parent1_count && !state->parent2_count && > + !state->mb_candidate_count) > + return NULL; > > - if (commit) > - commit->object.flags &= ~ENQUEUED; > + paint_count_update(state, commit->object.flags, -1); > return commit; > } So: pop, clear, check the counters, and _then_ decrement the counters. This means the zero-counter-check still include the just-popped commit. If the decrement were before the check, we'd actually just barely miss the merge-base most the time, so this order is important. > > @@ -109,18 +161,19 @@ static int paint_down_to_common(struct repository *r, > enum merge_base_flags mb_flags, > struct commit_list **result) > { > - struct nonstale_queue queue = { > - { compare_commits_by_gen_then_commit_date } > + struct paint_state state = { > + .queue = { compare_commits_by_gen_then_commit_date }, > + .gen_ordered = 1, > }; > + struct commit *commit; > int i; > - int gen_ordered = 1; > int steps = 0; > timestamp_t last_gen = GENERATION_NUMBER_INFINITY; > struct commit_list **tail = result; > > if (!min_generation && !corrected_commit_dates_enabled(r)) { > - queue.pq.compare = compare_commits_by_commit_date; > - gen_ordered = 0; > + state.queue.compare = compare_commits_by_commit_date; > + state.gen_ordered = 0; > } > > one->object.flags |= PARENT1; > @@ -128,15 +181,12 @@ static int paint_down_to_common(struct repository *r, > commit_list_append(one, result); > return 0; > } > - nonstale_queue_put_dedup(&queue, one); > + paint_queue_put(&state, one, 0); > > - for (i = 0; i < n; i++) { > - twos[i]->object.flags |= PARENT2; > - nonstale_queue_put_dedup(&queue, twos[i]); > - } > + for (i = 0; i < n; i++) > + paint_queue_put(&state, twos[i], PARENT2); > > - while (queue.max_nonstale) { > - struct commit *commit = nonstale_queue_get_dedup(&queue); > + while ((commit = paint_queue_get(&state))) { > struct commit_list *parents; > int flags; > timestamp_t generation = commit_graph_generation(commit); > @@ -162,7 +212,7 @@ static int paint_down_to_common(struct repository *r, > * descendant of this one. > */ > if (!(mb_flags & MERGE_BASE_FIND_ALL) && > - gen_ordered && > + state.gen_ordered && > generation < GENERATION_NUMBER_INFINITY) > break; > } > @@ -176,7 +226,7 @@ static int paint_down_to_common(struct repository *r, > if ((p->object.flags & flags) == flags) > continue; > if (repo_parse_commit(r, p)) { > - clear_nonstale_queue(&queue); > + clear_prio_queue(&state.queue); > commit_list_free(*result); > *result = NULL; > /* > @@ -191,12 +241,11 @@ static int paint_down_to_common(struct repository *r, > return error(_("could not parse commit %s"), > oid_to_hex(&p->object.oid)); > } > - p->object.flags |= flags; > - nonstale_queue_put_dedup(&queue, p); > + paint_queue_put(&state, p, flags); > } > } > > - clear_nonstale_queue(&queue); > + clear_prio_queue(&state.queue); > trace2_data_intmax("paint_down_to_common", r, > "steps", steps); > commit_list_sort_by_date(result); Looks like the straightforward translation in paint_down_to_common() from the old algorithm to the new adjustment; nice that a few spots actually become a little shorter. > diff --git a/t/t6600-test-reach.sh b/t/t6600-test-reach.sh > index 55aa220bb3..f9895f5fd7 100755 > --- a/t/t6600-test-reach.sh > +++ b/t/t6600-test-reach.sh > @@ -366,7 +366,7 @@ test_expect_success 'get_merge_bases_many:pending-stale' ' > git rev-parse ps-B > } >expect && > test_all_modes get_merge_bases_many && > - test_paint_down_steps 6 6 6 6 > + test_paint_down_steps 5 5 5 5 > ' > > test_expect_success 'get_merge_bases_many:infinity-both-sides' ' > @@ -381,7 +381,7 @@ test_expect_success 'get_merge_bases_many:infinity-both-sides' ' > git rev-parse pi-B > } >expect && > test_all_modes get_merge_bases_many && > - test_paint_down_steps 5 5 5 5 > + test_paint_down_steps 5 4 5 5 > ' > > test_expect_success 'setup mixed finite/INFINITY topology' ' > @@ -438,7 +438,7 @@ test_expect_success 'merge-base --all with clock skew and redundant ancestor (si > >input && > git rev-parse se2-MB1 >expect && > run_all_modes git merge-base --all se2-A se2-B && > - test_paint_down_steps 8 7 8 8 > + test_paint_down_steps 8 6 8 8 > ' > > test_expect_success 'reduce_heads' ' > -- > gitgitgadget Looks good.