Skip to content

Make reloadHistory tip hiding much faster #2707

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: master
Choose a base branch
from

Conversation

Cykelero
Copy link
Contributor

@Cykelero Cykelero commented Jun 4, 2025

Summary: Reloading the history in repos with many commits and many branches can be problematically slow. This commit changes the way commits are hidden from libgit2's revwalk, making the process vastly faster. (in the Swift repo, goes from roughly 3.2s to just 70ms)

Intro

Whenever the repository's history changes (either externally, or after manipulating commits within the app itself), GitUpKit's GCLiveRepository updates its internal cache of the repo's history. Since this process occurs very often, including when reacting to user input, it needs to be extremely fast.

To a large degree, it already is! But on repos with many commits and many branches (e.g. the Swift repo), history reloads would take very long (e.g. around three seconds on a M1 Pro MBP).

Context

To update its history cache, GUK asks libgit2 to walk over new commits.

When using libgit2's revision walker, you provide it with a list of tips to explore (“I want callbacks for all parents of that tip”) and a list of tips to ignore (“do not tell me about the parents of that tip, if they ever come up while exploring”). Intuitively, the more you add to that ignore list, the faster the iteration. In practice, the opposite was true!

When first starting a walk, libgit2 iterates over every parent of every ignored tip, to mark these commits as ignored: this way, if they come up later, while exploring the parents of the interesting tips, it'll know not to return them.

What this means is that at best, adding ignored tips has no effect on performance (their parents will be walked anyway) and at worse, they give a lot more work to libgit2. This is what was happening here.

Solution

Thankfully there's an incredibly, almost suspiciously-simple solution: libgit2 revision walkers can be provided an ignore callback. The walker invokes the ignore callback for every parent it explores, to ask whether that commit should be ignored. It's a second way of ignoring commits, except this one causes no extra iteration: the burden is on us, the callback provider, to respond performantly.

As it turns out, GitUpKit already has the perfect solution for this, as it keeps a map of all currently known commits, indexed by OID. This allows us to very quickly skip entire parent hierarchies, as libgit2 does not explore the parents of ignored commits, either.

In informal tests on the Swift repo, this change causes the history reload to go from roughly 3.2 seconds to just 70 milliseconds—that's a 45× speedup!

Summary: Reloading the history in repos with many commits and many branches can be problematically slow. This commit changes the way commits are hidden from libgit2's revwalk, making the process vastly faster. (in the Swift repo, goes from roughly 3.2s to just 70ms)

# Intro

Whenever the repository's history changes (either externally, or after manipulating commits within the app itself), GitUpKit's GCLiveRepository updates its internal cache of the repo's history. Since this process occurs very often, including when reacting to user input, it needs to be extremely fast.

To a large degree, it already is! But on repos with many commits *and* many branches (e.g. the Swift repo), history reloads would take very long (e.g. around three seconds on a M1 Pro MBP).

# Context

To update its history cache, GUK asks libgit2 to walk over new commits.

When using libgit2's revision walker, you provide it with a list of tips to explore (“I want callbacks for all parents of that tip”) and a list of tips to ignore (“do *not* tell me about the parents of that tip, if they ever come up while exploring”).
Intuitively, the more you add to that ignore list, the faster the iteration. In practice, the opposite was true!

When first starting a walk, libgit2 _iterates over every parent of every ignored tip_, to mark these commits as ignored: this way, if they come up later, while exploring the parents of the interesting tips, it'll know *not* to return them.

What this means is that at best, adding ignored tips has no effect on performance (their parents will be walked anyway) and at worse, they give a lot more work to libgit2. This is what was happening here.

# Solution

Thankfully there's an incredibly, almost suspiciously-simple solution: libgit2 revision walkers can be provided an _ignore callback_. The walker invokes the ignore callback for every parent it explores, to ask whether that commit should be ignored. It's a second way of ignoring commits, except this one causes no extra iteration: the burden is on us, the callback provider, to respond performantly.

As it turns out, GitUpKit already has the perfect solution for this, as it keeps a map of all currently known commits, indexed by OID. This allows us to very quickly skip entire parent hierarchies, as libgit2 does not explore the parents of ignored commits, either.

In informal tests on the Swift repo, this change causes the history reload to go from roughly 3.2 seconds to just 70 milliseconds—that's a 45× speedup!
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Development

Successfully merging this pull request may close these issues.

1 participant