Files
gitea/modules/util/trie.go
Shudhanshu Singh ee10ae168c 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>
2026-07-23 08:29:28 +00:00

57 lines
1.2 KiB
Go

// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package util
type trieEdge struct {
b byte
node *TrieNode
}
// TrieNode represents a node in a slice-based Trie for fast byte-sequence prefix matching.
type TrieNode struct {
children []trieEdge
isEnd bool
}
func (t *TrieNode) child(b byte) *TrieNode {
for _, edge := range t.children {
if edge.b == b {
return edge.node
}
}
return nil
}
// Insert adds a string (byte sequence) to the Trie.
func (t *TrieNode) Insert(val string) {
curr := t
for i := 0; i < len(val); i++ {
next := curr.child(val[i])
if next == nil {
next = &TrieNode{}
curr.children = append(curr.children, trieEdge{b: val[i], node: next})
}
curr = next
}
curr.isEnd = true
}
// Match returns the length of the longest matching prefix starting at index `start` in string `s`.
// It returns -1 if no prefix is matched.
func (t *TrieNode) Match(s string, start int) int {
curr := t
matchLen := -1
for j := start; j < len(s); j++ {
next := curr.child(s[j])
if next == nil {
break
}
curr = next
if curr.isEnd {
matchLen = j - start + 1
}
}
return matchLen
}