on main help sign in
perf: lsp batching
commit8ea7890d99db936074b611ff960dc8fccd50ea04 authorRaphael Amorim <rapha850@gmail.com> dateMon 16 Jun 2025, 12:34 (1 year ago) parent20a1704
2 files changed+252−9
internal/lsp/watcher/watcher.go+25−9 internal/lsp/watcher/watcher_performance_test.go+227
internal/lsp/watcher/watcher.go+25−9
@@ -257,7 +257,10 @@ func (w *WorkspaceWatcher) openHighPriorityFiles(ctx context.Context, serverName
257257 }
258258 }
259259
260− // For each pattern, find and open matching files
260+ // Collect all files to open first
261+ var filesToOpen []string
262+
263+ // For each pattern, find matching files
261264 for _, pattern := range patterns {
262265 // Use doublestar.Glob to find files matching the pattern (supports ** patterns)
263266 matches, err := doublestar.Glob(os.DirFS(w.workspacePath), pattern)
@@ -278,7 +281,23 @@ func (w *WorkspaceWatcher) openHighPriorityFiles(ctx context.Context, serverName
278281 continue
279282 }
280283
281− // Open the file
284+ filesToOpen = append(filesToOpen, fullPath)
285+
286+ // Limit the number of files per pattern
287+ if len(filesToOpen) >= 5 && (serverName != "java" && serverName != "jdtls") {
288+ break
289+ }
290+ }
291+ }
292+
293+ // Open files in batches to reduce overhead
294+ batchSize := 3
295+ for i := 0; i < len(filesToOpen); i += batchSize {
296+ end := min(i+batchSize, len(filesToOpen))
297+
298+ // Open batch of files
299+ for j := i; j < end; j++ {
300+ fullPath := filesToOpen[j]
282301 if err := w.client.OpenFile(ctx, fullPath); err != nil {
283302 if cnf.DebugLSP {
284303 logging.Debug("Error opening high-priority file", "path", fullPath, "error", err)
@@ -289,14 +308,11 @@ func (w *WorkspaceWatcher) openHighPriorityFiles(ctx context.Context, serverName
289308 logging.Debug("Opened high-priority file", "path", fullPath)
290309 }
291310 }
311+ }
292312
293− // Add a small delay to prevent overwhelming the server
294− time.Sleep(20 * time.Millisecond)
295−
296− // Limit the number of files opened per pattern
297− if filesOpened >= 5 && (serverName != "java" && serverName != "jdtls") {
298− break
299− }
313+ // Only add delay between batches, not individual files
314+ if end < len(filesToOpen) {
315+ time.Sleep(50 * time.Millisecond)
300316 }
301317 }
302318
internal/lsp/watcher/watcher_performance_test.gonew file+227
@@ -0,0 +1,227 @@
1+package watcher
2+
3+import (
4+ "os"
5+ "path/filepath"
6+ "testing"
7+ "time"
8+
9+ "github.com/bmatcuk/doublestar/v4"
10+)
11+
12+// createTestWorkspace creates a temporary workspace with test files
13+func createTestWorkspace(tb testing.TB) string {
14+ tmpDir, err := os.MkdirTemp("", "watcher_test")
15+ if err != nil {
16+ tb.Fatal(err)
17+ }
18+
19+ // Create test files for Go project
20+ testFiles := []string{
21+ "go.mod",
22+ "go.sum",
23+ "main.go",
24+ "src/lib.go",
25+ "src/utils.go",
26+ "cmd/app.go",
27+ "internal/config.go",
28+ "internal/db.go",
29+ "pkg/api.go",
30+ "pkg/client.go",
31+ "test/main_test.go",
32+ "test/lib_test.go",
33+ "docs/README.md",
34+ "scripts/build.sh",
35+ "Makefile",
36+ }
37+
38+ for _, file := range testFiles {
39+ fullPath := filepath.Join(tmpDir, file)
40+ dir := filepath.Dir(fullPath)
41+
42+ if err := os.MkdirAll(dir, 0755); err != nil {
43+ tb.Fatal(err)
44+ }
45+
46+ if err := os.WriteFile(fullPath, []byte("// test content"), 0644); err != nil {
47+ tb.Fatal(err)
48+ }
49+ }
50+
51+ return tmpDir
52+}
53+
54+// simulateOldApproach simulates the old file opening approach with per-file delays
55+func simulateOldApproach(workspacePath string, serverName string) (int, time.Duration) {
56+ start := time.Now()
57+ filesOpened := 0
58+
59+ // Define patterns for high-priority files based on server type
60+ var patterns []string
61+
62+ switch serverName {
63+ case "gopls":
64+ patterns = []string{
65+ "**/go.mod",
66+ "**/go.sum",
67+ "**/main.go",
68+ }
69+ default:
70+ patterns = []string{
71+ "**/package.json",
72+ "**/Makefile",
73+ }
74+ }
75+
76+ // OLD APPROACH: For each pattern, find and open matching files with per-file delays
77+ for _, pattern := range patterns {
78+ matches, err := doublestar.Glob(os.DirFS(workspacePath), pattern)
79+ if err != nil {
80+ continue
81+ }
82+
83+ for _, match := range matches {
84+ fullPath := filepath.Join(workspacePath, match)
85+ info, err := os.Stat(fullPath)
86+ if err != nil || info.IsDir() {
87+ continue
88+ }
89+
90+ // Simulate file opening (1ms overhead)
91+ time.Sleep(1 * time.Millisecond)
92+ filesOpened++
93+
94+ // OLD: Add delay after each file
95+ time.Sleep(20 * time.Millisecond)
96+
97+ // Limit files
98+ if filesOpened >= 5 {
99+ break
100+ }
101+ }
102+ }
103+
104+ return filesOpened, time.Since(start)
105+}
106+
107+// simulateNewApproach simulates the new batched file opening approach
108+func simulateNewApproach(workspacePath string, serverName string) (int, time.Duration) {
109+ start := time.Now()
110+ filesOpened := 0
111+
112+ // Define patterns for high-priority files based on server type
113+ var patterns []string
114+
115+ switch serverName {
116+ case "gopls":
117+ patterns = []string{
118+ "**/go.mod",
119+ "**/go.sum",
120+ "**/main.go",
121+ }
122+ default:
123+ patterns = []string{
124+ "**/package.json",
125+ "**/Makefile",
126+ }
127+ }
128+
129+ // NEW APPROACH: Collect all files first
130+ var filesToOpen []string
131+
132+ // For each pattern, find matching files
133+ for _, pattern := range patterns {
134+ matches, err := doublestar.Glob(os.DirFS(workspacePath), pattern)
135+ if err != nil {
136+ continue
137+ }
138+
139+ for _, match := range matches {
140+ fullPath := filepath.Join(workspacePath, match)
141+ info, err := os.Stat(fullPath)
142+ if err != nil || info.IsDir() {
143+ continue
144+ }
145+
146+ filesToOpen = append(filesToOpen, fullPath)
147+
148+ // Limit the number of files per pattern
149+ if len(filesToOpen) >= 5 {
150+ break
151+ }
152+ }
153+ }
154+
155+ // Open files in batches to reduce overhead
156+ batchSize := 3
157+ for i := 0; i < len(filesToOpen); i += batchSize {
158+ end := min(i+batchSize, len(filesToOpen))
159+
160+ // Open batch of files
161+ for j := i; j < end; j++ {
162+ // Simulate file opening (1ms overhead)
163+ time.Sleep(1 * time.Millisecond)
164+ filesOpened++
165+ }
166+
167+ // Only add delay between batches, not individual files
168+ if end < len(filesToOpen) {
169+ time.Sleep(50 * time.Millisecond)
170+ }
171+ }
172+
173+ return filesOpened, time.Since(start)
174+}
175+
176+func BenchmarkOldApproach(b *testing.B) {
177+ tmpDir := createTestWorkspace(b)
178+ defer os.RemoveAll(tmpDir)
179+
180+ b.ResetTimer()
181+ for i := 0; i < b.N; i++ {
182+ simulateOldApproach(tmpDir, "gopls")
183+ }
184+}
185+
186+func BenchmarkNewApproach(b *testing.B) {
187+ tmpDir := createTestWorkspace(b)
188+ defer os.RemoveAll(tmpDir)
189+
190+ b.ResetTimer()
191+ for i := 0; i < b.N; i++ {
192+ simulateNewApproach(tmpDir, "gopls")
193+ }
194+}
195+
196+func TestPerformanceComparison(t *testing.T) {
197+ tmpDir := createTestWorkspace(t)
198+ defer os.RemoveAll(tmpDir)
199+
200+ // Test old approach
201+ filesOpenedOld, oldDuration := simulateOldApproach(tmpDir, "gopls")
202+
203+ // Test new approach
204+ filesOpenedNew, newDuration := simulateNewApproach(tmpDir, "gopls")
205+
206+ t.Logf("Old approach: %d files in %v", filesOpenedOld, oldDuration)
207+ t.Logf("New approach: %d files in %v", filesOpenedNew, newDuration)
208+
209+ if newDuration > 0 && oldDuration > 0 {
210+ improvement := float64(oldDuration-newDuration) / float64(oldDuration) * 100
211+ t.Logf("Performance improvement: %.1f%%", improvement)
212+
213+ if improvement <= 0 {
214+ t.Errorf("Expected performance improvement, but new approach was slower")
215+ }
216+ }
217+
218+ // Verify same number of files opened
219+ if filesOpenedOld != filesOpenedNew {
220+ t.Errorf("Different number of files opened: old=%d, new=%d", filesOpenedOld, filesOpenedNew)
221+ }
222+
223+ // Verify new approach is faster
224+ if newDuration >= oldDuration {
225+ t.Errorf("New approach should be faster: old=%v, new=%v", oldDuration, newDuration)
226+ }
227+}