From af38004a76a01811169ef0b2f1baf15109f710e5 Mon Sep 17 00:00:00 2001 From: Lou Knauer Date: Mon, 24 Jan 2022 09:55:33 +0100 Subject: [PATCH] better JSON encoding (less allocs) --- api.go | 23 +++++++++++++------- float.go | 58 +++++++++++++++++++++++++++++++++++++++++++++++++ lineprotocol.go | 37 ------------------------------- 3 files changed, 73 insertions(+), 45 deletions(-) create mode 100644 float.go diff --git a/api.go b/api.go index 38bccb5..d64f492 100644 --- a/api.go +++ b/api.go @@ -8,6 +8,7 @@ import ( "encoding/json" "errors" "fmt" + "io" "log" "math" "net/http" @@ -21,13 +22,13 @@ import ( ) type ApiMetricData struct { - Error *string `json:"error,omitempty"` - From int64 `json:"from"` - To int64 `json:"to"` - Data []Float `json:"data,omitempty"` - Avg Float `json:"avg"` - Min Float `json:"min"` - Max Float `json:"max"` + Error *string `json:"error,omitempty"` + From int64 `json:"from"` + To int64 `json:"to"` + Data FloatArray `json:"data,omitempty"` + Avg Float `json:"avg"` + Min Float `json:"min"` + Max Float `json:"max"` } // TODO: Optimize this, just like the stats endpoint! @@ -109,7 +110,13 @@ func handleWrite(rw http.ResponseWriter, r *http.Request) { return } - dec := lineprotocol.NewDecoder(bufio.NewReader(r.Body)) + bytes, err := io.ReadAll(r.Body) + if err != nil { + http.Error(rw, err.Error(), http.StatusInternalServerError) + return + } + + dec := lineprotocol.NewDecoderWithBytes(bytes) if err := decodeLine(dec); err != nil { http.Error(rw, err.Error(), http.StatusBadRequest) return diff --git a/float.go b/float.go new file mode 100644 index 0000000..818661a --- /dev/null +++ b/float.go @@ -0,0 +1,58 @@ +package main + +import ( + "math" + "strconv" +) + +// Go's JSON encoder for floats does not support NaN (https://github.com/golang/go/issues/3480). +// This program uses NaN as a signal for missing data. +// For the HTTP JSON API to be able to handle NaN values, +// we have to use our own type which implements encoding/json.Marshaler itself. +type Float float32 + +var NaN Float = Float(math.NaN()) +var nullAsBytes []byte = []byte("null") + +func (f Float) IsNaN() bool { + return math.IsNaN(float64(f)) +} + +func (f Float) MarshalJSON() ([]byte, error) { + if math.IsNaN(float64(f)) { + return nullAsBytes, nil + } + + return strconv.AppendFloat(make([]byte, 0, 10), float64(f), 'f', 1, 32), nil +} + +func (f *Float) UnmarshalJSON(input []byte) error { + if string(input) == "null" { + *f = NaN + return nil + } + + val, err := strconv.ParseFloat(string(input), 64) + if err != nil { + return err + } + *f = Float(val) + return nil +} + +// Same as `[]Float`, but can be marshaled to JSON with less allocations. +type FloatArray []Float + +func (fa FloatArray) MarshalJSON() ([]byte, error) { + buf := make([]byte, 0, 2+len(fa)*8) + buf = append(buf, '[') + if len(fa) > 0 { + buf = strconv.AppendFloat(buf, float64(fa[0]), 'f', 1, 32) + for i := 1; i < len(fa); i++ { + buf = append(buf, ',') + buf = strconv.AppendFloat(buf, float64(fa[i]), 'f', 1, 32) + } + } + buf = append(buf, ']') + return buf, nil +} diff --git a/lineprotocol.go b/lineprotocol.go index d2ecb51..743b737 100644 --- a/lineprotocol.go +++ b/lineprotocol.go @@ -4,8 +4,6 @@ import ( "context" "fmt" "log" - "math" - "strconv" "sync" "time" @@ -13,41 +11,6 @@ import ( "github.com/nats-io/nats.go" ) -// Go's JSON encoder for floats does not support NaN (https://github.com/golang/go/issues/3480). -// This program uses NaN as a signal for missing data. -// For the HTTP JSON API to be able to handle NaN values, -// we have to use our own type which implements encoding/json.Marshaler itself. -type Float float64 - -var NaN Float = Float(math.NaN()) - -func (f Float) IsNaN() bool { - return math.IsNaN(float64(f)) -} - -func (f Float) MarshalJSON() ([]byte, error) { - if math.IsNaN(float64(f)) { - return []byte("null"), nil - } - - return []byte(strconv.FormatFloat(float64(f), 'f', 2, 64)), nil -} - -func (f *Float) UnmarshalJSON(input []byte) error { - s := string(input) - if s == "null" { - *f = NaN - return nil - } - - val, err := strconv.ParseFloat(s, 64) - if err != nil { - return err - } - *f = Float(val) - return nil -} - type Metric struct { Name string minfo metricInfo