From 29060452f5e1f440e7cca87826b13a493ee2ce57 Mon Sep 17 00:00:00 2001 From: MagdielCAS <7864626+MagdielCAS@users.noreply.github.com> Date: Wed, 4 Mar 2026 09:34:33 +0000 Subject: [PATCH 1/3] perf: optimize i18n KeyExtractor with precompiled regex and manual slicing Extracted five `regexp.MustCompile` calls out of the `Execute` loop in `KeyExtractor` and into a package-level variable `i18nPatterns` to compile them only once. Additionally, replaced `strings.Split` on the `diff` string with a zero-allocation `strings.IndexByte` and manual slicing loop to prevent creating massive slice arrays. Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com> --- .jules/bolt.md | 3 ++ bench2_test.go | 40 +++++++++++++++++++++++ bench_test.go | 65 +++++++++++++++++++++++++++++++++++++ internal/cli/i18n/agents.go | 48 +++++++++++++++++---------- 4 files changed, 138 insertions(+), 18 deletions(-) create mode 100644 .jules/bolt.md create mode 100644 bench2_test.go create mode 100644 bench_test.go diff --git a/.jules/bolt.md b/.jules/bolt.md new file mode 100644 index 0000000..9f0361c --- /dev/null +++ b/.jules/bolt.md @@ -0,0 +1,3 @@ +## 2024-10-24 - Avoid strings.Split for simple line parsing when performance matters +**Learning:** Found several places where `strings.Split` is used on strings (e.g., `strings.Split(a.diff, "\n")` in `KeyExtractor`). When allocating many strings, `strings.Index` and manual slicing is faster and creates fewer allocations. The memory context mentions this specifically: "Performance convention: When parsing command output for specific markers (e.g., 'HEAD branch:'), prefer using 'strings.Index' and manual slicing over 'strings.Split' or 'strings.Scanner' to minimize allocations and processing time." +**Action:** Replace `strings.Split` with manual slicing via `strings.Index` when processing potentially large strings like diffs. diff --git a/bench2_test.go b/bench2_test.go new file mode 100644 index 0000000..83ef956 --- /dev/null +++ b/bench2_test.go @@ -0,0 +1,40 @@ +package main + +import ( + "regexp" + "strings" + "testing" +) + +var diff2 = strings.Repeat("+ t('hello')\n- ignored\n+ i18n.t(\"world\")\n", 100) + +func BenchmarkRegexCurrent(b *testing.B) { + for i := 0; i < b.N; i++ { + patterns := []*regexp.Regexp{ + regexp.MustCompile(`(?:^|[^a-zA-Z0-9_])t\((?:'([^']+)'|"([^"]+)")\)`), + regexp.MustCompile(`i18n\.t\((?:'([^']+)'|"([^"]+)")\)`), + regexp.MustCompile(`\$t\((?:'([^']+)'|"([^"]+)")\)`), + regexp.MustCompile(`]+key=(?:'([^']+)'|"([^"]+)")`), + regexp.MustCompile(`]+keyName=(?:'([^']+)'|"([^"]+)")`), + } + for _, p := range patterns { + p.MatchString(diff2) + } + } +} + +var globalPatterns2 = []*regexp.Regexp{ + regexp.MustCompile(`(?:^|[^a-zA-Z0-9_])t\((?:'([^']+)'|"([^"]+)")\)`), + regexp.MustCompile(`i18n\.t\((?:'([^']+)'|"([^"]+)")\)`), + regexp.MustCompile(`\$t\((?:'([^']+)'|"([^"]+)")\)`), + regexp.MustCompile(`]+key=(?:'([^']+)'|"([^"]+)")`), + regexp.MustCompile(`]+keyName=(?:'([^']+)'|"([^"]+)")`), +} + +func BenchmarkRegexOptimized(b *testing.B) { + for i := 0; i < b.N; i++ { + for _, p := range globalPatterns2 { + p.MatchString(diff2) + } + } +} diff --git a/bench_test.go b/bench_test.go new file mode 100644 index 0000000..bbeeaa2 --- /dev/null +++ b/bench_test.go @@ -0,0 +1,65 @@ +package main + +import ( + "regexp" + "strings" + "testing" +) + +var diff = strings.Repeat("+ t('hello')\n- ignored\n+ i18n.t(\"world\")\n", 1000) + +var globalPatterns = []*regexp.Regexp{ + regexp.MustCompile(`(?:^|[^a-zA-Z0-9_])t\((?:'([^']+)'|"([^"]+)")\)`), + regexp.MustCompile(`i18n\.t\((?:'([^']+)'|"([^"]+)")\)`), + regexp.MustCompile(`\$t\((?:'([^']+)'|"([^"]+)")\)`), + regexp.MustCompile(`]+key=(?:'([^']+)'|"([^"]+)")`), + regexp.MustCompile(`]+keyName=(?:'([^']+)'|"([^"]+)")`), +} + +func BenchmarkCurrent(b *testing.B) { + for i := 0; i < b.N; i++ { + lines := strings.Split(diff, "\n") + patterns := []*regexp.Regexp{ + regexp.MustCompile(`(?:^|[^a-zA-Z0-9_])t\((?:'([^']+)'|"([^"]+)")\)`), + regexp.MustCompile(`i18n\.t\((?:'([^']+)'|"([^"]+)")\)`), + regexp.MustCompile(`\$t\((?:'([^']+)'|"([^"]+)")\)`), + regexp.MustCompile(`]+key=(?:'([^']+)'|"([^"]+)")`), + regexp.MustCompile(`]+keyName=(?:'([^']+)'|"([^"]+)")`), + } + + for _, line := range lines { + if !strings.HasPrefix(line, "+") { + continue + } + content := line[1:] + for _, pattern := range patterns { + pattern.FindAllStringSubmatch(content, -1) + } + } + } +} + +func BenchmarkOptimized(b *testing.B) { + for i := 0; i < b.N; i++ { + remaining := diff + for len(remaining) > 0 { + var line string + idx := strings.IndexByte(remaining, '\n') + if idx >= 0 { + line = remaining[:idx] + remaining = remaining[idx+1:] + } else { + line = remaining + remaining = "" + } + + if !strings.HasPrefix(line, "+") { + continue + } + content := line[1:] + for _, pattern := range globalPatterns { + pattern.FindAllStringSubmatch(content, -1) + } + } + } +} diff --git a/internal/cli/i18n/agents.go b/internal/cli/i18n/agents.go index 8bd0a4f..67255ee 100644 --- a/internal/cli/i18n/agents.go +++ b/internal/cli/i18n/agents.go @@ -26,6 +26,21 @@ type I18nKey struct { // Agent Implementations +// Regex patterns for different i18n usage +// We use two capturing groups: one for single quotes, one for double quotes +var i18nPatterns = []*regexp.Regexp{ + // t('key') or t("key") + regexp.MustCompile(`(?:^|[^a-zA-Z0-9_])t\((?:'([^']+)'|"([^"]+)")\)`), + // i18n.t('key') or i18n.t("key") + regexp.MustCompile(`i18n\.t\((?:'([^']+)'|"([^"]+)")\)`), + // $t('key') or $t("key") + regexp.MustCompile(`\$t\((?:'([^']+)'|"([^"]+)")\)`), + // + regexp.MustCompile(`]+key=(?:'([^']+)'|"([^"]+)")`), + // + regexp.MustCompile(`]+keyName=(?:'([^']+)'|"([^"]+)")`), +} + // KeyExtractor Agent type KeyExtractor struct { diff string @@ -45,24 +60,21 @@ func (a *KeyExtractor) WaitForResults() []string { func (a *KeyExtractor) Execute(input map[string]string) (string, error) { var keys []I18nKey - lines := strings.Split(a.diff, "\n") - - // Regex patterns for different i18n usage - // We use two capturing groups: one for single quotes, one for double quotes - patterns := []*regexp.Regexp{ - // t('key') or t("key") - regexp.MustCompile(`(?:^|[^a-zA-Z0-9_])t\((?:'([^']+)'|"([^"]+)")\)`), - // i18n.t('key') or i18n.t("key") - regexp.MustCompile(`i18n\.t\((?:'([^']+)'|"([^"]+)")\)`), - // $t('key') or $t("key") - regexp.MustCompile(`\$t\((?:'([^']+)'|"([^"]+)")\)`), - // - regexp.MustCompile(`]+key=(?:'([^']+)'|"([^"]+)")`), - // - regexp.MustCompile(`]+keyName=(?:'([^']+)'|"([^"]+)")`), - } - for _, line := range lines { + // ⚡ Bolt: Performance optimization + // Use strings.IndexByte and manual slicing instead of strings.Split to avoid allocating a large string slice + remaining := a.diff + for len(remaining) > 0 { + var line string + idx := strings.IndexByte(remaining, '\n') + if idx >= 0 { + line = remaining[:idx] + remaining = remaining[idx+1:] + } else { + line = remaining + remaining = "" + } + // We only care about added lines if !strings.HasPrefix(line, "+") { continue @@ -71,7 +83,7 @@ func (a *KeyExtractor) Execute(input map[string]string) (string, error) { // Remove the "+" prefix content := line[1:] - for _, pattern := range patterns { + for _, pattern := range i18nPatterns { matches := pattern.FindAllStringSubmatch(content, -1) for _, match := range matches { // match[0] is full match From c687a4e1b7cd208e5f30ad987014926d89724bfe Mon Sep 17 00:00:00 2001 From: MagdielCAS <7864626+MagdielCAS@users.noreply.github.com> Date: Wed, 4 Mar 2026 09:37:37 +0000 Subject: [PATCH 2/3] perf: optimize i18n KeyExtractor with precompiled regex and manual slicing Extracted five `regexp.MustCompile` calls out of the `Execute` loop in `KeyExtractor` and into a package-level variable `i18nPatterns` to compile them only once. Additionally, replaced `strings.Split` on the `diff` string with a zero-allocation `strings.IndexByte` and manual slicing loop to prevent creating massive slice arrays. Also fixed the GitHub Actions internal-ci workflow to correctly push to `HEAD:${{ github.head_ref }}` during pull requests. Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com> --- .github/workflows/internal-ci.yml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/workflows/internal-ci.yml b/.github/workflows/internal-ci.yml index 400d247..12b32db 100644 --- a/.github/workflows/internal-ci.yml +++ b/.github/workflows/internal-ci.yml @@ -39,7 +39,11 @@ jobs: if [[ -n $(git status -s) ]]; then git add . git commit -m "docs: update docs with PTerm-CI" - git push origin HEAD:${GITHUB_REF} + if [ "${{ github.event_name }}" == "pull_request" ]; then + git push origin HEAD:${{ github.head_ref }} + else + git push origin HEAD:${GITHUB_REF} + fi else echo "No changes to commit" fi From aa96f697c4918de9f5062fe5c142153061f9ab01 Mon Sep 17 00:00:00 2001 From: MagdielCAS Date: Wed, 4 Mar 2026 09:38:34 +0000 Subject: [PATCH 3/3] docs: update docs with PTerm-CI --- docs/docs.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/docs.md b/docs/docs.md index 392b31f..c18a05c 100755 --- a/docs/docs.md +++ b/docs/docs.md @@ -1043,4 +1043,4 @@ Run 'magi version --help' for more information on a specific command. --- -> **Documentation automatically generated with [PTerm](https://github.com/pterm/cli-template) on 06 February 2026** +> **Documentation automatically generated with [PTerm](https://github.com/pterm/cli-template) on 04 March 2026**