Skip to content

Commit 7e60220

Browse files
jacksontjkrasi-georgiev
authored andcommitted
Switch from encoding/json -> jsoniter (#570)
* Switch from encoding/json -> jsoniter Signed-off-by: Thomas Jackson <[email protected]>
1 parent 388f986 commit 7e60220

File tree

6 files changed

+328
-16
lines changed

6 files changed

+328
-16
lines changed

api/client_test.go

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,6 @@ import (
2020
"net/http/httptest"
2121
"net/url"
2222
"testing"
23-
24-
"github.com/prometheus/tsdb/testutil"
2523
)
2624

2725
func TestConfig(t *testing.T) {
@@ -148,7 +146,9 @@ func TestDoGetFallback(t *testing.T) {
148146
defer server.Close()
149147

150148
u, err := url.Parse(server.URL)
151-
testutil.Ok(t, err)
149+
if err != nil {
150+
t.Fatal(err)
151+
}
152152
client := &httpClient{client: *(server.Client())}
153153

154154
// Do a post, and ensure that the post succeeds.
@@ -158,7 +158,7 @@ func TestDoGetFallback(t *testing.T) {
158158
}
159159
resp := &testResponse{}
160160
if err := json.Unmarshal(b, resp); err != nil {
161-
testutil.Ok(t, err)
161+
t.Fatal(err)
162162
}
163163
if resp.Method != http.MethodPost {
164164
t.Fatalf("Mismatch method")
@@ -174,7 +174,7 @@ func TestDoGetFallback(t *testing.T) {
174174
t.Fatalf("Error doing local request: %v", err)
175175
}
176176
if err := json.Unmarshal(b, resp); err != nil {
177-
testutil.Ok(t, err)
177+
t.Fatal(err)
178178
}
179179
if resp.Method != http.MethodGet {
180180
t.Fatalf("Mismatch method")

api/prometheus/v1/api.go

Lines changed: 89 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,18 +17,105 @@ package v1
1717

1818
import (
1919
"context"
20-
"encoding/json"
2120
"errors"
2221
"fmt"
22+
"math"
2323
"net/http"
2424
"strconv"
2525
"strings"
2626
"time"
27+
"unsafe"
28+
29+
json "github.com/json-iterator/go"
2730

28-
"github.com/prometheus/client_golang/api"
2931
"github.com/prometheus/common/model"
32+
33+
"github.com/prometheus/client_golang/api"
3034
)
3135

36+
func init() {
37+
json.RegisterTypeEncoderFunc("model.SamplePair", marshalPointJSON, marshalPointJSONIsEmpty)
38+
json.RegisterTypeDecoderFunc("model.SamplePair", unMarshalPointJSON)
39+
}
40+
41+
func unMarshalPointJSON(ptr unsafe.Pointer, iter *json.Iterator) {
42+
p := (*model.SamplePair)(ptr)
43+
if !iter.ReadArray() {
44+
iter.ReportError("unmarshal model.SamplePair", "SamplePair must be [timestamp, value]")
45+
return
46+
}
47+
t := iter.ReadNumber()
48+
if err := p.Timestamp.UnmarshalJSON([]byte(t)); err != nil {
49+
iter.ReportError("unmarshal model.SamplePair", err.Error())
50+
return
51+
}
52+
if !iter.ReadArray() {
53+
iter.ReportError("unmarshal model.SamplePair", "SamplePair missing value")
54+
return
55+
}
56+
57+
f, err := strconv.ParseFloat(iter.ReadString(), 64)
58+
if err != nil {
59+
iter.ReportError("unmarshal model.SamplePair", err.Error())
60+
return
61+
}
62+
p.Value = model.SampleValue(f)
63+
64+
if iter.ReadArray() {
65+
iter.ReportError("unmarshal model.SamplePair", "SamplePair has too many values, must be [timestamp, value]")
66+
return
67+
}
68+
}
69+
70+
func marshalPointJSON(ptr unsafe.Pointer, stream *json.Stream) {
71+
p := *((*model.SamplePair)(ptr))
72+
stream.WriteArrayStart()
73+
// Write out the timestamp as a float divided by 1000.
74+
// This is ~3x faster than converting to a float.
75+
t := int64(p.Timestamp)
76+
if t < 0 {
77+
stream.WriteRaw(`-`)
78+
t = -t
79+
}
80+
stream.WriteInt64(t / 1000)
81+
fraction := t % 1000
82+
if fraction != 0 {
83+
stream.WriteRaw(`.`)
84+
if fraction < 100 {
85+
stream.WriteRaw(`0`)
86+
}
87+
if fraction < 10 {
88+
stream.WriteRaw(`0`)
89+
}
90+
stream.WriteInt64(fraction)
91+
}
92+
stream.WriteMore()
93+
stream.WriteRaw(`"`)
94+
95+
// Taken from https://github.com/json-iterator/go/blob/master/stream_float.go#L71 as a workaround
96+
// to https://github.com/json-iterator/go/issues/365 (jsoniter, to follow json standard, doesn't allow inf/nan)
97+
buf := stream.Buffer()
98+
abs := math.Abs(float64(p.Value))
99+
fmt := byte('f')
100+
// Note: Must use float32 comparisons for underlying float32 value to get precise cutoffs right.
101+
if abs != 0 {
102+
if abs < 1e-6 || abs >= 1e21 {
103+
fmt = 'e'
104+
fmt = 'e'
105+
}
106+
}
107+
buf = strconv.AppendFloat(buf, float64(p.Value), fmt, -1, 64)
108+
stream.SetBuffer(buf)
109+
110+
stream.WriteRaw(`"`)
111+
stream.WriteArrayEnd()
112+
113+
}
114+
115+
func marshalPointJSONIsEmpty(ptr unsafe.Pointer) bool {
116+
return false
117+
}
118+
32119
const (
33120
statusAPIError = 422
34121

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
// Copyright 2019 The Prometheus Authors
2+
// Licensed under the Apache License, Version 2.0 (the "License");
3+
// you may not use this file except in compliance with the License.
4+
// You may obtain a copy of the License at
5+
//
6+
// http://www.apache.org/licenses/LICENSE-2.0
7+
//
8+
// Unless required by applicable law or agreed to in writing, software
9+
// distributed under the License is distributed on an "AS IS" BASIS,
10+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11+
// See the License for the specific language governing permissions and
12+
// limitations under the License.
13+
package v1
14+
15+
import (
16+
"encoding/json"
17+
"strconv"
18+
"testing"
19+
"time"
20+
21+
jsoniter "github.com/json-iterator/go"
22+
23+
"github.com/prometheus/common/model"
24+
)
25+
26+
func generateData(timeseries, datapoints int) model.Matrix {
27+
m := make(model.Matrix, 0)
28+
29+
for i := 0; i < timeseries; i++ {
30+
lset := map[model.LabelName]model.LabelValue{
31+
model.MetricNameLabel: model.LabelValue("timeseries_" + strconv.Itoa(i)),
32+
}
33+
now := model.Now()
34+
values := make([]model.SamplePair, datapoints)
35+
36+
for x := datapoints; x > 0; x-- {
37+
values[x-1] = model.SamplePair{
38+
// Set the time back assuming a 15s interval. Since this is used for
39+
// Marshal/Unmarshal testing the actual interval doesn't matter.
40+
Timestamp: now.Add(time.Second * -15 * time.Duration(x)),
41+
Value: model.SampleValue(float64(x)),
42+
}
43+
}
44+
45+
ss := &model.SampleStream{
46+
Metric: model.Metric(lset),
47+
Values: values,
48+
}
49+
50+
m = append(m, ss)
51+
}
52+
return m
53+
}
54+
55+
func BenchmarkSamplesJsonSerialization(b *testing.B) {
56+
for _, timeseriesCount := range []int{10, 100, 1000} {
57+
b.Run(strconv.Itoa(timeseriesCount), func(b *testing.B) {
58+
for _, datapointCount := range []int{10, 100, 1000} {
59+
b.Run(strconv.Itoa(datapointCount), func(b *testing.B) {
60+
data := generateData(timeseriesCount, datapointCount)
61+
62+
dataBytes, err := json.Marshal(data)
63+
if err != nil {
64+
b.Fatalf("Error marshaling: %v", err)
65+
}
66+
67+
b.Run("marshal", func(b *testing.B) {
68+
b.Run("encoding/json", func(b *testing.B) {
69+
b.ReportAllocs()
70+
for i := 0; i < b.N; i++ {
71+
if _, err := json.Marshal(data); err != nil {
72+
b.Fatal(err)
73+
}
74+
}
75+
})
76+
77+
b.Run("jsoniter", func(b *testing.B) {
78+
b.ReportAllocs()
79+
for i := 0; i < b.N; i++ {
80+
if _, err := jsoniter.Marshal(data); err != nil {
81+
b.Fatal(err)
82+
}
83+
}
84+
})
85+
})
86+
87+
b.Run("unmarshal", func(b *testing.B) {
88+
b.Run("encoding/json", func(b *testing.B) {
89+
b.ReportAllocs()
90+
var m model.Matrix
91+
for i := 0; i < b.N; i++ {
92+
if err := json.Unmarshal(dataBytes, &m); err != nil {
93+
b.Fatal(err)
94+
}
95+
}
96+
})
97+
98+
b.Run("jsoniter", func(b *testing.B) {
99+
b.ReportAllocs()
100+
var m model.Matrix
101+
for i := 0; i < b.N; i++ {
102+
if err := jsoniter.Unmarshal(dataBytes, &m); err != nil {
103+
b.Fatal(err)
104+
}
105+
}
106+
})
107+
})
108+
})
109+
}
110+
})
111+
}
112+
}

api/prometheus/v1/api_test.go

Lines changed: 102 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,19 +15,22 @@ package v1
1515

1616
import (
1717
"context"
18-
"encoding/json"
1918
"errors"
2019
"fmt"
20+
"math"
2121
"net/http"
2222
"net/url"
2323
"reflect"
2424
"strings"
2525
"testing"
2626
"time"
2727

28-
"github.com/prometheus/client_golang/api"
28+
json "github.com/json-iterator/go"
29+
2930
"github.com/prometheus/common/model"
3031
"github.com/prometheus/tsdb/testutil"
32+
33+
"github.com/prometheus/client_golang/api"
3134
)
3235

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

883886
}
884887
}
888+
889+
func TestSamplesJsonSerialization(t *testing.T) {
890+
tests := []struct {
891+
point model.SamplePair
892+
expected string
893+
}{
894+
{
895+
point: model.SamplePair{0, 0},
896+
expected: `[0,"0"]`,
897+
},
898+
{
899+
point: model.SamplePair{1, 20},
900+
expected: `[0.001,"20"]`,
901+
},
902+
{
903+
point: model.SamplePair{10, 20},
904+
expected: `[0.010,"20"]`,
905+
},
906+
{
907+
point: model.SamplePair{100, 20},
908+
expected: `[0.100,"20"]`,
909+
},
910+
{
911+
point: model.SamplePair{1001, 20},
912+
expected: `[1.001,"20"]`,
913+
},
914+
{
915+
point: model.SamplePair{1010, 20},
916+
expected: `[1.010,"20"]`,
917+
},
918+
{
919+
point: model.SamplePair{1100, 20},
920+
expected: `[1.100,"20"]`,
921+
},
922+
{
923+
point: model.SamplePair{12345678123456555, 20},
924+
expected: `[12345678123456.555,"20"]`,
925+
},
926+
{
927+
point: model.SamplePair{-1, 20},
928+
expected: `[-0.001,"20"]`,
929+
},
930+
{
931+
point: model.SamplePair{0, model.SampleValue(math.NaN())},
932+
expected: `[0,"NaN"]`,
933+
},
934+
{
935+
point: model.SamplePair{0, model.SampleValue(math.Inf(1))},
936+
expected: `[0,"+Inf"]`,
937+
},
938+
{
939+
point: model.SamplePair{0, model.SampleValue(math.Inf(-1))},
940+
expected: `[0,"-Inf"]`,
941+
},
942+
{
943+
point: model.SamplePair{0, model.SampleValue(1.2345678e6)},
944+
expected: `[0,"1234567.8"]`,
945+
},
946+
{
947+
point: model.SamplePair{0, 1.2345678e-6},
948+
expected: `[0,"0.0000012345678"]`,
949+
},
950+
{
951+
point: model.SamplePair{0, 1.2345678e-67},
952+
expected: `[0,"1.2345678e-67"]`,
953+
},
954+
}
955+
956+
for _, test := range tests {
957+
t.Run(test.expected, func(t *testing.T) {
958+
b, err := json.Marshal(test.point)
959+
if err != nil {
960+
t.Fatal(err)
961+
}
962+
if string(b) != test.expected {
963+
t.Fatalf("Mismatch marshal expected=%s actual=%s", test.expected, string(b))
964+
}
965+
966+
// To test Unmarshal we will Unmarshal then re-Marshal this way we
967+
// can do a string compare, otherwise Nan values don't show equivalence
968+
// properly.
969+
var sp model.SamplePair
970+
if err = json.Unmarshal(b, &sp); err != nil {
971+
t.Fatal(err)
972+
}
973+
974+
b, err = json.Marshal(sp)
975+
if err != nil {
976+
t.Fatal(err)
977+
}
978+
if string(b) != test.expected {
979+
t.Fatalf("Mismatch marshal expected=%s actual=%s", test.expected, string(b))
980+
}
981+
})
982+
}
983+
}

0 commit comments

Comments
 (0)