Skip to content

Commit 3f4473e

Browse files
committed
connected: add incremental connectivity check via rev-list
The connectivity check uses rev-list to find commits reachable from the incoming tips but not from any local ref and then walks their object closure. Commit traversal stops at the connectivity boundary, but trees and blobs reachable from that boundary still need to be walked so they can be marked uninteresting, allocating a struct object for each one. On repositories where the boundary commits have large trees, the connectivity check for small incoming changes visits and tracks more objects than needed. Add an alternative connectivity check that verifies incoming commits incrementally against their parents. Instead of traversing the full boundary closure, the new check compares each new commit's tree with its parent trees. Already trusted entries are skipped, changed subtrees are descended into recursively, and blobs are checked for existence. This approach thus avoids descending into untouched subtrees. For example, consider a commit that changes one file under lib/ and also moves an unchanged subtree from src/ to dev/: Parent tree New tree +-- src/ (aaa) +-- dev/ (aaa) +-- lib/ (bbb) +-- lib/ (ccc) +-- foo.c (ddd) +-- foo.c (ddd) +-- bar.c (eee) +-- bar.c (fff) The verifier first scans the new root and collects aaa and ccc as work items. It then scans the parent root, publishing aaa and bbb into the trusted sets and recording bbb as the comparison base for ccc. When the work list is revisited, aaa is now trusted and skipped even though it appears at a different path. The verifier descends into ccc using bbb as its parent base. Scanning bbb similarly makes ddd and eee trusted, leaving only the new fff blob to be checked for existence. Thus neither the moved subtree nor any other unchanged subtree is recursively explored; only the changed lib/ subtree is descended into, and only the new bar.c blob needs an existence check. The root trees still need to be read and scanned as comparison bases. New commits are processed with ancestors before descendants. Parents outside the incoming commit set are on the already-connected side of the boundary and provide the initial trusted bases. Once an incoming commit has been verified, its tree can in turn be used as a trusted base for descendant commits. The verifier distinguishes trusted trees from expanded trees. A trusted tree can be accepted without further verification. An expanded tree has additionally published its direct non-gitlink entries into the trusted sets. Expanded parent trees therefore need not be read again for blob-only work, but may still be reread when recursive verification needs same-path parent subtrees. The implementation adds a --verify-trees-incremental flag to rev-list, following the same pattern as --exclude-promisor-objects: a pre-setup_revisions() scan sets the flag, the post-setup_revisions() option loop skips it, and the main traversal short-circuits into the incremental verifier after collecting commits from the revision walk. Because the incremental block eagerly consumes all commits via get_revision(), the subsequent mark_edges_uninteresting() and traverse_commit_list_filtered() calls naturally find no commit work to do and only process any non-commit tips (trees, blobs) left in revs.pending. For partial clones, missing promisor objects (trees and blobs promised by a promisor remote) are silently accepted during verification instead of triggering errors. The revision walk receives --exclude-promisor-objects so promisor commits do not enter the verification set. Gate the new algorithm behind transfer.connectivityCheck=incremental, keeping full as the default. Fall back to the full algorithm for deepening fetches to keep this commit easy to reason about, though incremental mode could potentially handle them as well. p5412 results (median of 3), scaling one dimension at a time. Each modified file is in a different directory, with directories chosen round-robin. Scaling tree size (10 commits, 10 files/commit): files full incr. full/incr 5K 0.01s 0.01s 1.1x 50K 0.04s 0.01s 2.2x 200K 0.14s 0.02s 5.7x 800K 0.60s 0.04s 14.8x The full mode must traverse the boundary tree closure, which grows with overall tree size. Incremental still scans the root trees, but avoids descending into unchanged subtrees, so it grows much more slowly with repository size. Scaling commit count (200K files, 10 files/commit): commits full incr. full/incr 1 0.14s 0.01s 7.5x 10 0.15s 0.02s 6.8x 100 0.16s 0.07s 2.2x 500 0.29s 0.29s 0.9x 3000 1.26s 1.67s 0.7x 5000 1.95s 2.71s 0.7x 10000 3.74s 6.02s 0.6x With many commits the per-commit overhead of scanning both the new and parent root trees accumulates and incremental becomes slower. Breakeven is around 500 commits and the ratio stabilizes near 0.6x for this fixture. Scaling files per commit (200K files, 10 commits): files/commit full incr. full/incr 1 0.15s 0.02s 7.5x 10 0.15s 0.02s 6.8x 100 0.15s 0.04s 3.1x 500 0.26s 0.16s 1.6x 1000 0.36s 0.35s 1.0x 2000 0.66s 0.79s 0.8x Breakeven is around 1000 files/commit. At 2000 files/commit (every directory touched), incremental is about 1.2x slower. For small repositories both modes are fast enough that the difference is difficult to measure reliably. Signed-off-by: Kristofer Karlsson <krka@spotify.com>
1 parent fa7f929 commit 3f4473e

12 files changed

Lines changed: 1057 additions & 0 deletions

Documentation/config/transfer.adoc

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,23 @@
1+
transfer.connectivityCheck::
2+
Choose which algorithm to use for the connectivity check
3+
performed during object transfer operations such as
4+
linkgit:git-fetch[1] and linkgit:git-receive-pack[1].
5+
The connectivity check verifies that all objects reachable
6+
from the incoming tips are available locally or, in a partial
7+
clone, promised by a promisor remote.
8+
The variants are as follows:
9+
+
10+
--
11+
`full` (default);;
12+
Walk the full object closure of the boundary commits.
13+
`incremental`;;
14+
Verify incoming commits by diffing their trees against parent
15+
trees, recursively descending only into entries that differ.
16+
The largest benefits occur when incoming commits change a
17+
small fraction of a large tree closure.
18+
Falls back to `full` for deepening fetches.
19+
--
20+
121
transfer.credentialsInUrl::
222
A configured URL can contain plaintext credentials in the form
323
`<protocol>://<user>:<password>@<domain>/<path>`. You may want

Documentation/rev-list-options.adoc

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1089,6 +1089,12 @@ we cannot get their Object ID though, an error will be raised.
10891089
stronger than `--missing=allow-promisor` because it limits the
10901090
traversal, rather than just silencing errors about missing
10911091
objects.
1092+
1093+
`--verify-trees-incremental`::
1094+
(For internal use only.) Verify tree connectivity
1095+
incrementally by comparing each commit's tree against its
1096+
parent trees. Used by `check_connected()` when
1097+
`transfer.connectivityCheck` is set to `incremental`.
10921098
endif::git-rev-list[]
10931099

10941100
`--no-walk[=(sorted|unsorted)]`::

Makefile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1357,6 +1357,7 @@ LIB_OBJS += trailer.o
13571357
LIB_OBJS += transport-helper.o
13581358
LIB_OBJS += transport.o
13591359
LIB_OBJS += tree-diff.o
1360+
LIB_OBJS += tree-verify.o
13601361
LIB_OBJS += tree-walk.o
13611362
LIB_OBJS += tree.o
13621363
LIB_OBJS += unpack-trees.o

builtin/rev-list.c

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
#include "commit-reach.h"
2929
#include "quote.h"
3030
#include "strbuf.h"
31+
#include "tree-verify.h"
3132

3233
struct rev_list_info {
3334
struct rev_info *revs;
@@ -706,6 +707,7 @@ int cmd_rev_list(int argc,
706707
int bisect_find_all = 0;
707708
int use_bitmap_index = 0;
708709
int filter_provided_objects = 0;
710+
int verify_trees_incremental = 0;
709711
const char *show_progress = NULL;
710712
int ret = 0;
711713

@@ -748,6 +750,8 @@ int cmd_rev_list(int argc,
748750
if (!strcmp(arg, "--exclude-promisor-objects")) {
749751
repo->fetch_if_missing = 0;
750752
revs.exclude_promisor_objects = 1;
753+
} else if (!strcmp(arg, "--verify-trees-incremental")) {
754+
verify_trees_incremental = 1;
751755
} else if (skip_prefix(arg, "--missing=", &arg)) {
752756
parse_missing_action_value(repo, arg);
753757
} else if (!strcmp(arg, "-z")) {
@@ -822,6 +826,8 @@ int cmd_rev_list(int argc,
822826

823827
if (!strcmp(arg, "--exclude-promisor-objects"))
824828
continue; /* already handled above */
829+
if (!strcmp(arg, "--verify-trees-incremental"))
830+
continue; /* already handled above */
825831
if (skip_prefix(arg, "--missing=", &arg))
826832
continue; /* already handled above */
827833

@@ -935,6 +941,18 @@ int cmd_rev_list(int argc,
935941

936942
prepare_maximal_independent(&revs);
937943

944+
if (verify_trees_incremental) {
945+
struct commit *commit;
946+
struct commit_list *new_commits = NULL;
947+
948+
while ((commit = get_revision(&revs)) != NULL)
949+
commit_list_insert(commit, &new_commits);
950+
951+
verify_commits_incremental(repo, &new_commits,
952+
revs.exclude_promisor_objects);
953+
commit_list_free(new_commits);
954+
}
955+
938956
if (revs.tree_objects)
939957
mark_edges_uninteresting(&revs, show_edge, 0);
940958

connected.c

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
#define USE_THE_REPOSITORY_VARIABLE
22

33
#include "git-compat-util.h"
4+
#include "config.h"
45
#include "gettext.h"
56
#include "hex.h"
67
#include "odb.h"
@@ -67,6 +68,26 @@ static int check_connected_promisor(oid_iterate_fn fn,
6768
return 1;
6869
}
6970

71+
static int incremental_check_applicable(struct check_connected_options *opt)
72+
{
73+
const char *algorithm = NULL;
74+
75+
if (repo_config_get_string_tmp(the_repository,
76+
"transfer.connectivitycheck",
77+
&algorithm))
78+
return 0;
79+
if (!strcasecmp(algorithm, "full"))
80+
return 0;
81+
if (strcasecmp(algorithm, "incremental"))
82+
die(_("unknown transfer.connectivityCheck algorithm '%s'"),
83+
algorithm);
84+
85+
if (opt->is_deepening_fetch)
86+
return 0;
87+
88+
return 1;
89+
}
90+
7091
/*
7192
* If we feed all the commits we want to verify to this command
7293
*
@@ -133,6 +154,9 @@ int check_connected(oid_iterate_fn fn, void *cb_data,
133154
if (opt->progress)
134155
strvec_pushf(&rev_list.args, "--progress=%s",
135156
_("Checking connectivity"));
157+
if (incremental_check_applicable(opt))
158+
strvec_push(&rev_list.args,
159+
"--verify-trees-incremental");
136160

137161
rev_list.git_cmd = 1;
138162
if (opt->env)

meson.build

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -562,6 +562,7 @@ libgit_sources = [
562562
'transport-helper.c',
563563
'transport.c',
564564
'tree-diff.c',
565+
'tree-verify.c',
565566
'tree-walk.c',
566567
'tree.c',
567568
'unpack-trees.c',

t/meson.build

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -652,6 +652,7 @@ integration_tests = [
652652
't5409-colorize-remote-messages.sh',
653653
't5410-receive-pack.sh',
654654
't5411-proc-receive-hook.sh',
655+
't5412-connectivity-check.sh',
655656
't5500-fetch-pack.sh',
656657
't5501-fetch-push-alternates.sh',
657658
't5502-quickfetch.sh',
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
#!/usr/bin/perl
2+
#
3+
# Generate a fast-import stream for p5412 connectivity check benchmarks.
4+
#
5+
# Usage: generate-repo-p5412-connectivity-check.perl
6+
# <dirs> <files_per_dir> <commits> [<hot_dirs>] [<files_per_commit>]
7+
#
8+
# Creates one initial commit with dirs*files_per_dir files, then
9+
# <commits> additional commits each modifying <files_per_commit>
10+
# files in directories chosen round-robin from 1..<hot_dirs>.
11+
12+
use strict;
13+
use warnings;
14+
15+
my ($nd, $nf, $nc, $hot, $fpc) = @ARGV;
16+
$hot = $nd if !$hot || $hot > $nd;
17+
$fpc = 1 if !$fpc;
18+
19+
sub data {
20+
printf "data %d\n%s\n", length($_[0]), $_[0];
21+
}
22+
23+
# Initial tree: one commit with nd*nf files.
24+
printf "commit refs/heads/main\n";
25+
printf "committer perf <perf\@test.com> now\n";
26+
data("initial");
27+
for my $d (1..$nd) {
28+
for my $f (1..$nf) {
29+
printf "M 100644 inline d-%04d/f-%03d\n", $d, $f;
30+
data(sprintf "%03d%03d", $d, $f);
31+
}
32+
}
33+
34+
# Subsequent commits (auto-chained by fast-import).
35+
for my $i (1..$nc) {
36+
printf "commit refs/heads/main\n";
37+
printf "committer perf <perf\@test.com> now\n";
38+
data(sprintf "change-%03d", $i);
39+
for my $j (0..$fpc-1) {
40+
my $d = (($i + $j) % $hot) + 1;
41+
printf "M 100644 inline d-%04d/f-001\n", $d;
42+
data(sprintf "c%d-%d", $i, $j);
43+
}
44+
}

t/perf/p5412-connectivity-check.sh

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
#!/bin/sh
2+
3+
test_description='performance of connectivity check modes
4+
5+
Compare the default and incremental rev-list connectivity modes
6+
directly, avoiding pack transfer noise.
7+
8+
Each repository has a flat tree of many directories with 100 files
9+
in each. Three axes are scaled independently: tree size, commit
10+
count, and files changed per commit.'
11+
12+
. ./perf-lib.sh
13+
14+
test_perf_fresh_repo
15+
16+
generate="$TEST_DIRECTORY/perf/generate-repo-p5412-connectivity-check.perl"
17+
18+
# $1=dirs $2=files_per_dir $3=commits $4=hot_dirs (optional, default=all)
19+
# $5=files_per_commit (optional, default=1)
20+
test_perf_conn () {
21+
local nd="$1" nf="$2" nc="$3" hot="${4:-$1}" fpc="${5:-1}"
22+
local total=$(($nd * $nf))
23+
local name="repo-${nd}d-${nf}f-${nc}c-${hot}h-${fpc}fpc"
24+
local label="${total} files, ${nc} commits"
25+
if test "$hot" -lt "$nd"
26+
then
27+
label="$label (${hot} hot dirs)"
28+
fi
29+
if test "$fpc" -gt 1
30+
then
31+
label="$label (${fpc} files/commit)"
32+
fi
33+
34+
test_expect_success "setup $label" '
35+
git init '"$name"' &&
36+
"$PERL_PATH" '"$generate"' '"$nd"' '"$nf"' '"$nc"' '"$hot"' '"$fpc"' |
37+
git -C '"$name"' fast-import --date-format=now --quiet &&
38+
(
39+
cd '"$name"' &&
40+
git rev-parse main~'"$nc"' >../'"${name}"'_old &&
41+
git rev-parse main >../'"${name}"'_new &&
42+
git update-ref refs/heads/main \
43+
$(cat ../'"${name}"'_old) &&
44+
git repack -ad &&
45+
git config gc.auto 0
46+
)
47+
'
48+
49+
test_perf "$label (full)" '
50+
cat '"${name}"'_new |
51+
git -C '"$name"' rev-list \
52+
--objects --stdin --not --all --quiet \
53+
--exclude-promisor-objects
54+
'
55+
56+
test_perf "$label (incremental)" '
57+
cat '"${name}"'_new |
58+
git -C '"$name"' rev-list --verify-trees-incremental \
59+
--objects --stdin --not --all --quiet \
60+
--exclude-promisor-objects
61+
'
62+
}
63+
64+
# Scaling tree size (10 commits, 10 files/commit).
65+
test_perf_conn 50 100 10 50 10
66+
test_perf_conn 500 100 10 500 10
67+
test_perf_conn 2000 100 10 2000 10
68+
test_perf_conn 8000 100 10 8000 10
69+
70+
# Scaling commit count (200K files, 10 files/commit).
71+
test_perf_conn 2000 100 1 2000 10
72+
test_perf_conn 2000 100 10 2000 10
73+
test_perf_conn 2000 100 100 2000 10
74+
test_perf_conn 2000 100 500 2000 10
75+
test_perf_conn 2000 100 3000 2000 10
76+
test_perf_conn 2000 100 5000 2000 10
77+
test_perf_conn 2000 100 10000 2000 10
78+
79+
# Scaling files per commit (200K files, 10 commits).
80+
test_perf_conn 2000 100 10 2000 1
81+
test_perf_conn 2000 100 10 2000 10
82+
test_perf_conn 2000 100 10 2000 100
83+
test_perf_conn 2000 100 10 2000 500
84+
test_perf_conn 2000 100 10 2000 1000
85+
test_perf_conn 2000 100 10 2000 2000
86+
87+
test_done

0 commit comments

Comments
 (0)