diff options
author | pg9182 <96569817+pg9182@users.noreply.github.com> | 2022-10-23 13:22:13 -0400 |
---|---|---|
committer | pg9182 <96569817+pg9182@users.noreply.github.com> | 2022-10-23 13:22:13 -0400 |
commit | 0ca336dd6f25a7fe7e8c09bffe7b5d96cd7617ee (patch) | |
tree | 12452ff679b741a2ab6b6435e35d5eca0f068eac | |
parent | 699b5f0d81e0ffd903bfa08cd057ac368b8a1447 (diff) | |
download | Atlas-0ca336dd6f25a7fe7e8c09bffe7b5d96cd7617ee.tar.gz Atlas-0ca336dd6f25a7fe7e8c09bffe7b5d96cd7617ee.zip |
pkg/api/api0: Improve serverlist JSON generation
-rw-r--r-- | pkg/api/api0/serverlist.go | 156 |
1 files changed, 98 insertions, 58 deletions
diff --git a/pkg/api/api0/serverlist.go b/pkg/api/api0/serverlist.go index 021af65..710fbf9 100644 --- a/pkg/api/api0/serverlist.go +++ b/pkg/api/api0/serverlist.go @@ -43,6 +43,7 @@ type ServerList struct { csUpdatePf bool // ensures only one update runs at a time csUpdateCv *sync.Cond // allows other goroutines to wait for that update to complete csBytes atomic.Pointer[[]byte] // contents of buffer must not be modified; only swapped + csEst atomic.Uint64 // estimated per-server json size // /client/servers gzipped json csgzPool sync.Pool // gzip writer pool @@ -260,60 +261,98 @@ func (s *ServerList) csGetJSON() []byte { return ss[i].Order < ss[j].Order }) - // generate the json + // generate the json and cache it // // note: we write it manually to avoid copying the entire list and to avoid the perf overhead of reflection - var b bytes.Buffer - b.WriteByte('[') + buf, est := csJSON(ss, int(s.csEst.Load())) + s.csBytes.Store(&buf) + s.csEst.Store(uint64(est)) + + return buf +} + +func csJSON(ss []*Server, est int) ([]byte, int) { + if len(ss) == 0 { + return []byte(`[]`), est + } + + const ( + estMin = 256 + estInit = 384 + estMax = 512 + ) + switch { + case est == 0: + est = estInit + case est < estMin: + est = estMin + case est > estMax: + est = estMax + } + + // note: we use a custom buffer so we can control allocations + + b := make([]byte, 0, len(ss)*est+2) + b = append(b, '[') for i, srv := range ss { + if r := len(ss) - i - 1; r >= 0 && cap(b)-len(b) < est*r { + bn := make([]byte, len(b), cap(b)+est*r) + copy(bn, b) + b = bn + } if i != 0 { - b.WriteByte(',') + b = append(b, ',') } - b.WriteString(`{"lastHeartbeat":`) - b.WriteString(strconv.FormatInt(srv.LastHeartbeat.UnixMilli(), 10)) - b.WriteString(`,"id":`) - encodeJSONString(&b, []byte(srv.ID)) - b.WriteString(`,"name":`) - encodeJSONString(&b, []byte(srv.Name)) - b.WriteString(`,"description":`) - encodeJSONString(&b, []byte(srv.Description)) - b.WriteString(`,"playerCount":`) - b.WriteString(strconv.FormatInt(int64(srv.PlayerCount), 10)) - b.WriteString(`,"maxPlayers":`) - b.WriteString(strconv.FormatInt(int64(srv.MaxPlayers), 10)) - b.WriteString(`,"map":`) - encodeJSONString(&b, []byte(srv.Map)) - b.WriteString(`,"playlist":`) - encodeJSONString(&b, []byte(srv.Playlist)) + b = append(b, `{"lastHeartbeat":`...) + b = strconv.AppendInt(b, srv.LastHeartbeat.UnixMilli(), 10) + b = append(b, `,"id":"`...) + b = append(b, srv.ID...) + b = append(b, `","name":`...) + b = appendJSONString(b, srv.Name) + b = append(b, `,"description":`...) + b = appendJSONString(b, srv.Description) + b = append(b, `,"playerCount":`...) + b = strconv.AppendInt(b, int64(srv.PlayerCount), 10) + b = append(b, `,"maxPlayers":`...) + b = strconv.AppendInt(b, int64(srv.MaxPlayers), 10) + b = append(b, `,"map":`...) + b = appendJSONString(b, srv.Map) + b = append(b, `,"playlist":`...) + b = appendJSONString(b, srv.Playlist) if srv.Password != "" { - b.WriteString(`,"hasPassword":true`) + b = append(b, `,"hasPassword":true`...) } else { - b.WriteString(`,"hasPassword":false`) + b = append(b, `,"hasPassword":false`...) } - b.WriteString(`,"modInfo":{"Mods":[`) + b = append(b, `,"modInfo":{"Mods":[`...) for j, mi := range srv.ModInfo { if j != 0 { - b.WriteByte(',') + b = append(b, ',') } - b.WriteString(`{"Name":`) - encodeJSONString(&b, []byte(mi.Name)) - b.WriteString(`,"Version":`) - encodeJSONString(&b, []byte(mi.Version)) + b = append(b, `{"Name":`...) + b = appendJSONString(b, mi.Name) + b = append(b, `,"Version":`...) + b = appendJSONString(b, mi.Version) if mi.RequiredOnClient { - b.WriteString(`,"RequiredOnClient":true}`) + b = append(b, `,"RequiredOnClient":true}`...) } else { - b.WriteString(`,"RequiredOnClient":false}`) + b = append(b, `,"RequiredOnClient":false}`...) } } - b.WriteString(`]}}`) + b = append(b, `]}}`...) } - b.WriteByte(']') - - // cache it - buf := b.Bytes() - s.csBytes.Store(&buf) - - return b.Bytes() + b = append(b, ']') + + est = (len(b) - 2 + (len(ss) - 1)) / len(ss) // note: round up + switch { + case est == 0: + est = estInit + case est < estMin: + est = estMin + case est > estMax: + est = estMax + } + return b, est } // csGetJSONGzip is like csGetJSON, but returns it gzipped with true, or false @@ -1136,11 +1175,11 @@ var jsonSafeSet = [utf8.RuneSelf]bool{ '\u007f': true, } -// encodeJSONString is based on encoding/json.encodeState.stringBytes. -func encodeJSONString(e *bytes.Buffer, s []byte) { +// appendJSONString is based on encoding/json.encodeState.stringBytes. +func appendJSONString(e []byte, s string) []byte { const hex = "0123456789abcdef" - e.WriteByte('"') + e = append(e, '"') start := 0 for i := 0; i < len(s); { if b := s[i]; b < utf8.RuneSelf { @@ -1149,38 +1188,38 @@ func encodeJSONString(e *bytes.Buffer, s []byte) { continue } if start < i { - e.Write(s[start:i]) + e = append(e, s[start:i]...) } - e.WriteByte('\\') + e = append(e, '\\') switch b { case '\\', '"': - e.WriteByte(b) + e = append(e, b) case '\n': - e.WriteByte('n') + e = append(e, 'n') case '\r': - e.WriteByte('r') + e = append(e, 'r') case '\t': - e.WriteByte('t') + e = append(e, 't') default: // This encodes bytes < 0x20 except for \t, \n and \r. // If escapeHTML is set, it also escapes <, >, and & // because they can lead to security holes when // user-controlled strings are rendered into JSON // and served to some browsers. - e.WriteString(`u00`) - e.WriteByte(hex[b>>4]) - e.WriteByte(hex[b&0xF]) + e = append(e, `u00`...) + e = append(e, hex[b>>4]) + e = append(e, hex[b&0xF]) } i++ start = i continue } - c, size := utf8.DecodeRune(s[i:]) + c, size := utf8.DecodeRune([]byte(s[i:])) if c == utf8.RuneError && size == 1 { if start < i { - e.Write(s[start:i]) + e = append(e, s[start:i]...) } - e.WriteString(`\ufffd`) + e = append(e, `\ufffd`...) i += size start = i continue @@ -1194,10 +1233,10 @@ func encodeJSONString(e *bytes.Buffer, s []byte) { // See http://timelessrepo.com/json-isnt-a-javascript-subset for discussion. if c == '\u2028' || c == '\u2029' { if start < i { - e.Write(s[start:i]) + e = append(e, s[start:i]...) } - e.WriteString(`\u202`) - e.WriteByte(hex[c&0xF]) + e = append(e, `\u202`...) + e = append(e, hex[c&0xF]) i += size start = i continue @@ -1205,7 +1244,8 @@ func encodeJSONString(e *bytes.Buffer, s []byte) { i += size } if start < len(s) { - e.Write(s[start:]) + e = append(e, s[start:]...) } - e.WriteByte('"') + e = append(e, '"') + return e } |