From 8b46a956d8d1e474272f14cb1900f62df06c5c45 Mon Sep 17 00:00:00 2001 From: Roland Date: Thu, 24 Sep 2026 15:12:02 +0800 Subject: [PATCH] perf(references): scan only the keyword window before a reference (#39396) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes https://github.com/go-gitea/gitea/issues/39395 `findActionKeywords` ran the close and reopen keyword patterns over all content before each reference, making `FindAllIssueReferences` quadratic on comments and commit messages. The patterns are anchored at the reference, so only the last few bytes can match. Scan only that window, sized from the longest keyword with room for `(?i)` matching wider runes like `ſ` for `s`. `"#1 "` repeated 4000 times: 2.04 s before, 16 ms after. --------- Co-authored-by: silverwind Co-authored-by: wxiaoguang --- modules/references/references.go | 20 +++++++++++++------- modules/references/references_test.go | 7 +++++++ 2 files changed, 20 insertions(+), 7 deletions(-) diff --git a/modules/references/references.go b/modules/references/references.go index d3d451269fd..0d470d479ae 100644 --- a/modules/references/references.go +++ b/modules/references/references.go @@ -7,9 +7,11 @@ import ( "bytes" "net/url" "regexp" + "slices" "strconv" "strings" "sync" + "unicode/utf8" "gitea.dev/modules/log" "gitea.dev/modules/markup/mdstripper" @@ -45,6 +47,7 @@ var ( timeLogPattern = regexp.MustCompile(`(?:\s|^|\(|\[)(@([0-9]+([\.,][0-9]+)?(w|d|m|h))+)(?:\s|$|\)|\]|[:;,.?!]\s|[:;,.?!]$)`) issueCloseKeywordsPat, issueReopenKeywordsPat *regexp.Regexp + issueKeywordWindow int issueKeywordsOnce sync.Once giteaHostInit sync.Once @@ -167,6 +170,10 @@ func newKeywords() { func doNewKeywords(closeKeywords, reopenKeywords []string) { issueCloseKeywordsPat = makeKeywordsPat(closeKeywords) issueReopenKeywordsPat = makeKeywordsPat(reopenKeywords) + issueKeywordWindow = 0 + for _, word := range slices.Concat(closeKeywords, reopenKeywords) { + issueKeywordWindow = max(issueKeywordWindow, utf8.RuneCountInString(word)*utf8.UTFMax+3) // delimiter, keyword with (?i)-folded runes like ſ for s, ": " + } } // getGiteaHostName returns a normalized string with the local host name, with no scheme or port information @@ -599,17 +606,16 @@ func getCrossReference(content []byte, start, end int, fromLink, prOnly bool) *r func findActionKeywords(content []byte, start int) (XRefAction, *RefSpan) { newKeywords() - var m []int + windowStart := max(start-issueKeywordWindow, 0) + prefix := content[windowStart:start] if issueCloseKeywordsPat != nil { - m = issueCloseKeywordsPat.FindSubmatchIndex(content[:start]) - if m != nil { - return XRefActionCloses, &RefSpan{Start: m[2], End: m[3]} + if m := issueCloseKeywordsPat.FindSubmatchIndex(prefix); m != nil { + return XRefActionCloses, &RefSpan{Start: windowStart + m[2], End: windowStart + m[3]} } } if issueReopenKeywordsPat != nil { - m = issueReopenKeywordsPat.FindSubmatchIndex(content[:start]) - if m != nil { - return XRefActionReopens, &RefSpan{Start: m[2], End: m[3]} + if m := issueReopenKeywordsPat.FindSubmatchIndex(prefix); m != nil { + return XRefActionReopens, &RefSpan{Start: windowStart + m[2], End: windowStart + m[3]} } } return XRefActionNone, nil diff --git a/modules/references/references_test.go b/modules/references/references_test.go index 222b93d4092..53cd98a931c 100644 --- a/modules/references/references_test.go +++ b/modules/references/references_test.go @@ -162,6 +162,13 @@ func TestFindAllIssueReferences(t *testing.T) { {1235, "", "", "1235", false, XRefActionNone, &RefSpan{Start: 8, End: 13}, nil, ""}, }, }, + { + "After a long investigation into the root cause, this resolves #1, unlike the still unresolved #2", + []testResult{ + {1, "", "", "1", false, XRefActionCloses, &RefSpan{Start: 62, End: 64}, &RefSpan{Start: 53, End: 61}, ""}, + {2, "", "", "2", false, XRefActionNone, &RefSpan{Start: 94, End: 96}, nil, ""}, + }, + }, { "For [!123] yes", []testResult{