on main help sign in
perf: optimize HTTP client pooling and binary file detection
b66be3bRaphael Amorim
internal/llm/tools/fetch.go+52−12
@@ -7,6 +7,7 @@ import (
7 "io"
7 "io"
8 "net/http"
8 "net/http"
9 "strings"
9 "strings"
10+ "sync"
10 "time"
11 "time"
11
12
12 md "github.com/JohannesKaufmann/html-to-markdown"
13 md "github.com/JohannesKaufmann/html-to-markdown"
@@ -28,8 +29,10 @@ type FetchPermissionsParams struct {
28}
29}
29
30
30type fetchTool struct {
31type fetchTool struct {
31− client *http.Client
32+ client *http.Client
33+ clientPool map[int]*http.Client
34+ clientPoolMu sync.RWMutex
32− permissions permission.Service
35+ permissions permission.Service
33}
36}
34
37
35const (
38const (
@@ -69,11 +72,57 @@ func NewFetchTool(permissions permission.Service) BaseTool {
69 return &fetchTool{
72 return &fetchTool{
70 client: &http.Client{
73 client: &http.Client{
71 Timeout: 30 * time.Second,
74 Timeout: 30 * time.Second,
75+ Transport: &http.Transport{
76+ MaxIdleConns: 100,
77+ MaxIdleConnsPerHost: 10,
78+ IdleConnTimeout: 90 * time.Second,
79+ },
72 },
80 },
81+ clientPool: make(map[int]*http.Client),
73 permissions: permissions,
82 permissions: permissions,
74 }
83 }
75}
84}
76
85
86+// getClientForTimeout returns a cached client for the given timeout or the default client
87+func (t *fetchTool) getClientForTimeout(timeout int) *http.Client {
88+ if timeout <= 0 {
89+ return t.client
90+ }
91+
92+ maxTimeout := 120 // 2 minutes
93+ if timeout > maxTimeout {
94+ timeout = maxTimeout
95+ }
96+
97+ // Check if we have a cached client for this timeout
98+ t.clientPoolMu.RLock()
99+ if client, exists := t.clientPool[timeout]; exists {
100+ t.clientPoolMu.RUnlock()
101+ return client
102+ }
103+ t.clientPoolMu.RUnlock()
104+
105+ // Create and cache a new client
106+ t.clientPoolMu.Lock()
107+ defer t.clientPoolMu.Unlock()
108+
109+ // Double-check in case another goroutine created it
110+ if client, exists := t.clientPool[timeout]; exists {
111+ return client
112+ }
113+
114+ client := &http.Client{
115+ Timeout: time.Duration(timeout) * time.Second,
116+ Transport: &http.Transport{
117+ MaxIdleConns: 100,
118+ MaxIdleConnsPerHost: 10,
119+ IdleConnTimeout: 90 * time.Second,
120+ },
121+ }
122+ t.clientPool[timeout] = client
123+ return client
124+}
125+
77func (t *fetchTool) Info() ToolInfo {
126func (t *fetchTool) Info() ToolInfo {
78 return ToolInfo{
127 return ToolInfo{
79 Name: FetchToolName,
128 Name: FetchToolName,
@@ -136,16 +185,7 @@ func (t *fetchTool) Run(ctx context.Context, call ToolCall) (ToolResponse, error
136 return ToolResponse{}, permission.ErrorPermissionDenied
185 return ToolResponse{}, permission.ErrorPermissionDenied
137 }
186 }
138
187
139− client := t.client
188+ client := t.getClientForTimeout(params.Timeout)
140− if params.Timeout > 0 {
141− maxTimeout := 120 // 2 minutes
142− if params.Timeout > maxTimeout {
143− params.Timeout = maxTimeout
144− }
145− client = &http.Client{
146− Timeout: time.Duration(params.Timeout) * time.Second,
147− }
148− }
149
189
150 req, err := http.NewRequestWithContext(ctx, "GET", params.URL, nil)
190 req, err := http.NewRequestWithContext(ctx, "GET", params.URL, nil)
151 if err != nil {
191 if err != nil {
internal/llm/tools/grep.go+46
@@ -5,6 +5,7 @@ import (
5 "context"
5 "context"
6 "encoding/json"
6 "encoding/json"
7 "fmt"
7 "fmt"
8+ "io"
8 "os"
9 "os"
9 "os/exec"
10 "os/exec"
10 "path/filepath"
11 "path/filepath"
@@ -377,6 +378,11 @@ func searchFilesWithRegex(pattern, rootPath, include string) ([]grepMatch, error
377}
378}
378
379
379func fileContainsPattern(filePath string, pattern *regexp.Regexp) (bool, int, string, error) {
380func fileContainsPattern(filePath string, pattern *regexp.Regexp) (bool, int, string, error) {
381+ // Quick binary file detection
382+ if isBinaryFile(filePath) {
383+ return false, 0, "", nil
384+ }
385+
380 file, err := os.Open(filePath)
386 file, err := os.Open(filePath)
381 if err != nil {
387 if err != nil {
382 return false, 0, "", err
388 return false, 0, "", err
@@ -396,6 +402,46 @@ func fileContainsPattern(filePath string, pattern *regexp.Regexp) (bool, int, st
396 return false, 0, "", scanner.Err()
402 return false, 0, "", scanner.Err()
397}
403}
398
404
405+// isBinaryFile performs a quick check to determine if a file is binary
406+func isBinaryFile(filePath string) bool {
407+ // Check file extension first (fastest)
408+ ext := strings.ToLower(filepath.Ext(filePath))
409+ binaryExts := map[string]bool{
410+ ".exe": true, ".dll": true, ".so": true, ".dylib": true,
411+ ".bin": true, ".obj": true, ".o": true, ".a": true,
412+ ".zip": true, ".tar": true, ".gz": true, ".bz2": true,
413+ ".jpg": true, ".jpeg": true, ".png": true, ".gif": true,
414+ ".pdf": true, ".doc": true, ".docx": true, ".xls": true,
415+ ".mp3": true, ".mp4": true, ".avi": true, ".mov": true,
416+ }
417+ if binaryExts[ext] {
418+ return true
419+ }
420+
421+ // Quick content check for files without clear extensions
422+ file, err := os.Open(filePath)
423+ if err != nil {
424+ return false // If we can't open it, let the caller handle the error
425+ }
426+ defer file.Close()
427+
428+ // Read first 512 bytes to check for null bytes
429+ buffer := make([]byte, 512)
430+ n, err := file.Read(buffer)
431+ if err != nil && err != io.EOF {
432+ return false
433+ }
434+
435+ // Check for null bytes (common in binary files)
436+ for i := 0; i < n; i++ {
437+ if buffer[i] == 0 {
438+ return true
439+ }
440+ }
441+
442+ return false
443+}
444+
399func globToRegex(glob string) string {
445func globToRegex(glob string) string {
400 regexPattern := strings.ReplaceAll(glob, ".", "\\.")
446 regexPattern := strings.ReplaceAll(glob, ".", "\\.")
401 regexPattern = strings.ReplaceAll(regexPattern, "*", ".*")
447 regexPattern = strings.ReplaceAll(regexPattern, "*", ".*")
internal/llm/tools/sourcegraph.go+51−11
@@ -8,6 +8,7 @@ import (
8 "io"
8 "io"
9 "net/http"
9 "net/http"
10 "strings"
10 "strings"
11+ "sync"
11 "time"
12 "time"
12)
13)
13
14
@@ -24,7 +25,9 @@ type SourcegraphResponseMetadata struct {
24}
25}
25
26
26type sourcegraphTool struct {
27type sourcegraphTool struct {
27− client *http.Client
28+ client *http.Client
29+ clientPool map[int]*http.Client
30+ clientPoolMu sync.RWMutex
28}
31}
29
32
30const (
33const (
@@ -129,10 +132,56 @@ func NewSourcegraphTool() BaseTool {
129 return &sourcegraphTool{
132 return &sourcegraphTool{
130 client: &http.Client{
133 client: &http.Client{
131 Timeout: 30 * time.Second,
134 Timeout: 30 * time.Second,
135+ Transport: &http.Transport{
136+ MaxIdleConns: 100,
137+ MaxIdleConnsPerHost: 10,
138+ IdleConnTimeout: 90 * time.Second,
139+ },
132 },
140 },
141+ clientPool: make(map[int]*http.Client),
133 }
142 }
134}
143}
135
144
145+// getClientForTimeout returns a cached client for the given timeout or the default client
146+func (t *sourcegraphTool) getClientForTimeout(timeout int) *http.Client {
147+ if timeout <= 0 {
148+ return t.client
149+ }
150+
151+ maxTimeout := 120 // 2 minutes
152+ if timeout > maxTimeout {
153+ timeout = maxTimeout
154+ }
155+
156+ // Check if we have a cached client for this timeout
157+ t.clientPoolMu.RLock()
158+ if client, exists := t.clientPool[timeout]; exists {
159+ t.clientPoolMu.RUnlock()
160+ return client
161+ }
162+ t.clientPoolMu.RUnlock()
163+
164+ // Create and cache a new client
165+ t.clientPoolMu.Lock()
166+ defer t.clientPoolMu.Unlock()
167+
168+ // Double-check in case another goroutine created it
169+ if client, exists := t.clientPool[timeout]; exists {
170+ return client
171+ }
172+
173+ client := &http.Client{
174+ Timeout: time.Duration(timeout) * time.Second,
175+ Transport: &http.Transport{
176+ MaxIdleConns: 100,
177+ MaxIdleConnsPerHost: 10,
178+ IdleConnTimeout: 90 * time.Second,
179+ },
180+ }
181+ t.clientPool[timeout] = client
182+ return client
183+}
184+
136func (t *sourcegraphTool) Info() ToolInfo {
185func (t *sourcegraphTool) Info() ToolInfo {
137 return ToolInfo{
186 return ToolInfo{
138 Name: SourcegraphToolName,
187 Name: SourcegraphToolName,
@@ -178,16 +227,7 @@ func (t *sourcegraphTool) Run(ctx context.Context, call ToolCall) (ToolResponse,
178 if params.ContextWindow <= 0 {
227 if params.ContextWindow <= 0 {
179 params.ContextWindow = 10 // Default context window
228 params.ContextWindow = 10 // Default context window
180 }
229 }
181− client := t.client
230+ client := t.getClientForTimeout(params.Timeout)
182− if params.Timeout > 0 {
183− maxTimeout := 120 // 2 minutes
184− if params.Timeout > maxTimeout {
185− params.Timeout = maxTimeout
186− }
187− client = &http.Client{
188− Timeout: time.Duration(params.Timeout) * time.Second,
189− }
190− }
191
231
192 type graphqlRequest struct {
232 type graphqlRequest struct {
193 Query string `json:"query"`
233 Query string `json:"query"`