Skip to content

Commit

Permalink
go api: reduce allocations in deck decompression
Browse files Browse the repository at this point in the history
Previously we would allocate quite often in order to decompress and
parse the deck strings that were given to us. This could be alleviated by
precomputing some of the regular expressions, preallocating, and using
manual conversion logic instead of relying on json.

This could obviously be taken much further, but this seemed like a good
compromise on benefit while still retaining readability.

Benchmark comparison:
```
goos: linux
goarch: amd64
pkg: github.com/MaT1g3R/slaytherelics/api
cpu: AMD Ryzen 9 5900X 12-Core Processor
                              │  before.txt  │              after.txt              │
                              │    sec/op    │   sec/op     vs base                │
DecompressDeck/Simple_deck-24    7.934µ ± 1%   1.318µ ± 0%  -83.39% (p=0.000 n=10)
DecompressDeck/Big_deck-24      227.58µ ± 1%   60.34µ ± 1%  -73.48% (p=0.000 n=10)
geomean                          42.49µ        8.916µ       -79.02%

                              │  before.txt  │              after.txt               │
                              │     B/op     │     B/op      vs base                │
DecompressDeck/Simple_deck-24    5494.5 ± 0%     836.0 ± 0%  -84.78% (p=0.000 n=10)
DecompressDeck/Big_deck-24      330.8Ki ± 0%   219.2Ki ± 0%  -33.72% (p=0.000 n=10)
geomean                         42.13Ki        13.38Ki       -68.24%

                              │ before.txt  │             after.txt              │
                              │  allocs/op  │ allocs/op   vs base                │
DecompressDeck/Simple_deck-24    94.00 ± 0%   17.00 ± 0%  -81.91% (p=0.000 n=10)
DecompressDeck/Big_deck-24      1810.0 ± 0%   165.0 ± 0%  -90.88% (p=0.000 n=10)
geomean                          412.5        52.96       -87.16%
```
  • Loading branch information
zdylag authored and MaT1g3R committed Oct 10, 2024
1 parent 9115ad5 commit caf7d31
Showing 1 changed file with 50 additions and 35 deletions.
85 changes: 50 additions & 35 deletions backend/api/deck.go
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
package api

import (
"encoding/json"
"errors"
"fmt"
"regexp"
"strconv"
"strings"

"github.com/gin-gonic/gin"
Expand All @@ -13,6 +13,19 @@ import (

const WILDCARDS = "0123456789abcdefghijklmnopqrstvwxyzABCDEFGHIJKLMNOPQRSTVWXYZ_`[]/^%?@><=-+*:;,.()#$!'{}~"

var compressionWildcardRegex []*regexp.Regexp

func init() {
escapeRegex := regexp.MustCompile(`[-\/\\^$*+?.()|[\]{}]`)

compressionWildcardRegex = make([]*regexp.Regexp, 0, len(WILDCARDS))
for i := range WILDCARDS {
wildCard := fmt.Sprintf("&%c", WILDCARDS[i])
escaped := escapeRegex.ReplaceAllString(wildCard, "\\$&")
compressionWildcardRegex = append(compressionWildcardRegex, regexp.MustCompile(escaped))
}
}

func (a *API) getDeckHandler(c *gin.Context) {
name := c.Param("name")
name = strings.ToLower(name)
Expand Down Expand Up @@ -60,11 +73,6 @@ func (a *API) getDeckHandler(c *gin.Context) {
c.Data(200, "text/plain", []byte(result.String()))
}

func escapeRegexp(s string) string {
r := regexp.MustCompile(`[-\/\\^$*+?.()|[\]{}]`)
return r.ReplaceAllString(s, "\\$&")
}

func decompress(s string) (string, error) {
parts := strings.Split(s, "||")
if len(parts) < 2 {
Expand All @@ -76,51 +84,58 @@ func decompress(s string) (string, error) {

for i := len(compressionDict) - 1; i >= 0; i-- {
word := compressionDict[i]
wildCard := fmt.Sprintf("&%c", WILDCARDS[i])
r, err := regexp.Compile(escapeRegexp(wildCard))
if err != nil {
return "", err
}
text = r.ReplaceAllString(text, word)
text = compressionWildcardRegex[i].ReplaceAllString(text, word)
}
return text, nil
}

func parseCommaDelimitedIntegerArray(s string) ([]int, error) {
if s == "-" {
return make([]int, 0), nil
return nil, nil
}
//nolint:prealloc
var result []int
err := json.Unmarshal([]byte(fmt.Sprintf("[%s]", s)), &result)
return result, err

currIndex := 0
result := make([]int, 0, strings.Count(s, ",")+1)
for currIndex < len(s) {
nextIndex := currIndex + strings.Index(s[currIndex:], ",")
if nextIndex < currIndex {
nextIndex = len(s)
}

resultVal, err := strconv.ParseInt(s[currIndex:nextIndex], 10, 64)
if err != nil {
return nil, err
}

currIndex = nextIndex + 1
result = append(result, int(resultVal))
}
return result, nil
}

func splitSemicolonDelimited2DArray(s string) [][]string {
func splitDoubleSemicolonArray(s string) []string {
if s == "-" {
return make([][]string, 0)
return nil
}

//nolint:prealloc
var result [][]string
split := strings.Split(s, ";;")
for _, element := range split {
result = append(result, strings.Split(element, ";"))
}
return result
return strings.Split(s, ";;")
}

func parseCards(cards [][]string) []string {
//nolint:prealloc
var result []string
for _, card := range cards {
result = append(result, parseCard(card))
func parseCards(cardSections []string) []string {
result := make([]string, 0, len(cardSections))
for _, cardSection := range cardSections {
result = append(result, parseCard(cardSection))
}
return result
}

func parseCard(c []string) string {
return c[0]
func parseCard(cardSection string) string {
sectionEnd := strings.Index(cardSection, ";")
if sectionEnd == -1 {
return cardSection
}

return cardSection[:sectionEnd]
}

func decompressDeck(deck string) (map[string]int, error) {
Expand All @@ -134,9 +149,9 @@ func decompressDeck(deck string) (map[string]int, error) {
if err != nil {
return nil, err
}
cards := parseCards(splitSemicolonDelimited2DArray(parts[1]))
cards := parseCards(splitDoubleSemicolonArray(parts[1]))

deckDict := make(map[string]int)
deckDict := make(map[string]int, len(cards))
for _, idx := range d {
if idx < 0 || idx >= len(cards) {
return nil, errors.New("card index out of bounds")
Expand Down

0 comments on commit caf7d31

Please sign in to comment.