Skip to content

Commit 389f979

Browse files
steveyeggeosamu2001claude
committed
perf: cache doctor beads dir resolution (PR #2643)
Cache ResolveBeadsDirForRepo results so the git worktree fallback lookup runs once per doctor invocation instead of 28 times. Co-Authored-By: osamu2001 <osamu2001@users.noreply.github.com> Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent fab29b0 commit 389f979

2 files changed

Lines changed: 84 additions & 2 deletions

File tree

cmd/bd/doctor/backend.go

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,14 @@ import (
55
"os/exec"
66
"path/filepath"
77
"strings"
8+
"sync"
89

910
"github.com/steveyegge/beads/internal/configfile"
1011
"github.com/steveyegge/beads/internal/utils"
1112
)
1213

14+
var resolveBeadsDirCache sync.Map
15+
1316
// getBackendAndBeadsDir resolves the effective .beads directory (following redirects)
1417
// and returns the configured storage backend ("dolt" by default).
1518
func getBackendAndBeadsDir(repoPath string) (backend string, beadsDir string) {
@@ -23,10 +26,17 @@ func getBackendAndBeadsDir(repoPath string) (backend string, beadsDir string) {
2326
}
2427

2528
func ResolveBeadsDirForRepo(repoPath string) string {
26-
return resolveDoctorBeadsDir(repoPath)
29+
cacheKey := utils.CanonicalizePath(repoPath)
30+
if resolved, ok := resolveBeadsDirCache.Load(cacheKey); ok {
31+
return resolved.(string)
32+
}
33+
34+
resolved := resolveBeadsDirForRepoUncached(repoPath)
35+
resolveBeadsDirCache.Store(cacheKey, resolved)
36+
return resolved
2737
}
2838

29-
func resolveDoctorBeadsDir(repoPath string) string {
39+
func resolveBeadsDirForRepoUncached(repoPath string) string {
3040
localBeadsDir := filepath.Join(repoPath, ".beads")
3141
if info, err := os.Stat(localBeadsDir); err == nil && info.IsDir() {
3242
return resolveBeadsDir(localBeadsDir)
@@ -39,6 +49,10 @@ func resolveDoctorBeadsDir(repoPath string) string {
3949
return resolveBeadsDir(localBeadsDir)
4050
}
4151

52+
func clearResolveBeadsDirCache() {
53+
resolveBeadsDirCache = sync.Map{}
54+
}
55+
4256
func worktreeFallbackBeadsDir(repoPath string) string {
4357
cmd := exec.Command("git", "-C", repoPath, "rev-parse", "--git-dir", "--git-common-dir")
4458
output, err := cmd.Output()

cmd/bd/doctor/bare_parent_fallback_test.go

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,9 @@ import (
1111
)
1212

1313
func TestResolveBeadsDirForRepo_BareParentWorktreeFallback(t *testing.T) {
14+
clearResolveBeadsDirCache()
15+
t.Cleanup(clearResolveBeadsDirCache)
16+
1417
bareDir, featureWorktreeDir := setupBareParentWorktreeForDoctorTest(t)
1518
bareBeadsDir := filepath.Join(bareDir, ".beads")
1619
if err := os.MkdirAll(bareBeadsDir, 0o750); err != nil {
@@ -23,7 +26,66 @@ func TestResolveBeadsDirForRepo_BareParentWorktreeFallback(t *testing.T) {
2326
}
2427
}
2528

29+
func TestResolveBeadsDirForRepo_CachesFallbackResult(t *testing.T) {
30+
clearResolveBeadsDirCache()
31+
t.Cleanup(clearResolveBeadsDirCache)
32+
33+
tmpDir := t.TempDir()
34+
repoPath := filepath.Join(tmpDir, "feature")
35+
bareDir := filepath.Join(tmpDir, "repo.git")
36+
bareBeadsDir := filepath.Join(bareDir, ".beads")
37+
gitBinDir := filepath.Join(tmpDir, "bin")
38+
gitLogPath := filepath.Join(tmpDir, "git.log")
39+
gitScriptPath := filepath.Join(gitBinDir, "git")
40+
41+
for _, dir := range []string{repoPath, bareBeadsDir, gitBinDir} {
42+
if err := os.MkdirAll(dir, 0o750); err != nil {
43+
t.Fatal(err)
44+
}
45+
}
46+
47+
gitScript := strings.Join([]string{
48+
"#!/bin/sh",
49+
"printf 'called\n' >> \"$FAKE_GIT_LOG\"",
50+
"printf '%s\\n%s\\n' \"$FAKE_GIT_DIR\" \"$FAKE_GIT_COMMON_DIR\"",
51+
"",
52+
}, "\n")
53+
if err := os.WriteFile(gitScriptPath, []byte(gitScript), 0o750); err != nil {
54+
t.Fatal(err)
55+
}
56+
57+
t.Setenv("PATH", gitBinDir)
58+
t.Setenv("FAKE_GIT_LOG", gitLogPath)
59+
t.Setenv("FAKE_GIT_DIR", filepath.Join(bareDir, "worktrees", "feature"))
60+
t.Setenv("FAKE_GIT_COMMON_DIR", bareDir)
61+
62+
first := ResolveBeadsDirForRepo(repoPath)
63+
if first != utils.CanonicalizePath(bareBeadsDir) {
64+
t.Fatalf("first ResolveBeadsDirForRepo() = %q, want %q", first, utils.CanonicalizePath(bareBeadsDir))
65+
}
66+
67+
if err := os.Remove(gitScriptPath); err != nil {
68+
t.Fatal(err)
69+
}
70+
71+
second := ResolveBeadsDirForRepo(repoPath)
72+
if second != first {
73+
t.Fatalf("second ResolveBeadsDirForRepo() = %q, want cached %q", second, first)
74+
}
75+
76+
logData, err := os.ReadFile(gitLogPath)
77+
if err != nil {
78+
t.Fatal(err)
79+
}
80+
if calls := strings.Count(string(logData), "called\n"); calls != 1 {
81+
t.Fatalf("git fallback call count = %d, want 1", calls)
82+
}
83+
}
84+
2685
func TestCheckMetadataVersionTracking_BareParentWorktreeFallback(t *testing.T) {
86+
clearResolveBeadsDirCache()
87+
t.Cleanup(clearResolveBeadsDirCache)
88+
2789
bareDir, featureWorktreeDir := setupBareParentWorktreeForDoctorTest(t)
2890
bareBeadsDir := filepath.Join(bareDir, ".beads")
2991
if err := os.MkdirAll(bareBeadsDir, 0o750); err != nil {
@@ -40,6 +102,9 @@ func TestCheckMetadataVersionTracking_BareParentWorktreeFallback(t *testing.T) {
40102
}
41103

42104
func TestCheckLockHealth_BareParentWorktreeFallback(t *testing.T) {
105+
clearResolveBeadsDirCache()
106+
t.Cleanup(clearResolveBeadsDirCache)
107+
43108
bareDir, featureWorktreeDir := setupBareParentWorktreeForDoctorTest(t)
44109
bareBeadsDir := filepath.Join(bareDir, ".beads")
45110
if err := os.MkdirAll(filepath.Join(bareBeadsDir, "dolt"), 0o750); err != nil {
@@ -56,6 +121,9 @@ func TestCheckLockHealth_BareParentWorktreeFallback(t *testing.T) {
56121
}
57122

58123
func TestCheckDoltLocks_BareParentWorktreeFallback(t *testing.T) {
124+
clearResolveBeadsDirCache()
125+
t.Cleanup(clearResolveBeadsDirCache)
126+
59127
bareDir, featureWorktreeDir := setupBareParentWorktreeForDoctorTest(t)
60128
bareBeadsDir := filepath.Join(bareDir, ".beads")
61129
if err := os.MkdirAll(bareBeadsDir, 0o750); err != nil {

0 commit comments

Comments
 (0)