From 77e81e99bc409ac01a43fa4df07223872c1b0777 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Mon, 10 Aug 2026 02:11:44 +0000 Subject: [PATCH] perf: Use strings.Cut instead of strings.Split for version parsing Replaces `strings.Split` with a custom `for` loop using `strings.Cut` in `CompareVersions` (`internal/plugin/manager.go`) and `compareVersions` (`internal/update/update.go`). This optimization avoids allocating intermediate string slices during version string comparisons, improving parsing performance in these code paths. Co-authored-by: himattm <6266621+himattm@users.noreply.github.com> --- .jules/bolt.md | 4 ++++ internal/plugin/manager.go | 23 ++++++++++++----------- internal/update/update.go | 23 ++++++++++++----------- 3 files changed, 28 insertions(+), 22 deletions(-) diff --git a/.jules/bolt.md b/.jules/bolt.md index c214509..4f3c926 100644 --- a/.jules/bolt.md +++ b/.jules/bolt.md @@ -21,3 +21,7 @@ ## 2025-05-10 - fmt.Sscanf is lenient, strconv.Atoi is strict **Learning:** In Go, `fmt.Sscanf("%d")` parses digits until it encounters a non-digit character (e.g., `"2beta"` parses as `2`), while `strconv.Atoi` fails and returns `0` for the entire string. If you need the lenient parsing behavior of `fmt.Sscanf` for performance optimization, implement a custom byte-traversal loop to extract leading digits rather than relying on `strconv.Atoi` or regex, as it is over 10x faster and maintains exact functional parity. **Action:** When replacing `fmt.Sscanf` for performance, always evaluate whether the leniency of the parser is being implicitly relied upon by the surrounding code. + +## 2024-06-10 - Avoid strings.Split in parsing loops +**Learning:** Using `strings.Split` in tight parsing loops (like version string comparisons) causes unnecessary slice allocations and degrades performance. +**Action:** To avoid this overhead and improve speed, use a `for` loop with `strings.Cut` to incrementally consume and parse the delimited string without allocating intermediate slices. diff --git a/internal/plugin/manager.go b/internal/plugin/manager.go index 7aad027..4ed4f8a 100644 --- a/internal/plugin/manager.go +++ b/internal/plugin/manager.go @@ -832,22 +832,23 @@ func parseLenientAtoi(s string) int { // CompareVersions compares two semver strings // Returns -1 if a < b, 0 if a == b, 1 if a > b func CompareVersions(a, b string) int { - partsA := strings.Split(a, ".") - partsB := strings.Split(b, ".") + for a != "" || b != "" { + var partA, partB string - maxLen := len(partsA) - if len(partsB) > maxLen { - maxLen = len(partsB) - } + if a != "" { + partA, a, _ = strings.Cut(a, ".") + } + if b != "" { + partB, b, _ = strings.Cut(b, ".") + } - for i := 0; i < maxLen; i++ { var numA, numB int - if i < len(partsA) { + if partA != "" { // Bolt optimization: using a custom loop is ~14x faster than fmt.Sscanf - numA = parseLenientAtoi(partsA[i]) + numA = parseLenientAtoi(partA) } - if i < len(partsB) { - numB = parseLenientAtoi(partsB[i]) + if partB != "" { + numB = parseLenientAtoi(partB) } if numA < numB { diff --git a/internal/update/update.go b/internal/update/update.go index 7bf6684..4b2637b 100644 --- a/internal/update/update.go +++ b/internal/update/update.go @@ -157,21 +157,22 @@ func fetchLatestVersion(ctx context.Context) (string, error) { } func compareVersions(a, b string) int { - partsA := strings.Split(a, ".") - partsB := strings.Split(b, ".") + for a != "" || b != "" { + var partA, partB string - maxLen := len(partsA) - if len(partsB) > maxLen { - maxLen = len(partsB) - } + if a != "" { + partA, a, _ = strings.Cut(a, ".") + } + if b != "" { + partB, b, _ = strings.Cut(b, ".") + } - for i := 0; i < maxLen; i++ { var numA, numB int - if i < len(partsA) { - numA, _ = strconv.Atoi(partsA[i]) + if partA != "" { + numA, _ = strconv.Atoi(partA) } - if i < len(partsB) { - numB, _ = strconv.Atoi(partsB[i]) + if partB != "" { + numB, _ = strconv.Atoi(partB) } if numA < numB {