on main help sign in
fix: regex pattern caching to eliminate repeated compilation overhead
commit4f81ca400469229cce7db94ae9c92f84be77b1b7 authorRaphael Amorim <rapha850@gmail.com> dateTue 10 Jun 2025, 12:39 (1 year ago) parentbc790a9
2 files changed+64−8
internal/diff/diff.go+8−4 internal/llm/tools/grep.go+56−4
internal/diff/diff.go+8−4
@@ -16,6 +16,12 @@ import (
1616 "github.com/sergi/go-diff/diffmatchpatch"
1717)
1818
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+
1925// -------------------------------------------------------------------------
2026// Core Types
2127// -------------------------------------------------------------------------
@@ -129,7 +135,6 @@ func ParseUnifiedDiff(diff string) (DiffResult, error) {
129135 var result DiffResult
130136 var currentHunk *Hunk
131137
132− hunkHeaderRe := regexp.MustCompile(`^@@ -(\d+),?(\d*) \+(\d+),?(\d*) @@`)
133138 lines := strings.Split(diff, "\n")
134139
135140 var oldLine, newLine int
@@ -150,7 +155,7 @@ func ParseUnifiedDiff(diff string) (DiffResult, error) {
150155 }
151156
152157 // Parse hunk headers
153− if matches := hunkHeaderRe.FindStringSubmatch(line); matches != nil {
158+ if matches := hunkHeaderRegex.FindStringSubmatch(line); matches != nil {
154159 if currentHunk != nil {
155160 result.Hunks = append(result.Hunks, *currentHunk)
156161 }
@@ -343,8 +348,7 @@ func createStyles(t *styles.Theme) (removedLineStyle, addedLineStyle, contextLin
343348
344349// applyHighlighting applies intra-line highlighting to a piece of text
345350func applyHighlighting(content string, segments []Segment, segmentType LineType, highlightBg color.Color) string {
346− // Find all ANSI sequences in the content
347− ansiRegex := regexp.MustCompile(`\x1b(?:[@-Z\\-_]|\[[0-9?]*(?:;[0-9?]*)*[@-~])`)
351+ // Find all ANSI sequences in the content using pre-compiled regex
348352 ansiMatches := ansiRegex.FindAllStringIndex(content, -1)
349353
350354 // Build a mapping of visible character positions to their actual indices
internal/llm/tools/grep.go+56−4
@@ -12,12 +12,63 @@ import (
1212 "sort"
1313 "strconv"
1414 "strings"
15+ "sync"
1516 "time"
1617
1718 "github.com/charmbracelet/crush/internal/config"
1819 "github.com/charmbracelet/crush/internal/fileutil"
1920)
2021
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+
2172type GrepParams struct {
2273 Pattern string `json:"pattern"`
2374 Path string `json:"path"`
@@ -266,7 +317,8 @@ func searchWithRipgrep(pattern, path, include string) ([]grepMatch, error) {
266317func searchFilesWithRegex(pattern, rootPath, include string) ([]grepMatch, error) {
267318 matches := []grepMatch{}
268319
269− regex, err := regexp.Compile(pattern)
320+ // Use cached regex compilation
321+ regex, err := searchRegexCache.get(pattern)
270322 if err != nil {
271323 return nil, fmt.Errorf("invalid regex pattern: %w", err)
272324 }
@@ -274,7 +326,7 @@ func searchFilesWithRegex(pattern, rootPath, include string) ([]grepMatch, error
274326 var includePattern *regexp.Regexp
275327 if include != "" {
276328 regexPattern := globToRegex(include)
277− includePattern, err = regexp.Compile(regexPattern)
329+ includePattern, err = globRegexCache.get(regexPattern)
278330 if err != nil {
279331 return nil, fmt.Errorf("invalid include pattern: %w", err)
280332 }
@@ -349,8 +401,8 @@ func globToRegex(glob string) string {
349401 regexPattern = strings.ReplaceAll(regexPattern, "*", ".*")
350402 regexPattern = strings.ReplaceAll(regexPattern, "?", ".")
351403
352− re := regexp.MustCompile(`\{([^}]+)\}`)
353− regexPattern = re.ReplaceAllStringFunc(regexPattern, func(match string) string {
404+ // Use pre-compiled regex instead of compiling each time
405+ regexPattern = globBraceRegex.ReplaceAllStringFunc(regexPattern, func(match string) string {
354406 inner := match[1 : len(match)-1]
355407 return "(" + strings.ReplaceAll(inner, ",", "|") + ")"
356408 })