Skip to content

Commit 6ff87d1

Browse files
committed
ci: parallelize testing on Windows
The fact that Git's test suite is implemented in Unix shell script that is as portable as we can muster, combined with the fact that Unix shell scripting is foreign to Windows (and therefore has to be emulated), results in pretty abysmal speed of the test suite on that platform, for pretty much no other reason than that language choice. For comparison: while the Linux build & test is typically done within about 8 minutes, the Windows build & test typically lasts about 80 minutes in Azure Pipelines. To help with that, let's use the Azure Pipeline feature where you can parallelize jobs, make jobs depend on each other, and pass artifacts between them. The tests are distributed using the following heuristic: listing all test scripts ordered by size in descending order (as a cheap way to estimate the overall run time), every Nth script is run (where N is the total number of parallel jobs), starting at the index corresponding to the parallel job. This slicing is performed by a new function that is added to the `test-tool`. To optimize the overall runtime of the entire Pipeline, we need to move the Windows jobs to the beginning (otherwise there would be a very decent chance for the Pipeline to be run only the Windows build, while all the parallel Windows test jobs wait for this single one). We use Azure Pipelines Artifacts for both the minimal Git for Windows SDK as well as the built executables, as deduplication and caching close to the agents makes that really fast. For comparison: while downloading and unpacking the minimal Git for Windows SDK via PowerShell takes only one minute (down from anywhere between 2.5 to 7 when using a shallow clone), uploading it as Pipeline Artifact takes less than 30s and downloading and unpacking less than 20s (sometimes even as little as only twelve seconds). Signed-off-by: Johannes Schindelin <[email protected]>
1 parent b39e165 commit 6ff87d1

File tree

5 files changed

+141
-8
lines changed

5 files changed

+141
-8
lines changed

Makefile

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2927,6 +2927,16 @@ rpm::
29272927
@false
29282928
.PHONY: rpm
29292929

2930+
artifacts-tar:: $(ALL_PROGRAMS) $(SCRIPT_LIB) $(BUILT_INS) $(OTHER_PROGRAMS) \
2931+
GIT-BUILD-OPTIONS $(TEST_PROGRAMS) $(test_bindir_programs) \
2932+
$(NO_INSTALL) $(MOFILES)
2933+
$(QUIET_SUBDIR0)templates $(QUIET_SUBDIR1) \
2934+
SHELL_PATH='$(SHELL_PATH_SQ)' PERL_PATH='$(PERL_PATH_SQ)'
2935+
test -n "$(ARTIFACTS_DIRECTORY)"
2936+
mkdir -p "$(ARTIFACTS_DIRECTORY)"
2937+
$(TAR) czf "$(ARTIFACTS_DIRECTORY)/artifacts.tar.gz" $^ templates/blt/
2938+
.PHONY: artifacts-tar
2939+
29302940
htmldocs = git-htmldocs-$(GIT_VERSION)
29312941
manpages = git-manpages-$(GIT_VERSION)
29322942
.PHONY: dist-doc distclean

azure-pipelines.yml

Lines changed: 71 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@ resources:
33
fetchDepth: 1
44

55
jobs:
6-
- job: windows
7-
displayName: Windows
6+
- job: windows_build
7+
displayName: Windows Build
88
condition: succeeded()
99
pool: Hosted
1010
timeoutInMinutes: 240
@@ -30,21 +30,84 @@ jobs:
3030
displayName: 'Download git-sdk-64-minimal'
3131
- powershell: |
3232
& git-sdk-64-minimal\usr\bin\bash.exe -lc @"
33-
export DEVELOPER=1
34-
export NO_PERL=1
35-
export NO_SVN_TESTS=1
36-
export GIT_TEST_SKIP_REBASE_P=1
33+
ci/make-test-artifacts.sh artifacts
34+
"@
35+
if (!$?) { exit(1) }
36+
displayName: Build
37+
env:
38+
HOME: $(Build.SourcesDirectory)
39+
MSYSTEM: MINGW64
40+
DEVELOPER: 1
41+
NO_PERL: 1
42+
- task: PublishPipelineArtifact@0
43+
displayName: 'Publish Pipeline Artifact: test artifacts'
44+
inputs:
45+
artifactName: 'windows-artifacts'
46+
targetPath: '$(Build.SourcesDirectory)\artifacts'
47+
- task: PublishPipelineArtifact@0
48+
displayName: 'Publish Pipeline Artifact: git-sdk-64-minimal'
49+
inputs:
50+
artifactName: 'git-sdk-64-minimal'
51+
targetPath: '$(Build.SourcesDirectory)\git-sdk-64-minimal'
52+
- powershell: |
53+
if ("$GITFILESHAREPWD" -ne "" -and "$GITFILESHAREPWD" -ne "`$`(gitfileshare.pwd)") {
54+
cmd /c rmdir "$(Build.SourcesDirectory)\test-cache"
55+
}
56+
displayName: 'Unmount test-cache'
57+
condition: true
58+
env:
59+
GITFILESHAREPWD: $(gitfileshare.pwd)
60+
61+
- job: windows_test
62+
displayName: Windows Test
63+
dependsOn: windows_build
64+
condition: succeeded()
65+
pool: Hosted
66+
timeoutInMinutes: 240
67+
strategy:
68+
parallel: 10
69+
steps:
70+
- powershell: |
71+
if ("$GITFILESHAREPWD" -ne "" -and "$GITFILESHAREPWD" -ne "`$`(gitfileshare.pwd)") {
72+
net use s: \\gitfileshare.file.core.windows.net\test-cache "$GITFILESHAREPWD" /user:AZURE\gitfileshare /persistent:no
73+
cmd /c mklink /d "$(Build.SourcesDirectory)\test-cache" S:\
74+
}
75+
displayName: 'Mount test-cache'
76+
env:
77+
GITFILESHAREPWD: $(gitfileshare.pwd)
78+
- task: DownloadPipelineArtifact@0
79+
displayName: 'Download Pipeline Artifact: test artifacts'
80+
inputs:
81+
artifactName: 'windows-artifacts'
82+
targetPath: '$(Build.SourcesDirectory)'
83+
- task: DownloadPipelineArtifact@0
84+
displayName: 'Download Pipeline Artifact: git-sdk-64-minimal'
85+
inputs:
86+
artifactName: 'git-sdk-64-minimal'
87+
targetPath: '$(Build.SourcesDirectory)\git-sdk-64-minimal'
88+
- powershell: |
89+
& git-sdk-64-minimal\usr\bin\bash.exe -lc @"
90+
test -f artifacts.tar.gz || {
91+
echo No test artifacts found\; skipping >&2
92+
exit 0
93+
}
94+
tar xf artifacts.tar.gz || exit 1
95+
96+
# Let Git ignore the SDK and the test-cache
97+
printf '%s\n' /git-sdk-64-minimal/ /test-cache/ >>.git/info/exclude
3798
38-
ci/run-build-and-tests.sh || {
99+
ci/run-test-slice.sh `$SYSTEM_JOBPOSITIONINPHASE `$SYSTEM_TOTALJOBSINPHASE || {
39100
ci/print-test-failures.sh
40101
exit 1
41102
}
42103
"@
43104
if (!$?) { exit(1) }
44-
displayName: 'Build & Test'
105+
displayName: 'Test (parallel)'
45106
env:
46107
HOME: $(Build.SourcesDirectory)
47108
MSYSTEM: MINGW64
109+
NO_SVN_TESTS: 1
110+
GIT_TEST_SKIP_REBASE_P: 1
48111
- powershell: |
49112
if ("$GITFILESHAREPWD" -ne "" -and "$GITFILESHAREPWD" -ne "`$`(gitfileshare.pwd)") {
50113
cmd /c rmdir "$(Build.SourcesDirectory)\test-cache"

ci/make-test-artifacts.sh

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
#!/bin/sh
2+
#
3+
# Build Git and store artifacts for testing
4+
#
5+
6+
mkdir -p "$1" # in case ci/lib.sh decides to quit early
7+
8+
. ${0%/*}/lib.sh
9+
10+
make artifacts-tar ARTIFACTS_DIRECTORY="$1"
11+
12+
check_unignored_build_artifacts

ci/run-test-slice.sh

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
#!/bin/sh
2+
#
3+
# Test Git in parallel
4+
#
5+
6+
. ${0%/*}/lib.sh
7+
8+
case "$CI_OS_NAME" in
9+
windows*) cmd //c mklink //j t\\.prove "$(cygpath -aw "$cache_dir/.prove")";;
10+
*) ln -s "$cache_dir/.prove" t/.prove;;
11+
esac
12+
13+
make --quiet -C t T="$(cd t &&
14+
./helper/test-tool path-utils slice-tests "$1" "$2" t[0-9]*.sh |
15+
tr '\n' ' ')"
16+
17+
check_unignored_build_artifacts

t/helper/test-path-utils.c

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,14 @@ static int is_dotgitmodules(const char *path)
177177
return is_hfs_dotgitmodules(path) || is_ntfs_dotgitmodules(path);
178178
}
179179

180+
static int cmp_by_st_size(const void *a, const void *b)
181+
{
182+
intptr_t x = (intptr_t)((struct string_list_item *)a)->util;
183+
intptr_t y = (intptr_t)((struct string_list_item *)b)->util;
184+
185+
return x > y ? -1 : (x < y ? +1 : 0);
186+
}
187+
180188
int cmd__path_utils(int argc, const char **argv)
181189
{
182190
if (argc == 3 && !strcmp(argv[1], "normalize_path_copy")) {
@@ -324,6 +332,29 @@ int cmd__path_utils(int argc, const char **argv)
324332
return 0;
325333
}
326334

335+
if (argc > 5 && !strcmp(argv[1], "slice-tests")) {
336+
int res = 0;
337+
long offset, stride, i;
338+
struct string_list list = STRING_LIST_INIT_NODUP;
339+
struct stat st;
340+
341+
offset = strtol(argv[2], NULL, 10);
342+
stride = strtol(argv[3], NULL, 10);
343+
if (stride < 1)
344+
stride = 1;
345+
for (i = 4; i < argc; i++)
346+
if (stat(argv[i], &st))
347+
res = error_errno("Cannot stat '%s'", argv[i]);
348+
else
349+
string_list_append(&list, argv[i])->util =
350+
(void *)(intptr_t)st.st_size;
351+
QSORT(list.items, list.nr, cmp_by_st_size);
352+
for (i = offset; i < list.nr; i+= stride)
353+
printf("%s\n", list.items[i].string);
354+
355+
return !!res;
356+
}
357+
327358
fprintf(stderr, "%s: unknown function name: %s\n", argv[0],
328359
argv[1] ? argv[1] : "(there was none)");
329360
return 1;

0 commit comments

Comments
 (0)