【问题标题】:How to troubleshoot an abnormally slow git-diff?如何对异常缓慢的 git-diff 进行故障排除?
【发布时间】:2019-12-22 16:51:23
【问题描述】:

我最近克隆了一个远程仓库,其中一些 git 命令运行非常缓慢。例如,运行

git diff --quiet

...大约需要 40 秒。 (对于它的价值,回购是干净的。我使用的是git 2.20.1 版。)

在试图找出导致这种迟缓的原因时,我遇到了一些取消它的程序,尽管我不知道为什么。

在这些过程中,我发现的最简单/最快的一个是:(从一个新克隆的 repo 实例开始)从master 创建一个分支,然后将其签出。在此之后,如果我再次检查 master,现在 git diff --quiet 会很快完成(不到 50 毫秒)。

下面是一个示例交互,显示了各种操作的时间信息1

rm -rf ./"$REPONAME"      #  0.174 s
git clone "$URL"          # 54.118 s
cd ./"$REPONAME"          #  0.007 s

git diff --quiet          # 39.438 s

git branch VOODOO         #  0.032 s
git checkout VOODOO       # 31.247 s
git diff --quiet          #  0.014 s

git checkout master       #  0.034 s
git diff --quiet          #  0.012 s

正如我已经强调的那样,这只是“修复”回购的几种可能程序之一,它们对我来说同样神秘。这只是我发现的最简单/最快的一个。

上述时序序列非常可重复(即,每次运行该特定序列时,我得到的时序大致相同)。

然而,它对看似微小的变化非常敏感。例如,如果我将git branch VOODOO; git checkout VOODOO 替换为git checkout -b VOODOO,随后的时序配置文件将发生根本性的变化:

rm -rf ./"$REPONAME"      #  0.015 s
git clone "$URL"          # 45.312 s
cd ./"$REPONAME"          #  0.007 s

git diff --quiet          # 46.145 s

git checkout -b VOODOO    # 42.363 s
git diff --quiet          # 41.180 s

git checkout master       # 47.345 s
git diff --quiet          #  0.018 s

我想弄清楚发生了什么。如何进一步解决问题?

是否有永久(“可提交”)方式来“修复”回购? (通过“修复”我的意思是:摆脱git diff --quietgit checkout ... 等的长时间延迟)

(顺便说一句,git gc 不会修复 repo,即使是暂时的;我试过了。)

我认为最终“修复”回购的是git 开始构建并缓存一些辅助数据结构,使其能够执行某些操作有效率的。如果这个假设是正确的,那么我的问题可以改写为:导致git构建这种辅助数据结构的最直接的方法是什么?


编辑: 可以说明上述情况的另外一点信息是,此存储库包含一个异常大 (1GB) 的文件。 (这就解释了git clone这一步的慢,不知道是不是和git diff --quiet等的慢有关系,如果有,是怎么回事。)


1不用说,我已将分支命名为VOODOO,以反映我对正在发生的事情的无知。

【问题讨论】:

  • git clone 似乎与其他长期操作处于同一水平。你对git clone 也需要很长时间感到惊讶吗?当您查看它的进度指示器时,它大部分时间都花在了哪里?
  • @j6t:确实,克隆速度非常慢。我对我的帖子进行了编辑,阐明了这一点。简而言之,克隆大部分时间都在“接收对象”阶段。
  • 您是在命令行上键入示例命令,即命令之间有一些延迟,还是从脚本中调用它们以便它们立即运行?另外,如果您背靠背运行两个git diff 会发生什么?当第一个慢时,第二个是快还是慢?
  • @j6t:我使用脚本来运行命令,以确保可重复性。如果我在原始序列中的每个git diff --quiet 命令之后立即添加第二个git diff --quiet 命令,我发现每对的第二个git diff --quiet 命令与(该对的)第一个命令所花费的时间大致相同。
  • 这或多或少排除了需要扫描工作树数据的“racily clean index”情况。 (在这种情况下,第二个git diff 会很快。)我建议您收集更多数据。例如,如果您使用的是 Linux 或类似系统,请运行 strace -f -tt -e file,clone,execve -o /tmp/git.strace git diff --quiet 并检查 /tmp/git.strace 中的时间戳。也许它揭示了时间花在哪里。

标签: git git-diff


【解决方案1】:

首先检查 Git 2.27 甚至即将推出的 2.28(2020 年第三季度)问题是否仍然存在

我会使用GIT_TRACE2_PERF 来衡量任何性能。 (如I did here

使用 Git 2.28(2020 年第三季度),在具有太多 stat 不匹配路径的工作树中“diff --quiet”期间的内存使用量已大大减少。

它的补丁描述说明了一个“diff --quiet”可能很慢的用例:​​

参见Jeff King (peff)commit d2d7fbe(2020 年 6 月 1 日)。
(由 Junio C Hamano -- gitster -- 合并于 commit 0cd0afc,2020 年 6 月 18 日)

diff: 从 stat-unmatched 对中丢弃 blob 数据

报告人:Jan Christoph Uhde
签字人:Jeff King

在对工作树执行树级差异时,我们可能会发现索引统计信息是脏的,因此我们将文件对排队等待稍后检查。
如果实际内容没有改变,我们称之为stat-unmatch;统计信息已过时,但没有实际差异。

通常diffcore_std() 会通过diffcore_skip_stat_unmatch() 检测并删除这些相同的文件对。

但是,当使用“--quiet”时,我们希望在看到任何更改后立即停止差异,因此我们会立即在 diff_change() 中检查 stat-unmatches。

该检查可能需要我们将文件内容实际加载到 diff_filespecs 对中。
如果我们发现这对不是统计不匹配的,那么没什么大不了的;无论如何,我们可能会稍后加载内容以生成补丁,进行重命名检测等,因此我们希望保留它。
但如果它是一个不匹配的统计数据,那么我们就不再使用该数据了;关键是我们要丢弃这对。但是,我们从不释放分配的 diff_filespec 数据。

在大多数情况下,保留这些数据不是问题。我们预计不会有很多 stat-unmatch 条目,而且由于我们使用的是--quiet,所以无论如何我们一看到这种真正的变化就会退出。

但是,在某些极端情况下它会产生很大的不同:

  1. 我们通常会 mmap() 对中的一半工作树。
    而且由于操作系统可能会限制地图的总数,我们可以在大型存储库中与此冲突。例如:

     $ cd linux
    $ git ls-files | wc -l
    67959
    $ sysctl vm.max_map_count
    vm.max_map_count = 65530
    $ git ls-files | xargs touch ;# everything is stat-dirty!
    $ git diff --quiet
    fatal: mmap failed: Cannot allocate memory
    

拥有如此多的stat-dirty文件应该是不寻常的,但如果您只是运行类似“sed -i”或类似的脚本,这是可能的。

在这个补丁之后,上面的代码正确退出,代码为 0。

  1. 即使您没有达到 mmap 限制,该对的一半索引也会从对象数据库中拉到堆内存中。
    再次在linux.git 的克隆中,运行:

    $ git ls-files | head -n 10000 | xargs touch
    $ git diff --quiet
    

此补丁之前的堆峰值为 145MB,之后为 94MB。

此补丁通过释放我们在 diff_changes 中的“--quiet”统计不匹配检查期间获取的任何 diff_filespec 数据来解决问题。
以后没有人会需要这些数据,因此保留它没有意义。
有几点需要注意:

  • 我们可以完全跳过对这对的排队,理论上这可以节省一些工作。但是没有什么可以节省的,因为无论如何我们都需要一个diff_filepair 来提供给diff_filespec_check_stat_unmatch()
    而且由于我们缓存了stat-unmatch 检查的结果,以后对diffcore_skip_stat_unmatch() 的调用将很快跳过它们。
    diffcore 代码在删除它们时还会计算 stat-unmatched 对的数量。任何调用者都会关心与--quiet 结合使用,这是值得怀疑的,但为了安全起见,我们必须重新实现这里的逻辑。所以这真的不值得麻烦。

  • 我没有编写测试,因为我们总是产生正确的输出,除非我们遇到系统 mmap 限制,这是 bot 不可移植且测试起来昂贵。测量峰值堆会很有趣,但我们的性能套件还不能做到这一点。

  • 请注意,没有“--quiet”的差异不会遇到同样的问题。在diffcore_skip_stat_unmatch() 中,我们检测到stat-unmatch 条目并立即删除它们,因此我们不会携带它们的数据。

  • 如果您确实有这么多具有​​实际更改的文件,您可以仍然触发mmap 限制问题。但这不太可能。如果大小不匹配,stat-unmatch 检查会避免加载文件内容,因此您需要对每个文件进行非常细微的更改。
    同样,不精确的重命名检测可能会同时加载许多文件的数据。但是您不仅需要 64k 的更改,还需要大量的删除和添加。最有可能的候选者可能是中断检测,它将加载所有对的数据并将其保留在内容级别差异中。但同样,您首先需要 64k 实际更改的文件。

所以仍然有可能触发这种情况,但似乎“我不小心把我的所有文件都弄脏了”是现实世界中最有可能发生的情况。


在 Git 2.30(2021 年第一季度)、“git diff(man) 和其他共享相同机制的命令以与工作树文件进行比较时,已学会利用fsmonitor 可用的数据。

请参阅commit 2bfa953commit 471b115commit ed5a245commit 89afd5fcommit 5851462commit dc69d47(2020 年 10 月 20 日)Nipunn Koorapati (nipunn1313)
请参阅 Alex Vandiver (alexmv)commit c9052a8(2020 年 10 月 20 日)。
(由 Junio C Hamano -- gitster -- 合并于 commit bf69da5,2020 年 11 月 9 日)

t/perf:为git diff 添加fsmonitor 性能测试

签字人:Nipunn Koorapati

parent-rev 补丁中 git-diff fsmonitor 优化的结果(使用 400k 文件 repo 进行测试)

正如您在此处看到的 - git diff(man) 使用此补丁系列运行 fsmonitor 明显更好(我的工作负载快 80%)!

GIT_PERF_LARGE_REPO=~/src/server ./run v2.29.0-rc1 。 -- p7519-fsmonitor.sh

Test                                                                     v2.29.0-rc1       this tree
-----------------------------------------------------------------------------------------------------------------
7519.2: status (fsmonitor=.git/hooks/fsmonitor-watchman)                 1.46(0.82+0.64)   1.47(0.83+0.62) +0.7%
7519.3: status -uno (fsmonitor=.git/hooks/fsmonitor-watchman)            0.16(0.12+0.04)   0.17(0.12+0.05) +6.3%
7519.4: status -uall (fsmonitor=.git/hooks/fsmonitor-watchman)           1.36(0.73+0.62)   1.37(0.76+0.60) +0.7%
7519.5: diff (fsmonitor=.git/hooks/fsmonitor-watchman)                   0.85(0.22+0.63)   0.14(0.10+0.05) -83.5%
7519.6: diff -- 0_files (fsmonitor=.git/hooks/fsmonitor-watchman)        0.12(0.08+0.05)   0.13(0.11+0.02) +8.3%
7519.7: diff -- 10_files (fsmonitor=.git/hooks/fsmonitor-watchman)       0.12(0.08+0.04)   0.13(0.09+0.04) +8.3%
7519.8: diff -- 100_files (fsmonitor=.git/hooks/fsmonitor-watchman)      0.12(0.07+0.05)   0.13(0.07+0.06) +8.3%
7519.9: diff -- 1000_files (fsmonitor=.git/hooks/fsmonitor-watchman)     0.12(0.09+0.04)   0.13(0.08+0.05) +8.3%
7519.10: diff -- 10000_files (fsmonitor=.git/hooks/fsmonitor-watchman)   0.14(0.09+0.05)   0.13(0.10+0.03) -7.1%
7519.12: status (fsmonitor=)                                             1.67(0.93+1.49)   1.67(0.99+1.42) +0.0%
7519.13: status -uno (fsmonitor=)                                        0.37(0.30+0.82)   0.37(0.33+0.79) +0.0%
7519.14: status -uall (fsmonitor=)                                       1.58(0.97+1.35)   1.57(0.86+1.45) -0.6%
7519.15: diff (fsmonitor=)                                               0.34(0.28+0.83)   0.34(0.27+0.83) +0.0%
7519.16: diff -- 0_files (fsmonitor=)                                    0.09(0.06+0.04)   0.09(0.08+0.02) +0.0%
7519.17: diff -- 10_files (fsmonitor=)                                   0.09(0.07+0.03)   0.09(0.06+0.05) +0.0%
7519.18: diff -- 100_files (fsmonitor=)                                  0.09(0.06+0.04)   0.09(0.06+0.04) +0.0%
7519.19: diff -- 1000_files (fsmonitor=)                                 0.09(0.06+0.04)   0.09(0.05+0.05) +0.0%
7519.20: diff -- 10000_files (fsmonitor=)                                0.10(0.08+0.04)   0.10(0.06+0.05) +0.0%

我还为带有路径规范的小型 git diff(man) 工作负载添加了基准。 我看到增加了大约 0.02 秒的开销,不带和不带 fsmonitor

通过查看这些结果,我怀疑refresh_fsmonitor 已经在git diff 期间发生了(man) - 与此补丁系列的优化无关。
通过打破refresh_fsmonitor 证实了这种怀疑。

(gdb) bt  [simplified]

【讨论】:

  • 这怎么只有一个赞!这么多的信息和研究。让我们解决这个问题...
  • @Codebling 功劳归 Jeff King、Nipunn Koorapati 和 Alex Vandiver 所有。我刚刚报告了他们的发现。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2010-09-08
  • 2018-07-14
  • 2012-08-07
  • 2011-02-07
相关资源
最近更新 更多