on main help sign in
perf: remove mutex from lsp manager

- used the synchronized client map - lsps root markers don't need to be gitignore aware - cache unexisting lsp binaries globally closes #2223 Signed-off-by: Carlos Alexandro Becker <caarlos0@users.noreply.github.com>

commiteecb1bfe22c36451003f65d1b30dac81e2e2ccaf authorCarlos Alexandro Becker <caarlos0@users.noreply.github.com> dateTue 17 Feb 2026, 13:21 (8 months ago) parent540deef
6 files changed+83−59
internal/agent/tools/glob.go+1−1 internal/fsext/fileutil.go+15−3 internal/fsext/fileutil_test.go+13−13 internal/lsp/client.go+1−1 internal/lsp/manager.go+52−40 internal/ui/model/lsp.go+1−1
internal/agent/tools/glob.go+1−1
@@ -81,7 +81,7 @@ func globFiles(ctx context.Context, pattern, searchPath string, limit int) ([]st
8181 slog.Warn("Ripgrep execution failed, falling back to doublestar", "error", err)
8282 }
8383
84− return fsext.GlobWithDoubleStar(pattern, searchPath, limit)
84+ return fsext.GlobGitignoreAware(pattern, searchPath, limit)
8585}
8686
8787func runRipgrep(cmd *exec.Cmd, searchRoot string, limit int) ([]string, error) {
internal/fsext/fileutil.go+15−3
@@ -82,7 +82,19 @@ func (w *FastGlobWalker) ShouldSkipDir(path string) bool {
8282 return w.directoryLister.shouldIgnore(path, nil, true)
8383}
8484
85−func GlobWithDoubleStar(pattern, searchPath string, limit int) ([]string, bool, error) {
85+// Glob globs files.
86+//
87+// Does not respect gitignore.
88+func Glob(pattern string, cwd string, limit int) ([]string, bool, error) {
89+ return globWithDoubleStar(pattern, cwd, limit, false)
90+}
91+
92+// GlobGitignoreAware globs files respecting gitignore.
93+func GlobGitignoreAware(pattern string, cwd string, limit int) ([]string, bool, error) {
94+ return globWithDoubleStar(pattern, cwd, limit, true)
95+}
96+
97+func globWithDoubleStar(pattern, searchPath string, limit int, gitignore bool) ([]string, bool, error) {
8698 // Normalize pattern to forward slashes on Windows so their config can use
8799 // backslashes
88100 pattern = filepath.ToSlash(pattern)
@@ -101,11 +113,11 @@ func GlobWithDoubleStar(pattern, searchPath string, limit int) ([]string, bool,
101113
102114 isDir := d.IsDir()
103115 if isDir {
104− if walker.ShouldSkipDir(path) {
116+ if gitignore && walker.ShouldSkipDir(path) {
105117 return filepath.SkipDir
106118 }
107119 } else {
108− if walker.ShouldSkip(path) {
120+ if gitignore && walker.ShouldSkip(path) {
109121 return nil
110122 }
111123 }
internal/fsext/fileutil_test.go+13−13
@@ -24,7 +24,7 @@ func TestGlobWithDoubleStar(t *testing.T) {
2424 require.NoError(t, os.WriteFile(file, []byte("test content"), 0o644))
2525 }
2626
27− matches, truncated, err := GlobWithDoubleStar("**/main.go", testDir, 0)
27+ matches, truncated, err := GlobGitignoreAware("**/main.go", testDir, 0)
2828 require.NoError(t, err)
2929 require.False(t, truncated)
3030
@@ -47,7 +47,7 @@ func TestGlobWithDoubleStar(t *testing.T) {
4747 require.NoError(t, os.WriteFile(filepath.Join(srcDir, "main.go"), []byte("package main"), 0o644))
4848 require.NoError(t, os.WriteFile(pkgFile, []byte("test"), 0o644))
4949
50− matches, truncated, err := GlobWithDoubleStar("pkg", testDir, 0)
50+ matches, truncated, err := GlobGitignoreAware("pkg", testDir, 0)
5151 require.NoError(t, err)
5252 require.False(t, truncated)
5353
@@ -66,7 +66,7 @@ func TestGlobWithDoubleStar(t *testing.T) {
6666 require.NoError(t, os.MkdirAll(dir, 0o755))
6767 }
6868
69− matches, truncated, err := GlobWithDoubleStar("**/pkg", testDir, 0)
69+ matches, truncated, err := GlobGitignoreAware("**/pkg", testDir, 0)
7070 require.NoError(t, err)
7171 require.False(t, truncated)
7272
@@ -95,7 +95,7 @@ func TestGlobWithDoubleStar(t *testing.T) {
9595 require.NoError(t, os.WriteFile(file, []byte("package main"), 0o644))
9696 }
9797
98− matches, truncated, err := GlobWithDoubleStar("pkg/**", testDir, 0)
98+ matches, truncated, err := GlobGitignoreAware("pkg/**", testDir, 0)
9999 require.NoError(t, err)
100100 require.False(t, truncated)
101101
@@ -124,7 +124,7 @@ func TestGlobWithDoubleStar(t *testing.T) {
124124 require.NoError(t, os.WriteFile(file, []byte("test"), 0o644))
125125 }
126126
127− matches, truncated, err := GlobWithDoubleStar("**/*.txt", testDir, 5)
127+ matches, truncated, err := GlobGitignoreAware("**/*.txt", testDir, 5)
128128 require.NoError(t, err)
129129 require.True(t, truncated, "Expected truncation with limit")
130130 require.Len(t, matches, 5, "Expected exactly 5 matches with limit")
@@ -143,7 +143,7 @@ func TestGlobWithDoubleStar(t *testing.T) {
143143 require.NoError(t, os.WriteFile(file, []byte("test"), 0o644))
144144 }
145145
146− matches, truncated, err := GlobWithDoubleStar("a/b/c/file1.txt", testDir, 0)
146+ matches, truncated, err := GlobGitignoreAware("a/b/c/file1.txt", testDir, 0)
147147 require.NoError(t, err)
148148 require.False(t, truncated)
149149
@@ -171,7 +171,7 @@ func TestGlobWithDoubleStar(t *testing.T) {
171171 require.NoError(t, os.Chtimes(file2, m2, m2))
172172 require.NoError(t, os.Chtimes(file3, m3, m3))
173173
174− matches, truncated, err := GlobWithDoubleStar("*.txt", testDir, 0)
174+ matches, truncated, err := GlobGitignoreAware("*.txt", testDir, 0)
175175 require.NoError(t, err)
176176 require.False(t, truncated)
177177
@@ -181,7 +181,7 @@ func TestGlobWithDoubleStar(t *testing.T) {
181181 t.Run("handles empty directory", func(t *testing.T) {
182182 testDir := t.TempDir()
183183
184− matches, truncated, err := GlobWithDoubleStar("**", testDir, 0)
184+ matches, truncated, err := GlobGitignoreAware("**", testDir, 0)
185185 require.NoError(t, err)
186186 require.False(t, truncated)
187187 // Even empty directories should return the directory itself
@@ -191,7 +191,7 @@ func TestGlobWithDoubleStar(t *testing.T) {
191191 t.Run("handles non-existent search path", func(t *testing.T) {
192192 nonExistentDir := filepath.Join(t.TempDir(), "does", "not", "exist")
193193
194− matches, truncated, err := GlobWithDoubleStar("**", nonExistentDir, 0)
194+ matches, truncated, err := GlobGitignoreAware("**", nonExistentDir, 0)
195195 require.Error(t, err, "Should return error for non-existent search path")
196196 require.False(t, truncated)
197197 require.Empty(t, matches)
@@ -219,17 +219,17 @@ func TestGlobWithDoubleStar(t *testing.T) {
219219 ignoredFileInDir := filepath.Join(testDir, "backup", "old.txt")
220220 require.NoError(t, os.WriteFile(ignoredFileInDir, []byte("old content"), 0o644))
221221
222− matches, truncated, err := GlobWithDoubleStar("*.tmp", testDir, 0)
222+ matches, truncated, err := GlobGitignoreAware("*.tmp", testDir, 0)
223223 require.NoError(t, err)
224224 require.False(t, truncated)
225225 require.Empty(t, matches, "Expected no matches for '*.tmp' pattern (should be ignored)")
226226
227− matches, truncated, err = GlobWithDoubleStar("backup", testDir, 0)
227+ matches, truncated, err = GlobGitignoreAware("backup", testDir, 0)
228228 require.NoError(t, err)
229229 require.False(t, truncated)
230230 require.Empty(t, matches, "Expected no matches for 'backup' pattern (should be ignored)")
231231
232− matches, truncated, err = GlobWithDoubleStar("*.txt", testDir, 0)
232+ matches, truncated, err = GlobGitignoreAware("*.txt", testDir, 0)
233233 require.NoError(t, err)
234234 require.False(t, truncated)
235235 require.Equal(t, []string{goodFile}, matches)
@@ -257,7 +257,7 @@ func TestGlobWithDoubleStar(t *testing.T) {
257257 require.NoError(t, os.Chtimes(middleDir, tMiddle, tMiddle))
258258 require.NoError(t, os.Chtimes(oldestFile, tNewest, tNewest))
259259
260− matches, truncated, err := GlobWithDoubleStar("*.rs", testDir, 0)
260+ matches, truncated, err := GlobGitignoreAware("*.rs", testDir, 0)
261261 require.NoError(t, err)
262262 require.False(t, truncated)
263263 require.Len(t, matches, 3)
internal/lsp/client.go+1−1
@@ -85,7 +85,7 @@ func New(
8585 resolver: resolver,
8686 cwd: cwd,
8787 }
88− client.serverState.Store(StateStarting)
88+ client.serverState.Store(StateStopped)
8989
9090 if err := client.createPowernapClient(); err != nil {
9191 return nil, err
internal/lsp/manager.go+52−40
@@ -21,13 +21,14 @@ import (
2121 "github.com/sourcegraph/jsonrpc2"
2222)
2323
24+var unavailable = csync.NewMap[string, struct{}]()
25+
2426// Manager handles lazy initialization of LSP clients based on file types.
2527type Manager struct {
2628 clients *csync.Map[string, *Client]
2729 cfg *config.Config
2830 manager *powernapconfig.Manager
2931 callback func(name string, client *Client)
30− mu sync.Mutex
3132}
3233
3334// NewManager creates a new LSP manager service.
@@ -72,17 +73,12 @@ func (s *Manager) Clients() *csync.Map[string, *Client] {
7273// SetCallback sets a callback that is invoked when a new LSP
7374// client is successfully started. This allows the coordinator to add LSP tools.
7475func (s *Manager) SetCallback(cb func(name string, client *Client)) {
75− s.mu.Lock()
76− defer s.mu.Unlock()
7776 s.callback = cb
7877}
7978
8079// TrackConfigured will callback the user-configured LSPs, but will not create
8180// any clients.
8281func (s *Manager) TrackConfigured() {
83− s.mu.Lock()
84− defer s.mu.Unlock()
85−
8682 var wg sync.WaitGroup
8783 for name := range s.manager.GetServers() {
8884 if !s.isUserConfigured(name) {
@@ -102,16 +98,10 @@ func (s *Manager) Start(ctx context.Context, path string) {
10298 return
10399 }
104100
105− s.mu.Lock()
106− defer s.mu.Unlock()
107−
108101 var wg sync.WaitGroup
109102 for name, server := range s.manager.GetServers() {
110− if !handles(server, path, s.cfg.WorkingDir()) {
111− continue
112− }
113103 wg.Go(func() {
114− s.startServer(ctx, name, server)
104+ s.startServer(ctx, name, path, server)
115105 })
116106 }
117107 wg.Wait()
@@ -149,12 +139,31 @@ var skipAutoStartCommands = map[string]bool{
149139 "tflint": true,
150140}
151141
152−func (s *Manager) startServer(ctx context.Context, name string, server *powernapconfig.ServerConfig) {
142+func (s *Manager) startServer(ctx context.Context, name, filepath string, server *powernapconfig.ServerConfig) {
143+ cfg := s.buildConfig(name, server)
144+ if cfg.Disabled {
145+ return
146+ }
147+
148+ if _, exists := unavailable.Get(name); exists {
149+ return
150+ }
151+
152+ if client, ok := s.clients.Get(name); ok {
153+ switch client.GetServerState() {
154+ case StateReady, StateStarting, StateDisabled:
155+ s.callback(name, client)
156+ // already done, return
157+ return
158+ }
159+ }
160+
153161 userConfigured := s.isUserConfigured(name)
154162
155163 if !userConfigured {
156164 if _, err := exec.LookPath(server.Command); err != nil {
157165 slog.Debug("LSP server not installed, skipping", "name", name, "command", server.Command)
166+ unavailable.Set(name, struct{}{})
158167 return
159168 }
160169 if skipAutoStartCommands[server.Command] {
@@ -163,34 +172,43 @@ func (s *Manager) startServer(ctx context.Context, name string, server *powernap
163172 }
164173 }
165174
166− cfg := s.buildConfig(name, server)
167− if client, ok := s.clients.Get(name); ok {
168− switch client.GetServerState() {
169− case StateReady, StateStarting:
170− s.callback(name, client)
171− // already done, return
172− return
173− }
175+ // this is the slowest bit, so we do it last.
176+ if !handles(server, filepath, s.cfg.WorkingDir()) {
177+ // nothing to do
178+ return
174179 }
175− client, err := New(
176− ctx,
177− name,
178− cfg,
179− s.cfg.Resolver(),
180− s.cfg.WorkingDir(),
181− s.cfg.Options.DebugLSP,
182− )
180+
181+ // check again in case another goroutine started it in the meantime
182+ var err error
183+ client := s.clients.GetOrSet(name, func() *Client {
184+ var cli *Client
185+ cli, err = New(
186+ ctx,
187+ name,
188+ cfg,
189+ s.cfg.Resolver(),
190+ s.cfg.WorkingDir(),
191+ s.cfg.Options.DebugLSP,
192+ )
193+ return cli
194+ })
183195 if err != nil {
184196 slog.Error("Failed to create LSP client", "name", name, "error", err)
185197 return
186198 }
187− s.callback(name, client)
188−
189199 defer func() {
190− s.clients.Set(name, client)
191200 s.callback(name, client)
192201 }()
193202
203+ switch client.GetServerState() {
204+ case StateReady, StateStarting:
205+ // already done, return
206+ return
207+ }
208+
209+ client.serverState.Store(StateStarting)
210+ s.callback(name, client)
211+
194212 initCtx, cancel := context.WithTimeout(ctx, time.Duration(cmp.Or(cfg.Timeout, 30))*time.Second)
195213 defer cancel()
196214
@@ -271,7 +289,7 @@ func hasRootMarkers(dir string, markers []string) bool {
271289 }
272290 for _, pattern := range markers {
273291 // Use fsext.GlobWithDoubleStar to find matches
274− matches, _, err := fsext.GlobWithDoubleStar(pattern, dir, 1)
292+ matches, _, err := fsext.Glob(pattern, dir, 1)
275293 if err == nil && len(matches) > 0 {
276294 return true
277295 }
@@ -291,9 +309,6 @@ func handles(server *powernapconfig.ServerConfig, filePath, workDir string) bool
291309// in the middle of writing something.
292310// Generally it doesn't matter when shutting down Crush, though.
293311func (s *Manager) KillAll(context.Context) {
294− s.mu.Lock()
295− defer s.mu.Unlock()
296−
297312 var wg sync.WaitGroup
298313 for name, client := range s.clients.Seq2() {
299314 wg.Go(func() {
@@ -308,9 +323,6 @@ func (s *Manager) KillAll(context.Context) {
308323
309324// StopAll stops all running LSP clients and clears the client map.
310325func (s *Manager) StopAll(ctx context.Context) {
311− s.mu.Lock()
312− defer s.mu.Unlock()
313−
314326 var wg sync.WaitGroup
315327 for name, client := range s.clients.Seq2() {
316328 wg.Go(func() {
internal/ui/model/lsp.go+1−1
@@ -108,7 +108,7 @@ func lspList(t *styles.Styles, lsps []LSPInfo, width, maxItems int) string {
108108 icon = t.ResourceOfflineIcon.Foreground(t.Muted.GetBackground()).String()
109109 description = t.ResourceStatus.Render("disabled")
110110 default:
111− icon = t.ResourceOfflineIcon.String()
111+ continue
112112 }
113113 renderedLsps = append(renderedLsps, common.Status(t, common.StatusOpts{
114114 Icon: icon,