From 800ec5d9797d5e12949f787d729a31a9072205a6 Mon Sep 17 00:00:00 2001 From: Nathan Manceaux-Panot Date: Wed, 4 Jun 2025 12:33:47 +0200 Subject: [PATCH] Make reloadHistory tip hiding much faster MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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! --- GitUpKit/Core/GCHistory.m | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/GitUpKit/Core/GCHistory.m b/GitUpKit/Core/GCHistory.m index aa5f2f98..eba8628f 100644 --- a/GitUpKit/Core/GCHistory.m +++ b/GitUpKit/Core/GCHistory.m @@ -901,20 +901,18 @@ - (BOOL)_reloadHistory:(GCHistory*)history // Configure commit tree walker to start from tips CALL_LIBGIT2_FUNCTION_GOTO(cleanup, git_revwalk_new, &walker, self.private); git_revwalk_sorting(walker, GIT_SORT_NONE); + git_revwalk_add_hide_block(walker, ^int(const git_oid *commit_id) { + return CFDictionaryContainsKey(lookup, commit_id); + }); if (historyTips) { for (GCCommit* tip in tips) { if (![historyTips containsObject:tip]) { const git_oid* oid = git_commit_id(tip.private); - if (CFDictionaryContainsKey(lookup, oid)) { - CALL_LIBGIT2_FUNCTION_GOTO(cleanup, git_revwalk_hide, walker, oid); - } else { + if (!CFDictionaryContainsKey(lookup, oid)) { CALL_LIBGIT2_FUNCTION_GOTO(cleanup, git_revwalk_push, walker, oid); } } } - for (GCCommit* historyTip in historyTips) { - CALL_LIBGIT2_FUNCTION_GOTO(cleanup, git_revwalk_hide, walker, git_commit_id(historyTip.private)); - } } else { for (GCCommit* tip in tips) { CALL_LIBGIT2_FUNCTION_GOTO(cleanup, git_revwalk_push, walker, git_commit_id(tip.private)); @@ -929,9 +927,6 @@ - (BOOL)_reloadHistory:(GCHistory*)history break; } CHECK_LIBGIT2_FUNCTION_CALL(goto cleanup, status, == GIT_OK); - if (historyTips && CFDictionaryContainsKey(lookup, &oid)) { - continue; - } git_commit* walkCommit; CALL_LIBGIT2_FUNCTION_GOTO(cleanup, git_commit_lookup, &walkCommit, self.private, &oid); GCHistoryCommit* commit = [[GCHistoryCommit alloc] initWithRepository:self commit:walkCommit autoIncrementID:nextAutoIncrementID++];