Files
Gitea/modules/emoji/emoji.go
T
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

147 lines
3.9 KiB
Go

// Copyright 2020 The Gitea Authors. All rights reserved.
// Copyright 2015 Kenneth Shaw
// SPDX-License-Identifier: MIT
package emoji
import (
"sort"
"strings"
"sync/atomic"
"gitea.dev/modules/setting"
"gitea.dev/modules/util"
)
// Gemoji is a set of emoji data.
type Gemoji []Emoji
// Emoji represents a single emoji and associated data.
type Emoji struct {
Emoji string
Description string
Aliases []string
UnicodeVersion string
SkinTones bool
}
type globalVarsStruct struct {
codeMap map[string]int // emoji Unicode code to its emoji data.
aliasMap map[string]int // the alias to its emoji data.
trie *util.TrieNode // trie for finding emoji positions.
isStartingByte [256]bool // fast-path skip for starting bytes.
codeReplacer *strings.Replacer // string replacer for emoji codes.
aliasReplacer *strings.Replacer // string replacer for emoji aliases.
}
var globalVarsStore atomic.Pointer[globalVarsStruct]
func globalVars() *globalVarsStruct {
vars := globalVarsStore.Load()
if vars != nil {
return vars
}
// although there can be concurrent calls, the result should be the same, and there is no performance problem
vars = &globalVarsStruct{}
vars.codeMap = make(map[string]int, len(GemojiData))
vars.aliasMap = make(map[string]int, len(GemojiData))
vars.trie = &util.TrieNode{}
// process emoji codes and aliases
codePairs := make([]string, 0)
aliasPairs := make([]string, 0)
// sort from largest to small so we match combined emoji first
sort.Slice(GemojiData, func(i, j int) bool {
return len(GemojiData[i].Emoji) > len(GemojiData[j].Emoji)
})
for idx, emoji := range GemojiData {
if emoji.Emoji == "" || len(emoji.Aliases) == 0 {
continue
}
// process aliases
firstAlias := ""
for _, alias := range emoji.Aliases {
if alias == "" {
continue
}
enabled := len(setting.UI.EnabledEmojisSet) == 0 || setting.UI.EnabledEmojisSet.Contains(alias)
if !enabled {
continue
}
if firstAlias == "" {
firstAlias = alias
}
vars.aliasMap[alias] = idx
aliasPairs = append(aliasPairs, ":"+alias+":", emoji.Emoji)
}
// process emoji code
if firstAlias != "" {
vars.codeMap[emoji.Emoji] = idx
codePairs = append(codePairs, emoji.Emoji, ":"+emoji.Aliases[0]+":")
vars.trie.Insert(emoji.Emoji)
vars.isStartingByte[emoji.Emoji[0]] = true
}
}
// create replacers
vars.codeReplacer = strings.NewReplacer(codePairs...)
vars.aliasReplacer = strings.NewReplacer(aliasPairs...)
globalVarsStore.Store(vars)
return vars
}
// FromCode retrieves the emoji data based on the provided Unicode code
// e.g.: "\u2618" will return the Gemoji data for "shamrock".
func FromCode(code string) *Emoji {
i, ok := globalVars().codeMap[code]
if !ok {
return nil
}
return &GemojiData[i]
}
// FromAlias retrieves the emoji data based on the provided alias in the form "alias" or ":alias:"
// e.g.: "shamrock" or ":shamrock:" will return the Gemoji data for "shamrock".
func FromAlias(alias string) *Emoji {
if strings.HasPrefix(alias, ":") && strings.HasSuffix(alias, ":") {
alias = alias[1 : len(alias)-1]
}
i, ok := globalVars().aliasMap[alias]
if !ok {
return nil
}
return &GemojiData[i]
}
// ReplaceCodes replaces all emoji codes with the first corresponding emoji alias in the form of ":alias:"
// e.g.: "\u2618" will be converted to ":shamrock:".
func ReplaceCodes(s string) string {
return globalVars().codeReplacer.Replace(s)
}
// ReplaceAliases replaces all aliases of the form ":alias:" with its corresponding Unicode value.
func ReplaceAliases(s string) string {
return globalVars().aliasReplacer.Replace(s)
}
// FindEmojiSubmatchIndex returns index-pair of the first emoji in a string
func FindEmojiSubmatchIndex(s string) []int {
vars := globalVars()
for i := 0; i < len(s); i++ {
if !vars.isStartingByte[s[i]] {
continue
}
if matchLen := vars.trie.Match(s, i); matchLen > 0 {
return []int{i, i + matchLen}
}
}
return nil
}