From 8ea7890d99db936074b611ff960dc8fccd50ea04 Mon Sep 17 00:00:00 2001 From: Raphael Amorim Date: Mon, 16 Jun 2025 12:34:28 +0200 Subject: [PATCH 1/3] perf: lsp batching --- internal/lsp/watcher/watcher.go | 34 ++- .../lsp/watcher/watcher_performance_test.go | 227 ++++++++++++++++++ 2 files changed, 252 insertions(+), 9 deletions(-) create mode 100644 internal/lsp/watcher/watcher_performance_test.go diff --git a/internal/lsp/watcher/watcher.go b/internal/lsp/watcher/watcher.go index 3b8c36d963b88c1c4b60ef23a5c7cd9c26af4025..4db10e5b7163468f9d45b307be493a70028381b5 100644 --- a/internal/lsp/watcher/watcher.go +++ b/internal/lsp/watcher/watcher.go @@ -257,7 +257,10 @@ func (w *WorkspaceWatcher) openHighPriorityFiles(ctx context.Context, serverName } } - // For each pattern, find and open matching files + // Collect all files to open first + var filesToOpen []string + + // For each pattern, find matching files for _, pattern := range patterns { // Use doublestar.Glob to find files matching the pattern (supports ** patterns) matches, err := doublestar.Glob(os.DirFS(w.workspacePath), pattern) @@ -278,7 +281,23 @@ func (w *WorkspaceWatcher) openHighPriorityFiles(ctx context.Context, serverName continue } - // Open the file + filesToOpen = append(filesToOpen, fullPath) + + // Limit the number of files per pattern + if len(filesToOpen) >= 5 && (serverName != "java" && serverName != "jdtls") { + break + } + } + } + + // Open files in batches to reduce overhead + batchSize := 3 + for i := 0; i < len(filesToOpen); i += batchSize { + end := min(i+batchSize, len(filesToOpen)) + + // Open batch of files + for j := i; j < end; j++ { + fullPath := filesToOpen[j] if err := w.client.OpenFile(ctx, fullPath); err != nil { if cnf.DebugLSP { logging.Debug("Error opening high-priority file", "path", fullPath, "error", err) @@ -289,14 +308,11 @@ func (w *WorkspaceWatcher) openHighPriorityFiles(ctx context.Context, serverName logging.Debug("Opened high-priority file", "path", fullPath) } } + } - // Add a small delay to prevent overwhelming the server - time.Sleep(20 * time.Millisecond) - - // Limit the number of files opened per pattern - if filesOpened >= 5 && (serverName != "java" && serverName != "jdtls") { - break - } + // Only add delay between batches, not individual files + if end < len(filesToOpen) { + time.Sleep(50 * time.Millisecond) } } diff --git a/internal/lsp/watcher/watcher_performance_test.go b/internal/lsp/watcher/watcher_performance_test.go new file mode 100644 index 0000000000000000000000000000000000000000..cc985b73355ac21e7f90a1aef786862e57aedf6e --- /dev/null +++ b/internal/lsp/watcher/watcher_performance_test.go @@ -0,0 +1,227 @@ +package watcher + +import ( + "os" + "path/filepath" + "testing" + "time" + + "github.com/bmatcuk/doublestar/v4" +) + +// createTestWorkspace creates a temporary workspace with test files +func createTestWorkspace(tb testing.TB) string { + tmpDir, err := os.MkdirTemp("", "watcher_test") + if err != nil { + tb.Fatal(err) + } + + // Create test files for Go project + testFiles := []string{ + "go.mod", + "go.sum", + "main.go", + "src/lib.go", + "src/utils.go", + "cmd/app.go", + "internal/config.go", + "internal/db.go", + "pkg/api.go", + "pkg/client.go", + "test/main_test.go", + "test/lib_test.go", + "docs/README.md", + "scripts/build.sh", + "Makefile", + } + + for _, file := range testFiles { + fullPath := filepath.Join(tmpDir, file) + dir := filepath.Dir(fullPath) + + if err := os.MkdirAll(dir, 0755); err != nil { + tb.Fatal(err) + } + + if err := os.WriteFile(fullPath, []byte("// test content"), 0644); err != nil { + tb.Fatal(err) + } + } + + return tmpDir +} + +// simulateOldApproach simulates the old file opening approach with per-file delays +func simulateOldApproach(workspacePath string, serverName string) (int, time.Duration) { + start := time.Now() + filesOpened := 0 + + // Define patterns for high-priority files based on server type + var patterns []string + + switch serverName { + case "gopls": + patterns = []string{ + "**/go.mod", + "**/go.sum", + "**/main.go", + } + default: + patterns = []string{ + "**/package.json", + "**/Makefile", + } + } + + // OLD APPROACH: For each pattern, find and open matching files with per-file delays + for _, pattern := range patterns { + matches, err := doublestar.Glob(os.DirFS(workspacePath), pattern) + if err != nil { + continue + } + + for _, match := range matches { + fullPath := filepath.Join(workspacePath, match) + info, err := os.Stat(fullPath) + if err != nil || info.IsDir() { + continue + } + + // Simulate file opening (1ms overhead) + time.Sleep(1 * time.Millisecond) + filesOpened++ + + // OLD: Add delay after each file + time.Sleep(20 * time.Millisecond) + + // Limit files + if filesOpened >= 5 { + break + } + } + } + + return filesOpened, time.Since(start) +} + +// simulateNewApproach simulates the new batched file opening approach +func simulateNewApproach(workspacePath string, serverName string) (int, time.Duration) { + start := time.Now() + filesOpened := 0 + + // Define patterns for high-priority files based on server type + var patterns []string + + switch serverName { + case "gopls": + patterns = []string{ + "**/go.mod", + "**/go.sum", + "**/main.go", + } + default: + patterns = []string{ + "**/package.json", + "**/Makefile", + } + } + + // NEW APPROACH: Collect all files first + var filesToOpen []string + + // For each pattern, find matching files + for _, pattern := range patterns { + matches, err := doublestar.Glob(os.DirFS(workspacePath), pattern) + if err != nil { + continue + } + + for _, match := range matches { + fullPath := filepath.Join(workspacePath, match) + info, err := os.Stat(fullPath) + if err != nil || info.IsDir() { + continue + } + + filesToOpen = append(filesToOpen, fullPath) + + // Limit the number of files per pattern + if len(filesToOpen) >= 5 { + break + } + } + } + + // Open files in batches to reduce overhead + batchSize := 3 + for i := 0; i < len(filesToOpen); i += batchSize { + end := min(i+batchSize, len(filesToOpen)) + + // Open batch of files + for j := i; j < end; j++ { + // Simulate file opening (1ms overhead) + time.Sleep(1 * time.Millisecond) + filesOpened++ + } + + // Only add delay between batches, not individual files + if end < len(filesToOpen) { + time.Sleep(50 * time.Millisecond) + } + } + + return filesOpened, time.Since(start) +} + +func BenchmarkOldApproach(b *testing.B) { + tmpDir := createTestWorkspace(b) + defer os.RemoveAll(tmpDir) + + b.ResetTimer() + for i := 0; i < b.N; i++ { + simulateOldApproach(tmpDir, "gopls") + } +} + +func BenchmarkNewApproach(b *testing.B) { + tmpDir := createTestWorkspace(b) + defer os.RemoveAll(tmpDir) + + b.ResetTimer() + for i := 0; i < b.N; i++ { + simulateNewApproach(tmpDir, "gopls") + } +} + +func TestPerformanceComparison(t *testing.T) { + tmpDir := createTestWorkspace(t) + defer os.RemoveAll(tmpDir) + + // Test old approach + filesOpenedOld, oldDuration := simulateOldApproach(tmpDir, "gopls") + + // Test new approach + filesOpenedNew, newDuration := simulateNewApproach(tmpDir, "gopls") + + t.Logf("Old approach: %d files in %v", filesOpenedOld, oldDuration) + t.Logf("New approach: %d files in %v", filesOpenedNew, newDuration) + + if newDuration > 0 && oldDuration > 0 { + improvement := float64(oldDuration-newDuration) / float64(oldDuration) * 100 + t.Logf("Performance improvement: %.1f%%", improvement) + + if improvement <= 0 { + t.Errorf("Expected performance improvement, but new approach was slower") + } + } + + // Verify same number of files opened + if filesOpenedOld != filesOpenedNew { + t.Errorf("Different number of files opened: old=%d, new=%d", filesOpenedOld, filesOpenedNew) + } + + // Verify new approach is faster + if newDuration >= oldDuration { + t.Errorf("New approach should be faster: old=%v, new=%v", oldDuration, newDuration) + } +} \ No newline at end of file From 052f54c87f86a2b37a138ee11b3ec8a80ee8fdea Mon Sep 17 00:00:00 2001 From: Raphael Amorim Date: Mon, 16 Jun 2025 12:45:08 +0200 Subject: [PATCH 2/3] fix: format internal/lsp/watcher/watcher_performance_test.go --- .../lsp/watcher/watcher_performance_test.go | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/internal/lsp/watcher/watcher_performance_test.go b/internal/lsp/watcher/watcher_performance_test.go index cc985b73355ac21e7f90a1aef786862e57aedf6e..e1387c191c3d4602c01fe7f1fc8251cf8193d2a6 100644 --- a/internal/lsp/watcher/watcher_performance_test.go +++ b/internal/lsp/watcher/watcher_performance_test.go @@ -19,7 +19,7 @@ func createTestWorkspace(tb testing.TB) string { // Create test files for Go project testFiles := []string{ "go.mod", - "go.sum", + "go.sum", "main.go", "src/lib.go", "src/utils.go", @@ -38,11 +38,11 @@ func createTestWorkspace(tb testing.TB) string { for _, file := range testFiles { fullPath := filepath.Join(tmpDir, file) dir := filepath.Dir(fullPath) - + if err := os.MkdirAll(dir, 0755); err != nil { tb.Fatal(err) } - + if err := os.WriteFile(fullPath, []byte("// test content"), 0644); err != nil { tb.Fatal(err) } @@ -128,7 +128,7 @@ func simulateNewApproach(workspacePath string, serverName string) (int, time.Dur // NEW APPROACH: Collect all files first var filesToOpen []string - + // For each pattern, find matching files for _, pattern := range patterns { matches, err := doublestar.Glob(os.DirFS(workspacePath), pattern) @@ -144,7 +144,7 @@ func simulateNewApproach(workspacePath string, serverName string) (int, time.Dur } filesToOpen = append(filesToOpen, fullPath) - + // Limit the number of files per pattern if len(filesToOpen) >= 5 { break @@ -156,14 +156,14 @@ func simulateNewApproach(workspacePath string, serverName string) (int, time.Dur batchSize := 3 for i := 0; i < len(filesToOpen); i += batchSize { end := min(i+batchSize, len(filesToOpen)) - + // Open batch of files for j := i; j < end; j++ { // Simulate file opening (1ms overhead) time.Sleep(1 * time.Millisecond) filesOpened++ } - + // Only add delay between batches, not individual files if end < len(filesToOpen) { time.Sleep(50 * time.Millisecond) @@ -205,11 +205,11 @@ func TestPerformanceComparison(t *testing.T) { t.Logf("Old approach: %d files in %v", filesOpenedOld, oldDuration) t.Logf("New approach: %d files in %v", filesOpenedNew, newDuration) - + if newDuration > 0 && oldDuration > 0 { improvement := float64(oldDuration-newDuration) / float64(oldDuration) * 100 t.Logf("Performance improvement: %.1f%%", improvement) - + if improvement <= 0 { t.Errorf("Expected performance improvement, but new approach was slower") } @@ -224,4 +224,4 @@ func TestPerformanceComparison(t *testing.T) { if newDuration >= oldDuration { t.Errorf("New approach should be faster: old=%v, new=%v", oldDuration, newDuration) } -} \ No newline at end of file +} From 54d5622bb0ac5e66588c1bd2ad77774d9c5065e2 Mon Sep 17 00:00:00 2001 From: Raphael Amorim Date: Mon, 16 Jun 2025 23:03:30 +0200 Subject: [PATCH 3/3] fix: remove watcher_performance_test.go --- .../lsp/watcher/watcher_performance_test.go | 227 ------------------ 1 file changed, 227 deletions(-) delete mode 100644 internal/lsp/watcher/watcher_performance_test.go diff --git a/internal/lsp/watcher/watcher_performance_test.go b/internal/lsp/watcher/watcher_performance_test.go deleted file mode 100644 index e1387c191c3d4602c01fe7f1fc8251cf8193d2a6..0000000000000000000000000000000000000000 --- a/internal/lsp/watcher/watcher_performance_test.go +++ /dev/null @@ -1,227 +0,0 @@ -package watcher - -import ( - "os" - "path/filepath" - "testing" - "time" - - "github.com/bmatcuk/doublestar/v4" -) - -// createTestWorkspace creates a temporary workspace with test files -func createTestWorkspace(tb testing.TB) string { - tmpDir, err := os.MkdirTemp("", "watcher_test") - if err != nil { - tb.Fatal(err) - } - - // Create test files for Go project - testFiles := []string{ - "go.mod", - "go.sum", - "main.go", - "src/lib.go", - "src/utils.go", - "cmd/app.go", - "internal/config.go", - "internal/db.go", - "pkg/api.go", - "pkg/client.go", - "test/main_test.go", - "test/lib_test.go", - "docs/README.md", - "scripts/build.sh", - "Makefile", - } - - for _, file := range testFiles { - fullPath := filepath.Join(tmpDir, file) - dir := filepath.Dir(fullPath) - - if err := os.MkdirAll(dir, 0755); err != nil { - tb.Fatal(err) - } - - if err := os.WriteFile(fullPath, []byte("// test content"), 0644); err != nil { - tb.Fatal(err) - } - } - - return tmpDir -} - -// simulateOldApproach simulates the old file opening approach with per-file delays -func simulateOldApproach(workspacePath string, serverName string) (int, time.Duration) { - start := time.Now() - filesOpened := 0 - - // Define patterns for high-priority files based on server type - var patterns []string - - switch serverName { - case "gopls": - patterns = []string{ - "**/go.mod", - "**/go.sum", - "**/main.go", - } - default: - patterns = []string{ - "**/package.json", - "**/Makefile", - } - } - - // OLD APPROACH: For each pattern, find and open matching files with per-file delays - for _, pattern := range patterns { - matches, err := doublestar.Glob(os.DirFS(workspacePath), pattern) - if err != nil { - continue - } - - for _, match := range matches { - fullPath := filepath.Join(workspacePath, match) - info, err := os.Stat(fullPath) - if err != nil || info.IsDir() { - continue - } - - // Simulate file opening (1ms overhead) - time.Sleep(1 * time.Millisecond) - filesOpened++ - - // OLD: Add delay after each file - time.Sleep(20 * time.Millisecond) - - // Limit files - if filesOpened >= 5 { - break - } - } - } - - return filesOpened, time.Since(start) -} - -// simulateNewApproach simulates the new batched file opening approach -func simulateNewApproach(workspacePath string, serverName string) (int, time.Duration) { - start := time.Now() - filesOpened := 0 - - // Define patterns for high-priority files based on server type - var patterns []string - - switch serverName { - case "gopls": - patterns = []string{ - "**/go.mod", - "**/go.sum", - "**/main.go", - } - default: - patterns = []string{ - "**/package.json", - "**/Makefile", - } - } - - // NEW APPROACH: Collect all files first - var filesToOpen []string - - // For each pattern, find matching files - for _, pattern := range patterns { - matches, err := doublestar.Glob(os.DirFS(workspacePath), pattern) - if err != nil { - continue - } - - for _, match := range matches { - fullPath := filepath.Join(workspacePath, match) - info, err := os.Stat(fullPath) - if err != nil || info.IsDir() { - continue - } - - filesToOpen = append(filesToOpen, fullPath) - - // Limit the number of files per pattern - if len(filesToOpen) >= 5 { - break - } - } - } - - // Open files in batches to reduce overhead - batchSize := 3 - for i := 0; i < len(filesToOpen); i += batchSize { - end := min(i+batchSize, len(filesToOpen)) - - // Open batch of files - for j := i; j < end; j++ { - // Simulate file opening (1ms overhead) - time.Sleep(1 * time.Millisecond) - filesOpened++ - } - - // Only add delay between batches, not individual files - if end < len(filesToOpen) { - time.Sleep(50 * time.Millisecond) - } - } - - return filesOpened, time.Since(start) -} - -func BenchmarkOldApproach(b *testing.B) { - tmpDir := createTestWorkspace(b) - defer os.RemoveAll(tmpDir) - - b.ResetTimer() - for i := 0; i < b.N; i++ { - simulateOldApproach(tmpDir, "gopls") - } -} - -func BenchmarkNewApproach(b *testing.B) { - tmpDir := createTestWorkspace(b) - defer os.RemoveAll(tmpDir) - - b.ResetTimer() - for i := 0; i < b.N; i++ { - simulateNewApproach(tmpDir, "gopls") - } -} - -func TestPerformanceComparison(t *testing.T) { - tmpDir := createTestWorkspace(t) - defer os.RemoveAll(tmpDir) - - // Test old approach - filesOpenedOld, oldDuration := simulateOldApproach(tmpDir, "gopls") - - // Test new approach - filesOpenedNew, newDuration := simulateNewApproach(tmpDir, "gopls") - - t.Logf("Old approach: %d files in %v", filesOpenedOld, oldDuration) - t.Logf("New approach: %d files in %v", filesOpenedNew, newDuration) - - if newDuration > 0 && oldDuration > 0 { - improvement := float64(oldDuration-newDuration) / float64(oldDuration) * 100 - t.Logf("Performance improvement: %.1f%%", improvement) - - if improvement <= 0 { - t.Errorf("Expected performance improvement, but new approach was slower") - } - } - - // Verify same number of files opened - if filesOpenedOld != filesOpenedNew { - t.Errorf("Different number of files opened: old=%d, new=%d", filesOpenedOld, filesOpenedNew) - } - - // Verify new approach is faster - if newDuration >= oldDuration { - t.Errorf("New approach should be faster: old=%v, new=%v", oldDuration, newDuration) - } -}