Skip to content

Commit

Permalink
Switch from encoding/json -> jsoniter (#570)
Browse files Browse the repository at this point in the history
* Switch from encoding/json -> jsoniter

Signed-off-by: Thomas Jackson <jacksontj.89@gmail.com>
  • Loading branch information
jacksontj authored and krasi-georgiev committed May 28, 2019
1 parent 388f986 commit 7e60220
Show file tree
Hide file tree
Showing 6 changed files with 328 additions and 16 deletions.
10 changes: 5 additions & 5 deletions api/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,6 @@ import (
"net/http/httptest"
"net/url"
"testing"

"github.com/prometheus/tsdb/testutil"
)

func TestConfig(t *testing.T) {
Expand Down Expand Up @@ -148,7 +146,9 @@ func TestDoGetFallback(t *testing.T) {
defer server.Close()

u, err := url.Parse(server.URL)
testutil.Ok(t, err)
if err != nil {
t.Fatal(err)
}
client := &httpClient{client: *(server.Client())}

// Do a post, and ensure that the post succeeds.
Expand All @@ -158,7 +158,7 @@ func TestDoGetFallback(t *testing.T) {
}
resp := &testResponse{}
if err := json.Unmarshal(b, resp); err != nil {
testutil.Ok(t, err)
t.Fatal(err)
}
if resp.Method != http.MethodPost {
t.Fatalf("Mismatch method")
Expand All @@ -174,7 +174,7 @@ func TestDoGetFallback(t *testing.T) {
t.Fatalf("Error doing local request: %v", err)
}
if err := json.Unmarshal(b, resp); err != nil {
testutil.Ok(t, err)
t.Fatal(err)
}
if resp.Method != http.MethodGet {
t.Fatalf("Mismatch method")
Expand Down
91 changes: 89 additions & 2 deletions api/prometheus/v1/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,18 +17,105 @@ package v1

import (
"context"
"encoding/json"
"errors"
"fmt"
"math"
"net/http"
"strconv"
"strings"
"time"
"unsafe"

json "github.com/json-iterator/go"

"github.com/prometheus/client_golang/api"
"github.com/prometheus/common/model"

"github.com/prometheus/client_golang/api"
)

func init() {
json.RegisterTypeEncoderFunc("model.SamplePair", marshalPointJSON, marshalPointJSONIsEmpty)
json.RegisterTypeDecoderFunc("model.SamplePair", unMarshalPointJSON)
}

func unMarshalPointJSON(ptr unsafe.Pointer, iter *json.Iterator) {
p := (*model.SamplePair)(ptr)
if !iter.ReadArray() {
iter.ReportError("unmarshal model.SamplePair", "SamplePair must be [timestamp, value]")
return
}
t := iter.ReadNumber()
if err := p.Timestamp.UnmarshalJSON([]byte(t)); err != nil {
iter.ReportError("unmarshal model.SamplePair", err.Error())
return
}
if !iter.ReadArray() {
iter.ReportError("unmarshal model.SamplePair", "SamplePair missing value")
return
}

f, err := strconv.ParseFloat(iter.ReadString(), 64)
if err != nil {
iter.ReportError("unmarshal model.SamplePair", err.Error())
return
}
p.Value = model.SampleValue(f)

if iter.ReadArray() {
iter.ReportError("unmarshal model.SamplePair", "SamplePair has too many values, must be [timestamp, value]")
return
}
}

func marshalPointJSON(ptr unsafe.Pointer, stream *json.Stream) {
p := *((*model.SamplePair)(ptr))
stream.WriteArrayStart()
// Write out the timestamp as a float divided by 1000.
// This is ~3x faster than converting to a float.
t := int64(p.Timestamp)
if t < 0 {
stream.WriteRaw(`-`)
t = -t
}
stream.WriteInt64(t / 1000)
fraction := t % 1000
if fraction != 0 {
stream.WriteRaw(`.`)
if fraction < 100 {
stream.WriteRaw(`0`)
}
if fraction < 10 {
stream.WriteRaw(`0`)
}
stream.WriteInt64(fraction)
}
stream.WriteMore()
stream.WriteRaw(`"`)

// Taken from https://github.com/json-iterator/go/blob/master/stream_float.go#L71 as a workaround
// to https://github.com/json-iterator/go/issues/365 (jsoniter, to follow json standard, doesn't allow inf/nan)
buf := stream.Buffer()
abs := math.Abs(float64(p.Value))
fmt := byte('f')
// Note: Must use float32 comparisons for underlying float32 value to get precise cutoffs right.
if abs != 0 {
if abs < 1e-6 || abs >= 1e21 {
fmt = 'e'
fmt = 'e'
}
}
buf = strconv.AppendFloat(buf, float64(p.Value), fmt, -1, 64)
stream.SetBuffer(buf)

stream.WriteRaw(`"`)
stream.WriteArrayEnd()

}

func marshalPointJSONIsEmpty(ptr unsafe.Pointer) bool {
return false
}

const (
statusAPIError = 422

Expand Down
112 changes: 112 additions & 0 deletions api/prometheus/v1/api_bench_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
// Copyright 2019 The Prometheus Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package v1

import (
"encoding/json"
"strconv"
"testing"
"time"

jsoniter "github.com/json-iterator/go"

"github.com/prometheus/common/model"
)

func generateData(timeseries, datapoints int) model.Matrix {
m := make(model.Matrix, 0)

for i := 0; i < timeseries; i++ {
lset := map[model.LabelName]model.LabelValue{
model.MetricNameLabel: model.LabelValue("timeseries_" + strconv.Itoa(i)),
}
now := model.Now()
values := make([]model.SamplePair, datapoints)

for x := datapoints; x > 0; x-- {
values[x-1] = model.SamplePair{
// Set the time back assuming a 15s interval. Since this is used for
// Marshal/Unmarshal testing the actual interval doesn't matter.
Timestamp: now.Add(time.Second * -15 * time.Duration(x)),
Value: model.SampleValue(float64(x)),
}
}

ss := &model.SampleStream{
Metric: model.Metric(lset),
Values: values,
}

m = append(m, ss)
}
return m
}

func BenchmarkSamplesJsonSerialization(b *testing.B) {
for _, timeseriesCount := range []int{10, 100, 1000} {
b.Run(strconv.Itoa(timeseriesCount), func(b *testing.B) {
for _, datapointCount := range []int{10, 100, 1000} {
b.Run(strconv.Itoa(datapointCount), func(b *testing.B) {
data := generateData(timeseriesCount, datapointCount)

dataBytes, err := json.Marshal(data)
if err != nil {
b.Fatalf("Error marshaling: %v", err)
}

b.Run("marshal", func(b *testing.B) {
b.Run("encoding/json", func(b *testing.B) {
b.ReportAllocs()
for i := 0; i < b.N; i++ {
if _, err := json.Marshal(data); err != nil {
b.Fatal(err)
}
}
})

b.Run("jsoniter", func(b *testing.B) {
b.ReportAllocs()
for i := 0; i < b.N; i++ {
if _, err := jsoniter.Marshal(data); err != nil {
b.Fatal(err)
}
}
})
})

b.Run("unmarshal", func(b *testing.B) {
b.Run("encoding/json", func(b *testing.B) {
b.ReportAllocs()
var m model.Matrix
for i := 0; i < b.N; i++ {
if err := json.Unmarshal(dataBytes, &m); err != nil {
b.Fatal(err)
}
}
})

b.Run("jsoniter", func(b *testing.B) {
b.ReportAllocs()
var m model.Matrix
for i := 0; i < b.N; i++ {
if err := jsoniter.Unmarshal(dataBytes, &m); err != nil {
b.Fatal(err)
}
}
})
})
})
}
})
}
}
105 changes: 102 additions & 3 deletions api/prometheus/v1/api_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,19 +15,22 @@ package v1

import (
"context"
"encoding/json"
"errors"
"fmt"
"math"
"net/http"
"net/url"
"reflect"
"strings"
"testing"
"time"

"github.com/prometheus/client_golang/api"
json "github.com/json-iterator/go"

"github.com/prometheus/common/model"
"github.com/prometheus/tsdb/testutil"

"github.com/prometheus/client_golang/api"
)

type apiTest struct {
Expand Down Expand Up @@ -792,7 +795,7 @@ func TestAPIClientDo(t *testing.T) {
response: "bad json",
expectedErr: &Error{
Type: ErrBadResponse,
Msg: "invalid character 'b' looking for beginning of value",
Msg: "readObjectStart: expect { or n, but found b, error found in #1 byte of ...|bad json|..., bigger context ...|bad json|...",
},
},
{
Expand Down Expand Up @@ -882,3 +885,99 @@ func TestAPIClientDo(t *testing.T) {

}
}

func TestSamplesJsonSerialization(t *testing.T) {
tests := []struct {
point model.SamplePair
expected string
}{
{
point: model.SamplePair{0, 0},
expected: `[0,"0"]`,
},
{
point: model.SamplePair{1, 20},
expected: `[0.001,"20"]`,
},
{
point: model.SamplePair{10, 20},
expected: `[0.010,"20"]`,
},
{
point: model.SamplePair{100, 20},
expected: `[0.100,"20"]`,
},
{
point: model.SamplePair{1001, 20},
expected: `[1.001,"20"]`,
},
{
point: model.SamplePair{1010, 20},
expected: `[1.010,"20"]`,
},
{
point: model.SamplePair{1100, 20},
expected: `[1.100,"20"]`,
},
{
point: model.SamplePair{12345678123456555, 20},
expected: `[12345678123456.555,"20"]`,
},
{
point: model.SamplePair{-1, 20},
expected: `[-0.001,"20"]`,
},
{
point: model.SamplePair{0, model.SampleValue(math.NaN())},
expected: `[0,"NaN"]`,
},
{
point: model.SamplePair{0, model.SampleValue(math.Inf(1))},
expected: `[0,"+Inf"]`,
},
{
point: model.SamplePair{0, model.SampleValue(math.Inf(-1))},
expected: `[0,"-Inf"]`,
},
{
point: model.SamplePair{0, model.SampleValue(1.2345678e6)},
expected: `[0,"1234567.8"]`,
},
{
point: model.SamplePair{0, 1.2345678e-6},
expected: `[0,"0.0000012345678"]`,
},
{
point: model.SamplePair{0, 1.2345678e-67},
expected: `[0,"1.2345678e-67"]`,
},
}

for _, test := range tests {
t.Run(test.expected, func(t *testing.T) {
b, err := json.Marshal(test.point)
if err != nil {
t.Fatal(err)
}
if string(b) != test.expected {
t.Fatalf("Mismatch marshal expected=%s actual=%s", test.expected, string(b))
}

// To test Unmarshal we will Unmarshal then re-Marshal this way we
// can do a string compare, otherwise Nan values don't show equivalence
// properly.
var sp model.SamplePair
if err = json.Unmarshal(b, &sp); err != nil {
t.Fatal(err)
}

b, err = json.Marshal(sp)
if err != nil {
t.Fatal(err)
}
if string(b) != test.expected {
t.Fatalf("Mismatch marshal expected=%s actual=%s", test.expected, string(b))
}
})
}
}

0 comments on commit 7e60220

Please sign in to comment.