on main help sign in
fix: regex pattern caching to eliminate repeated compilation overhead
4f81ca4Raphael Amorim
internal/diff/diff.go+8−4
@@ -16,6 +16,12 @@ import (
16 "github.com/sergi/go-diff/diffmatchpatch"
16 "github.com/sergi/go-diff/diffmatchpatch"
17)
17)
18
18
19+// Pre-compiled regex patterns for better performance
20+var (
21+ hunkHeaderRegex = regexp.MustCompile(`^@@ -(\d+),?(\d*) \+(\d+),?(\d*) @@`)
22+ ansiRegex = regexp.MustCompile(`\x1b(?:[@-Z\\-_]|\[[0-9?]*(?:;[0-9?]*)*[@-~])`)
23+)
24+
19// -------------------------------------------------------------------------
25// -------------------------------------------------------------------------
20// Core Types
26// Core Types
21// -------------------------------------------------------------------------
27// -------------------------------------------------------------------------
@@ -129,7 +135,6 @@ func ParseUnifiedDiff(diff string) (DiffResult, error) {
129 var result DiffResult
135 var result DiffResult
130 var currentHunk *Hunk
136 var currentHunk *Hunk
131
137
132− hunkHeaderRe := regexp.MustCompile(`^@@ -(\d+),?(\d*) \+(\d+),?(\d*) @@`)
133 lines := strings.Split(diff, "\n")
138 lines := strings.Split(diff, "\n")
134
139
135 var oldLine, newLine int
140 var oldLine, newLine int
@@ -150,7 +155,7 @@ func ParseUnifiedDiff(diff string) (DiffResult, error) {
150 }
155 }
151
156
152 // Parse hunk headers
157 // Parse hunk headers
153− if matches := hunkHeaderRe.FindStringSubmatch(line); matches != nil {
158+ if matches := hunkHeaderRegex.FindStringSubmatch(line); matches != nil {
154 if currentHunk != nil {
159 if currentHunk != nil {
155 result.Hunks = append(result.Hunks, *currentHunk)
160 result.Hunks = append(result.Hunks, *currentHunk)
156 }
161 }
@@ -343,8 +348,7 @@ func createStyles(t *styles.Theme) (removedLineStyle, addedLineStyle, contextLin
343
348
344// applyHighlighting applies intra-line highlighting to a piece of text
349// applyHighlighting applies intra-line highlighting to a piece of text
345func applyHighlighting(content string, segments []Segment, segmentType LineType, highlightBg color.Color) string {
350func applyHighlighting(content string, segments []Segment, segmentType LineType, highlightBg color.Color) string {
346− // Find all ANSI sequences in the content
351+ // Find all ANSI sequences in the content using pre-compiled regex
347− ansiRegex := regexp.MustCompile(`\x1b(?:[@-Z\\-_]|\[[0-9?]*(?:;[0-9?]*)*[@-~])`)
348 ansiMatches := ansiRegex.FindAllStringIndex(content, -1)
352 ansiMatches := ansiRegex.FindAllStringIndex(content, -1)
349
353
350 // Build a mapping of visible character positions to their actual indices
354 // Build a mapping of visible character positions to their actual indices
internal/llm/tools/grep.go+56−4
@@ -12,12 +12,63 @@ import (
12 "sort"
12 "sort"
13 "strconv"
13 "strconv"
14 "strings"
14 "strings"
15+ "sync"
15 "time"
16 "time"
16
17
17 "github.com/charmbracelet/crush/internal/config"
18 "github.com/charmbracelet/crush/internal/config"
18 "github.com/charmbracelet/crush/internal/fileutil"
19 "github.com/charmbracelet/crush/internal/fileutil"
19)
20)
20
21
22+// regexCache provides thread-safe caching of compiled regex patterns
23+type regexCache struct {
24+ cache map[string]*regexp.Regexp
25+ mu sync.RWMutex
26+}
27+
28+// newRegexCache creates a new regex cache
29+func newRegexCache() *regexCache {
30+ return &regexCache{
31+ cache: make(map[string]*regexp.Regexp),
32+ }
33+}
34+
35+// get retrieves a compiled regex from cache or compiles and caches it
36+func (rc *regexCache) get(pattern string) (*regexp.Regexp, error) {
37+ // Try to get from cache first (read lock)
38+ rc.mu.RLock()
39+ if regex, exists := rc.cache[pattern]; exists {
40+ rc.mu.RUnlock()
41+ return regex, nil
42+ }
43+ rc.mu.RUnlock()
44+
45+ // Compile the regex (write lock)
46+ rc.mu.Lock()
47+ defer rc.mu.Unlock()
48+
49+ // Double-check in case another goroutine compiled it while we waited
50+ if regex, exists := rc.cache[pattern]; exists {
51+ return regex, nil
52+ }
53+
54+ // Compile and cache the regex
55+ regex, err := regexp.Compile(pattern)
56+ if err != nil {
57+ return nil, err
58+ }
59+
60+ rc.cache[pattern] = regex
61+ return regex, nil
62+}
63+
64+// Global regex cache instances
65+var (
66+ searchRegexCache = newRegexCache()
67+ globRegexCache = newRegexCache()
68+ // Pre-compiled regex for glob conversion (used frequently)
69+ globBraceRegex = regexp.MustCompile(`\{([^}]+)\}`)
70+)
71+
21type GrepParams struct {
72type GrepParams struct {
22 Pattern string `json:"pattern"`
73 Pattern string `json:"pattern"`
23 Path string `json:"path"`
74 Path string `json:"path"`
@@ -266,7 +317,8 @@ func searchWithRipgrep(pattern, path, include string) ([]grepMatch, error) {
266func searchFilesWithRegex(pattern, rootPath, include string) ([]grepMatch, error) {
317func searchFilesWithRegex(pattern, rootPath, include string) ([]grepMatch, error) {
267 matches := []grepMatch{}
318 matches := []grepMatch{}
268
319
320+ // Use cached regex compilation
269− regex, err := regexp.Compile(pattern)
321+ regex, err := searchRegexCache.get(pattern)
270 if err != nil {
322 if err != nil {
271 return nil, fmt.Errorf("invalid regex pattern: %w", err)
323 return nil, fmt.Errorf("invalid regex pattern: %w", err)
272 }
324 }
@@ -274,7 +326,7 @@ func searchFilesWithRegex(pattern, rootPath, include string) ([]grepMatch, error
274 var includePattern *regexp.Regexp
326 var includePattern *regexp.Regexp
275 if include != "" {
327 if include != "" {
276 regexPattern := globToRegex(include)
328 regexPattern := globToRegex(include)
277− includePattern, err = regexp.Compile(regexPattern)
329+ includePattern, err = globRegexCache.get(regexPattern)
278 if err != nil {
330 if err != nil {
279 return nil, fmt.Errorf("invalid include pattern: %w", err)
331 return nil, fmt.Errorf("invalid include pattern: %w", err)
280 }
332 }
@@ -349,8 +401,8 @@ func globToRegex(glob string) string {
349 regexPattern = strings.ReplaceAll(regexPattern, "*", ".*")
401 regexPattern = strings.ReplaceAll(regexPattern, "*", ".*")
350 regexPattern = strings.ReplaceAll(regexPattern, "?", ".")
402 regexPattern = strings.ReplaceAll(regexPattern, "?", ".")
351
403
352− re := regexp.MustCompile(`\{([^}]+)\}`)
404+ // Use pre-compiled regex instead of compiling each time
353− regexPattern = re.ReplaceAllStringFunc(regexPattern, func(match string) string {
405+ regexPattern = globBraceRegex.ReplaceAllStringFunc(regexPattern, func(match string) string {
354 inner := match[1 : len(match)-1]
406 inner := match[1 : len(match)-1]
355 return "(" + strings.ReplaceAll(inner, ",", "|") + ")"
407 return "(" + strings.ReplaceAll(inner, ",", "|") + ")"
356 })
408 })