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 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/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** 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