perf(references): scan only the keyword window before a reference (#39396)

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 <me@silverwind.io>
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
This commit is contained in:
Roland
2026-09-24 07:12:02 +00:00
committed by GitHub
co-authored by silverwind wxiaoguang
parent 2177969aba
commit 8b46a956d8
2 changed files with 20 additions and 7 deletions
+13 -7
View File
@@ -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
+7
View File
@@ -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{