perf(emoji): optimize FindEmojiSubmatchIndex using slice-based Trie (#38573)

This pull request optimizes `FindEmojiSubmatchIndex` in Gitea's emoji
package (`modules/emoji/emoji.go`) by replacing the
`strings.Replacer`-based search with a slice-based trie and a
constant-time starting-byte check (`isStartingByte`).

The new implementation avoids heap allocations during the search and
reduces CPU overhead when rendering Markdown, particularly for plain
text that does not contain emojis.

### Verification

Verified with unit tests:

```sh
go test -count=1 ./modules/emoji/...
```

Benchmarks:

```sh
go test -bench=. -benchmem ./modules/emoji/...
```

### Results

| Benchmark | Before | After |
| ---------- | ------ | ----- |
| `BenchmarkFindEmojiSubmatchIndex` | 168.3 ns/op, 2 allocs/op | 85.78
ns/op, 1 alloc/op |
| `BenchmarkFindEmojiSubmatchIndexNoMatch` | 239.8 ns/op, 1 alloc/op |
105.1 ns/op, 0 allocs/op |
### Benchmark Output

```text
goos: linux
goarch: amd64
pkg: gitea.dev/modules/emoji
cpu: Intel(R) Core(TM) i5-7400 CPU @ 3.00GHz

BenchmarkFindEmojiSubmatchIndex-4              13498539        85.78 ns/op      16 B/op   1 allocs/op
BenchmarkFindEmojiSubmatchIndexNoMatch-4       11220450       105.1 ns/op        0 B/op   0 allocs/op
BenchmarkFindEmojiSubmatchIndexOld-4            6569360       168.3 ns/op      48 B/op   2 allocs/op
BenchmarkFindEmojiSubmatchIndexOldNoMatch-4     5026116       239.8 ns/op      32 B/op   1 allocs/op
```

---------

Signed-off-by: Sudhanshu Singh <sudhanshuwriterblc@gmail.com>
Co-authored-by: silverwind <me@silverwind.io>
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
This commit is contained in:
Shudhanshu Singh
2026-07-23 08:29:28 +00:00
committed by GitHub
co-authored by silverwind wxiaoguang
parent acbdc44d00
commit ee10ae168c
4 changed files with 156 additions and 76 deletions
+39 -7
View File
@@ -61,13 +61,18 @@ func TestReplacers(t *testing.T) {
}
}
const (
testInputWithEmojis = "This is a test string containing some emojis like \U0001f44d and \U0001f37a and some text in between."
testInputNoEmojis = "This is a test string containing no emojis at all, just plain old ASCII text, which should ideally be scanned very quickly by our trie implementation."
)
func TestFindEmojiSubmatchIndex(t *testing.T) {
type testcase struct {
teststring string
expected []int
input string
expected []int
}
testcases := []testcase{
testCases := []testcase{
{
"\U0001f44d",
[]int{0, len("\U0001f44d")},
@@ -81,13 +86,40 @@ func TestFindEmojiSubmatchIndex(t *testing.T) {
[]int{1, 1 + len("\U0001f44d")},
},
{
string([]byte{'\u0001'}) + "\U0001f44d",
"\u0001\U0001f44d",
[]int{1, 1 + len("\U0001f44d")},
},
{
// This package can handle keycap emoji if it is registered in the emoji data.
// However, many other places (e.g.: markup rendering) also might not handle such cases correctly.
// For example: how is "**{U+FE0F}{U+20E3}**" rendered in Markdown/Markup?
"a 8\U0000fe0f\U000020e3 b", // keycap emoji "8\ufe0f\u20e3" in emoji data
[]int{2, 2 + len("8\U0000fe0f\U000020e3")},
},
{
testInputWithEmojis,
[]int{50, 54},
},
{
testInputNoEmojis,
nil,
},
}
for _, kase := range testcases {
actual := FindEmojiSubmatchIndex(kase.teststring)
assert.Equal(t, kase.expected, actual)
for _, tc := range testCases {
actual := FindEmojiSubmatchIndex(tc.input)
assert.Equal(t, tc.expected, actual)
}
}
func BenchmarkFindEmojiSubmatchIndex(b *testing.B) {
for i := 0; i < b.N; i++ {
_ = FindEmojiSubmatchIndex(testInputWithEmojis)
}
}
func BenchmarkFindEmojiSubmatchIndexNoMatch(b *testing.B) {
for i := 0; i < b.N; i++ {
_ = FindEmojiSubmatchIndex(testInputNoEmojis)
}
}