From db2170de098c01bdeeaf9b261810216e9e8e5719 Mon Sep 17 00:00:00 2001 From: Raphael Amorim Date: Mon, 16 Jun 2025 11:59:08 +0200 Subject: [PATCH 1/2] perf: shell commands using exponential backoff --- internal/llm/provider/openai.go | 2 +- internal/llm/tools/shell/comparison_test.go | 83 +++++++++++++++++++++ internal/llm/tools/shell/shell.go | 18 ++++- internal/llm/tools/shell/shell_test.go | 54 ++++++++++++++ 4 files changed, 155 insertions(+), 2 deletions(-) create mode 100644 internal/llm/tools/shell/comparison_test.go create mode 100644 internal/llm/tools/shell/shell_test.go diff --git a/internal/llm/provider/openai.go b/internal/llm/provider/openai.go index 672ef1eb6b36bf65a8db8491cefbe83e8272845a..aa917ca40620c386d1b5b27ea6f6197260697b2e 100644 --- a/internal/llm/provider/openai.go +++ b/internal/llm/provider/openai.go @@ -166,7 +166,7 @@ func (o *openaiClient) preparedParams(messages []openai.ChatCompletionMessagePar Tools: tools, } - if o.providerOptions.model.CanReason == true { + if o.providerOptions.model.CanReason { params.MaxCompletionTokens = openai.Int(o.providerOptions.maxTokens) switch o.options.reasoningEffort { case "low": diff --git a/internal/llm/tools/shell/comparison_test.go b/internal/llm/tools/shell/comparison_test.go new file mode 100644 index 0000000000000000000000000000000000000000..d0b615f0968cdfa00df24b62894e0b0bf8ab8bec --- /dev/null +++ b/internal/llm/tools/shell/comparison_test.go @@ -0,0 +1,83 @@ +package shell + +import ( + "context" + "os" + "runtime" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestShellPerformanceComparison(t *testing.T) { + tmpDir, err := os.MkdirTemp("", "shell-test") + require.NoError(t, err) + defer os.RemoveAll(tmpDir) + + shell := GetPersistentShell(tmpDir) + defer shell.Close() + + // Test quick command + start := time.Now() + stdout, stderr, exitCode, _, err := shell.Exec(context.Background(), "echo 'hello'", 0) + duration := time.Since(start) + + require.NoError(t, err) + assert.Equal(t, 0, exitCode) + assert.Contains(t, stdout, "hello") + assert.Empty(t, stderr) + + t.Logf("Quick command took: %v", duration) +} + +func TestShellCPUUsageComparison(t *testing.T) { + tmpDir, err := os.MkdirTemp("", "shell-test") + require.NoError(t, err) + defer os.RemoveAll(tmpDir) + + shell := GetPersistentShell(tmpDir) + defer shell.Close() + + // Measure CPU and memory usage during a longer command + var m1, m2 runtime.MemStats + runtime.GC() + runtime.ReadMemStats(&m1) + + start := time.Now() + _, stderr, exitCode, _, err := shell.Exec(context.Background(), "sleep 0.1", 1000) + duration := time.Since(start) + + runtime.ReadMemStats(&m2) + + require.NoError(t, err) + assert.Equal(t, 0, exitCode) + assert.Empty(t, stderr) + + memGrowth := m2.Alloc - m1.Alloc + t.Logf("Sleep 0.1s command took: %v", duration) + t.Logf("Memory growth during polling: %d bytes", memGrowth) + t.Logf("GC cycles during test: %d", m2.NumGC-m1.NumGC) +} + +// Benchmark CPU usage during polling +func BenchmarkShellPolling(b *testing.B) { + tmpDir, err := os.MkdirTemp("", "shell-bench") + require.NoError(b, err) + defer os.RemoveAll(tmpDir) + + shell := GetPersistentShell(tmpDir) + defer shell.Close() + + b.ResetTimer() + b.ReportAllocs() + + for i := 0; i < b.N; i++ { + // Use a short sleep to measure polling overhead + _, _, exitCode, _, err := shell.Exec(context.Background(), "sleep 0.02", 500) + if err != nil || exitCode != 0 { + b.Fatalf("Command failed: %v, exit code: %d", err, exitCode) + } + } +} \ No newline at end of file diff --git a/internal/llm/tools/shell/shell.go b/internal/llm/tools/shell/shell.go index fffe8fcfe73894f30790c6a21be402332af21c9c..99e197afbcc2d3c8923455567a5a288e868c9ffb 100644 --- a/internal/llm/tools/shell/shell.go +++ b/internal/llm/tools/shell/shell.go @@ -189,6 +189,13 @@ echo $EXEC_EXIT_CODE > %s done := make(chan bool) go func() { + // Use exponential backoff polling + pollInterval := 1 * time.Millisecond + maxPollInterval := 100 * time.Millisecond + + ticker := time.NewTicker(pollInterval) + defer ticker.Stop() + for { select { case <-ctx.Done(): @@ -197,7 +204,7 @@ echo $EXEC_EXIT_CODE > %s done <- true return - case <-time.After(10 * time.Millisecond): + case <-ticker.C: if fileExists(statusFile) && fileSize(statusFile) > 0 { done <- true return @@ -212,6 +219,15 @@ echo $EXEC_EXIT_CODE > %s return } } + + // Exponential backoff to reduce CPU usage for longer-running commands + if pollInterval < maxPollInterval { + pollInterval = time.Duration(float64(pollInterval) * 1.5) + if pollInterval > maxPollInterval { + pollInterval = maxPollInterval + } + ticker.Reset(pollInterval) + } } } }() diff --git a/internal/llm/tools/shell/shell_test.go b/internal/llm/tools/shell/shell_test.go new file mode 100644 index 0000000000000000000000000000000000000000..f454e87398e6d2617f4d3f1210161f8f9d5b0893 --- /dev/null +++ b/internal/llm/tools/shell/shell_test.go @@ -0,0 +1,54 @@ +package shell + +import ( + "context" + "os" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestShellPerformanceImprovement(t *testing.T) { + // Create a temporary directory for the shell + tmpDir, err := os.MkdirTemp("", "shell-test") + require.NoError(t, err) + defer os.RemoveAll(tmpDir) + + shell := GetPersistentShell(tmpDir) + defer shell.Close() + + // Test that quick commands complete fast + start := time.Now() + stdout, stderr, exitCode, _, err := shell.Exec(context.Background(), "echo 'hello world'", 0) + duration := time.Since(start) + + require.NoError(t, err) + assert.Equal(t, 0, exitCode) + assert.Contains(t, stdout, "hello world") + assert.Empty(t, stderr) + + // Quick commands should complete very fast with our exponential backoff + assert.Less(t, duration, 50*time.Millisecond, "Quick command should complete fast with exponential backoff") +} + +// Benchmark to measure CPU efficiency +func BenchmarkShellQuickCommands(b *testing.B) { + tmpDir, err := os.MkdirTemp("", "shell-bench") + require.NoError(b, err) + defer os.RemoveAll(tmpDir) + + shell := GetPersistentShell(tmpDir) + defer shell.Close() + + b.ResetTimer() + b.ReportAllocs() + + for i := 0; i < b.N; i++ { + _, _, exitCode, _, err := shell.Exec(context.Background(), "echo test", 0) + if err != nil || exitCode != 0 { + b.Fatalf("Command failed: %v, exit code: %d", err, exitCode) + } + } +} \ No newline at end of file From aa0fb4c05437c3941fc860fe34e1eae920f7cd8a Mon Sep 17 00:00:00 2001 From: Raphael Amorim Date: Mon, 16 Jun 2025 12:11:29 +0200 Subject: [PATCH 2/2] fix: fmt files --- internal/llm/tools/shell/comparison_test.go | 10 +++++----- internal/llm/tools/shell/shell.go | 6 +++--- internal/llm/tools/shell/shell_test.go | 6 +++--- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/internal/llm/tools/shell/comparison_test.go b/internal/llm/tools/shell/comparison_test.go index d0b615f0968cdfa00df24b62894e0b0bf8ab8bec..7fcd720b5ecdfba236ed7316dfc26b63d5ff9605 100644 --- a/internal/llm/tools/shell/comparison_test.go +++ b/internal/llm/tools/shell/comparison_test.go @@ -28,7 +28,7 @@ func TestShellPerformanceComparison(t *testing.T) { assert.Equal(t, 0, exitCode) assert.Contains(t, stdout, "hello") assert.Empty(t, stderr) - + t.Logf("Quick command took: %v", duration) } @@ -48,13 +48,13 @@ func TestShellCPUUsageComparison(t *testing.T) { start := time.Now() _, stderr, exitCode, _, err := shell.Exec(context.Background(), "sleep 0.1", 1000) duration := time.Since(start) - + runtime.ReadMemStats(&m2) require.NoError(t, err) assert.Equal(t, 0, exitCode) assert.Empty(t, stderr) - + memGrowth := m2.Alloc - m1.Alloc t.Logf("Sleep 0.1s command took: %v", duration) t.Logf("Memory growth during polling: %d bytes", memGrowth) @@ -72,7 +72,7 @@ func BenchmarkShellPolling(b *testing.B) { b.ResetTimer() b.ReportAllocs() - + for i := 0; i < b.N; i++ { // Use a short sleep to measure polling overhead _, _, exitCode, _, err := shell.Exec(context.Background(), "sleep 0.02", 500) @@ -80,4 +80,4 @@ func BenchmarkShellPolling(b *testing.B) { b.Fatalf("Command failed: %v, exit code: %d", err, exitCode) } } -} \ No newline at end of file +} diff --git a/internal/llm/tools/shell/shell.go b/internal/llm/tools/shell/shell.go index 99e197afbcc2d3c8923455567a5a288e868c9ffb..6c94904c9584c8d1500130196fef10cad202620d 100644 --- a/internal/llm/tools/shell/shell.go +++ b/internal/llm/tools/shell/shell.go @@ -192,10 +192,10 @@ echo $EXEC_EXIT_CODE > %s // Use exponential backoff polling pollInterval := 1 * time.Millisecond maxPollInterval := 100 * time.Millisecond - + ticker := time.NewTicker(pollInterval) defer ticker.Stop() - + for { select { case <-ctx.Done(): @@ -219,7 +219,7 @@ echo $EXEC_EXIT_CODE > %s return } } - + // Exponential backoff to reduce CPU usage for longer-running commands if pollInterval < maxPollInterval { pollInterval = time.Duration(float64(pollInterval) * 1.5) diff --git a/internal/llm/tools/shell/shell_test.go b/internal/llm/tools/shell/shell_test.go index f454e87398e6d2617f4d3f1210161f8f9d5b0893..327ec91db5f2cdffdbb501648df1546e4746fabb 100644 --- a/internal/llm/tools/shell/shell_test.go +++ b/internal/llm/tools/shell/shell_test.go @@ -28,7 +28,7 @@ func TestShellPerformanceImprovement(t *testing.T) { assert.Equal(t, 0, exitCode) assert.Contains(t, stdout, "hello world") assert.Empty(t, stderr) - + // Quick commands should complete very fast with our exponential backoff assert.Less(t, duration, 50*time.Millisecond, "Quick command should complete fast with exponential backoff") } @@ -44,11 +44,11 @@ func BenchmarkShellQuickCommands(b *testing.B) { b.ResetTimer() b.ReportAllocs() - + for i := 0; i < b.N; i++ { _, _, exitCode, _, err := shell.Exec(context.Background(), "echo test", 0) if err != nil || exitCode != 0 { b.Fatalf("Command failed: %v, exit code: %d", err, exitCode) } } -} \ No newline at end of file +}