Skip to content

git: worktree, hoist Config() calls out of per-file loops#1990

Merged
pjbgf merged 1 commit into
go-git:mainfrom
cedric-appdirect:hoist-config-calls
May 29, 2026
Merged

git: worktree, hoist Config() calls out of per-file loops#1990
pjbgf merged 1 commit into
go-git:mainfrom
cedric-appdirect:hoist-config-calls

Conversation

@cedric-appdirect

Copy link
Copy Markdown
Contributor

Summary

Cache the *config.Config result at each public entry point and thread it
through internal functions instead of repeatedly calling w.r.Config() for
each file during checkout, reset, status, and add operations.

Addresses Tier 1 item 1 from #1956: "Cache r.Config() result outside the
checkout file loop, pass it through as a parameter."

Problem

On the filesystem storage backend, each w.r.Config() call does:
file open, full file read, INI parse, struct alloc. During checkout,
Config() was called N+4 times (once per file in copyObjectToWorktree
plus overhead), where N is the number of files being checked out. The config
cannot change mid-operation, so all but the first call are wasted work.

Solution

Call w.r.Config() once at each public entry point (Reset,
StatusWithOptions, doAdd, AddGlob), then pass the resulting
*config.Config through all private functions in the call chain.

  • Reset() threads cfg through containsUnstagedChanges, resetWorktree,
    resetWorktreeToTree, checkoutChange, checkoutChangeRegularFile,
    checkoutFile, copyObjectToWorktree
  • StatusWithOptions() threads cfg through status, diffStagingWithWorktree,
    getSubmodulesStatus
  • doAdd() / AddGlob() threads cfg through doAddFile, doAddDirectory,
    copyFileToStorage, fillEncodedObjectFromFile
  • autoAddModifiedAndDeleted() threads cfg through doAddFile

The public Submodules() method keeps its existing signature and delegates
to a new private submodulesWithConfig(cfg) that internal callers use
directly.

No public API changes. No new struct fields. No cached state.

Config() calls per operation

Operation Before After
Reset(MergeReset) on N files N + 4 1
Reset(HardReset) on N files N + 2 1
Status() 2 1
Add on N files N 1
Submodules() (public, standalone) 1 1

Benchmark results

goos: darwin
goarch: arm64
cpu: Apple M4 Pro

                     | bench-before.txt |           bench-after.txt            |
                     |      sec/op      |    sec/op     vs base                |
CloneLargeRepo-12           6.093 +- 38%    1.946 +- 15%  -68.06% (p=0.000 n=10)
CloneDeepRepo-12            3.878 +- 72%    2.143 +-  7%  -44.75% (p=0.000 n=10)
Status/Clean-12            59.96m +-  3%   63.43m +-  7%        ~ (p=0.075 n=10)
Status/Modified-12         63.30m +-  4%   62.86m +-  4%        ~ (p=0.796 n=10)
StatusLarge/Clean-12       148.1m +-  2%   142.8m +-  3%   -3.53% (p=0.015 n=10)
geomean                    421.3m         298.6m        -29.12%

                     | bench-before.txt |           bench-after.txt           |
                     |       B/op       |     B/op      vs base               |
CloneLargeRepo-12          151.1Mi +- 0%   137.5Mi +- 0%  -9.02% (p=0.000 n=10)
CloneDeepRepo-12           163.7Mi +- 0%   150.1Mi +- 0%  -8.35% (p=0.000 n=10)
Status/Clean-12            5.497Mi +- 0%   5.497Mi +- 0%  -0.01% (p=0.043 n=10)
Status/Modified-12         5.581Mi +- 0%   5.574Mi +- 0%  -0.12% (p=0.015 n=10)
StatusLarge/Clean-12       12.68Mi +- 0%   12.68Mi +- 0%       ~ (p=0.118 n=10)
geomean                    24.93Mi        24.03Mi       -3.60%

                     | bench-before.txt |           bench-after.txt           |
                     |    allocs/op     |  allocs/op   vs base                |
CloneLargeRepo-12          1013.4k +- 0%   805.3k +- 0%  -20.54% (p=0.000 n=10)
CloneDeepRepo-12           1113.7k +- 0%   905.6k +- 0%  -18.69% (p=0.000 n=10)
Status/Clean-12             83.84k +- 0%   83.84k +- 0%        ~ (p=0.195 n=10)
Status/Modified-12          84.62k +- 0%   84.62k +- 0%        ~ (p=0.237 n=10)
StatusLarge/Clean-12        207.1k +- 0%   207.1k +- 0%        ~ (p=0.337 n=10)
geomean                     277.9k        254.7k        -8.37%

Known follow-up opportunities

A few internal paths still double-read Config because they call the public
w.Status() method (which has its own w.r.Config() call):

  • doAdd / AddGlob / autoAddModifiedAndDeleted call w.Status()
    internally
  • checkoutChangeSubmodule calls w.Submodule() which calls w.Submodules()
  • preloadStatus in status.go has its own Config() call

These are pre-existing and not regressions. Addressing them requires a
statusWithConfig private variant — a good candidate for a follow-up PR.

@cedric-appdirect

Copy link
Copy Markdown
Contributor Author

I was surprised by the speedup, but I guess it makes sense. The more files you are checking out, the more excessif amount of Config() read you are doing and removing it pay off a lot.

@cedric-appdirect cedric-appdirect force-pushed the hoist-config-calls branch 2 times, most recently from ad0bf6f to 5aa8b97 Compare May 1, 2026 16:21
@cedric-appdirect cedric-appdirect force-pushed the hoist-config-calls branch 2 times, most recently from 35c4af1 to 498a276 Compare May 19, 2026 15:58
Cache the *config.Config result and thread it through internal
functions instead of repeatedly calling w.r.Config() for each file
during checkout, reset, status, and add operations.

Previously, each worktree operation (Reset, Status, Add) called
Config() once per file plus overhead, resulting in N+4 disk I/O
operations (open, read, parse .git/config) where N is the number of
files. Now Config() is called once per public entry point and passed
to private functions via a new cfg parameter.

Changes:
- Reset() calls Config once; threaded through resetWorktreeToTree(),
  resetWorktree(), checkoutChange(), checkoutChangeRegularFile(),
  and checkoutFile()
- StatusWithOptions(), doAdd(), AddGlob() call Config once; threaded
  through diffStagingWithWorktree(), checkoutChange(), and related
  functions
- autoAddModifiedAndDeleted() passes cfg through checkoutFile()

No public API changes. No new struct fields.

Benchmark results (benchstat):
                     bench-before.txt  bench-after.txt
                          sec/op             sec/op
CloneLargeRepo-12         6.093 ± 38%    1.946 ± 15%  -68.06%
CloneDeepRepo-12          3.878 ± 72%    2.143 ±  7%  -44.75%
                          B/op              B/op
CloneLargeRepo-12         151.1Mi ± 0%   137.5Mi ± 0%  -9.02%
CloneDeepRepo-12          163.7Mi ± 0%   150.1Mi ± 0%  -8.35%
                       allocs/op        allocs/op
CloneLargeRepo-12        1013.4k ± 0%   805.3k ± 0%  -20.54%
CloneDeepRepo-12         1113.7k ± 0%   905.6k ± 0%  -18.69%

Assisted-by: OpenCode with Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Cedric BAIL <cedric.bail@appdirect.com>

@pjbgf pjbgf left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@cedric-appdirect thanks for working on this. 🙇

@pjbgf pjbgf merged commit 6923824 into go-git:main May 29, 2026
17 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants