Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Switch from encoding/json -> jsoniter #570

Merged
merged 2 commits into from May 28, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
10 changes: 5 additions & 5 deletions api/client_test.go
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
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
@@ -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"
jacksontj marked this conversation as resolved.
Show resolved Hide resolved
)

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
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"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This line has to be before L28 to satisfy the CI style test.

However, for consistency, the named import should be in its own block (separated by a blank line), and L28 should be in the same block as L31 and L32.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we're going to be this picky (which I think is fine) I'd suggest adding a make imports function similar to https://github.com/jacksontj/promxy/blob/master/Makefile#L17


"github.com/prometheus/common/model"
"github.com/prometheus/tsdb/testutil"
jacksontj marked this conversation as resolved.
Show resolved Hide resolved

"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))
}
})
}
}