Skip to content

Commit cfafd75

Browse files
Use easyjson to json marshalling
The JSON marshalling of Metadata now uses easyJSON to marshal this gives us a nice speed improvement and fixes a issue where marhshaling and unmarshaling json would result is a different order. ``` BenchmarkMarshalJSON-4 500000 2553 ns/op 496 B/op 8 allocs/op ```
1 parent 39b7a4d commit cfafd75

File tree

2 files changed

+115
-13
lines changed

2 files changed

+115
-13
lines changed

metadata/metadata.go

Lines changed: 104 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,19 +3,31 @@ package metadata
33
import (
44
"encoding/json"
55

6+
"github.com/mailru/easyjson"
67
"github.com/mailru/easyjson/jlexer"
8+
"github.com/mailru/easyjson/jwriter"
9+
"github.com/pkg/errors"
710
)
811

9-
// Metadata is an immutable map[string]interface{} implementation
10-
type Metadata interface {
11-
// Value returns the value associated with this context for key, or nil
12-
// if no value is associated with key. Successive calls to Value with
13-
// the same key returns the same result.
14-
Value(key string) interface{}
12+
type (
13+
// Metadata is an immutable map[string]interface{} implementation
14+
Metadata interface {
15+
// Value returns the value associated with this context for key, or nil
16+
// if no value is associated with key. Successive calls to Value with
17+
// the same key returns the same result.
18+
Value(key string) interface{}
1519

16-
// AsMap return the Metadata as a map[string]interface{}
17-
AsMap() map[string]interface{}
18-
}
20+
// AsMap return the Metadata as a map[string]interface{}
21+
AsMap() map[string]interface{}
22+
}
23+
24+
// jsonMarshaler is an interface used to speed up the json marshalling process
25+
jsonMarshaler interface {
26+
// MarshalJSONKeyValue writes the Metadata Key and Value and all it's parent Key Value pairs into the writer
27+
// If last is true it MUST omit the `,` from the last Key Value pair
28+
MarshalJSONKeyValue(out *jwriter.Writer, last bool)
29+
}
30+
)
1931

2032
// New return a new Metadata instance without any information
2133
func New() Metadata {
@@ -43,8 +55,12 @@ type emptyData int
4355
var (
4456
// Ensure emptyData implements the Metadata interface
4557
_ Metadata = new(emptyData)
46-
// Ensure valueData implements the json.Marshaler interface
58+
// Ensure valueData implements the jsonMarshaler interface
59+
_ jsonMarshaler = new(emptyData)
60+
// Ensure emptyData implements the json.Marshaler interface
4761
_ json.Marshaler = new(emptyData)
62+
// Ensure emptyData implements the easyjson.Marshaler interface
63+
_ easyjson.Marshaler = new(emptyData)
4864
)
4965

5066
func (*emptyData) Value(key string) interface{} {
@@ -59,6 +75,14 @@ func (v *emptyData) MarshalJSON() ([]byte, error) {
5975
return []byte("{}"), nil
6076
}
6177

78+
func (v *emptyData) MarshalEasyJSON(w *jwriter.Writer) {
79+
w.RawByte('{')
80+
w.RawByte('}')
81+
}
82+
83+
func (v *emptyData) MarshalJSONKeyValue(*jwriter.Writer, bool) {
84+
}
85+
6286
// valueData represents a key, value pair in a metadata chain
6387
type valueData struct {
6488
Metadata
@@ -69,8 +93,12 @@ type valueData struct {
6993
var (
7094
// Ensure valueData implements the Metadata interface
7195
_ Metadata = new(valueData)
96+
// Ensure valueData implements the jsonMarshaler interface
97+
_ jsonMarshaler = new(valueData)
7298
// Ensure valueData implements the json.Marshaler interface
7399
_ json.Marshaler = new(valueData)
100+
// Ensure valueData implements the easyjson.Marshaler interface
101+
_ easyjson.Marshaler = new(valueData)
74102
)
75103

76104
func (v *valueData) Value(key string) interface{} {
@@ -95,7 +123,33 @@ func (v *valueData) AsMap() map[string]interface{} {
95123
}
96124

97125
func (v *valueData) MarshalJSON() ([]byte, error) {
98-
return json.Marshal(v.AsMap())
126+
w := jwriter.Writer{}
127+
v.MarshalEasyJSON(&w)
128+
return w.Buffer.BuildBytes(), w.Error
129+
}
130+
131+
func (v *valueData) MarshalEasyJSON(w *jwriter.Writer) {
132+
w.RawByte('{')
133+
v.MarshalJSONKeyValue(w, true)
134+
w.RawByte('}')
135+
}
136+
137+
func (v *valueData) MarshalJSONKeyValue(out *jwriter.Writer, last bool) {
138+
marshalJSONKeyValues(out, v.Metadata)
139+
140+
out.String(v.key)
141+
out.RawByte(':')
142+
if vm, ok := v.val.(easyjson.Marshaler); ok {
143+
vm.MarshalEasyJSON(out)
144+
} else if vm, ok := v.val.(json.Marshaler); ok {
145+
out.Raw(vm.MarshalJSON())
146+
} else {
147+
out.Raw(json.Marshal(v.val))
148+
}
149+
150+
if !last {
151+
out.RawByte(',')
152+
}
99153
}
100154

101155
// UnmarshalJSON unmarshals the provided json into a Metadata instance
@@ -127,3 +181,42 @@ func UnmarshalJSON(json []byte) (Metadata, error) {
127181

128182
return metadata, in.Error()
129183
}
184+
185+
func marshalJSONKeyValues(out *jwriter.Writer, parent Metadata) {
186+
if parent == nil {
187+
return
188+
}
189+
190+
if m, ok := parent.(jsonMarshaler); ok {
191+
m.MarshalJSONKeyValue(out, false)
192+
return
193+
}
194+
195+
var (
196+
parentJSON []byte
197+
err error
198+
)
199+
if vm, ok := parent.(easyjson.Marshaler); ok {
200+
w := &jwriter.Writer{}
201+
vm.MarshalEasyJSON(out)
202+
parentJSON = w.Buffer.BuildBytes()
203+
err = w.Error
204+
} else if vm, ok := parent.(json.Marshaler); ok {
205+
parentJSON, err = vm.MarshalJSON()
206+
} else {
207+
parentJSON, err = json.Marshal(parent)
208+
}
209+
210+
if err != nil {
211+
out.Raw(parentJSON, err)
212+
return
213+
}
214+
215+
plen := len(parentJSON)
216+
if parentJSON[1] != '{' || parentJSON[plen-1] != '}' {
217+
out.Raw(parentJSON, errors.Errorf("JSON unmarshal failed for Metadata of type %T", parent))
218+
return
219+
}
220+
221+
out.Raw(parentJSON[1:plen-2], nil)
222+
}

metadata/metadata_test.go

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,10 @@ package metadata_test
44

55
import (
66
"encoding/json"
7+
"strings"
78
"testing"
89
"time"
10+
"unicode"
911

1012
"github.com/hellofresh/goengine/metadata"
1113
"github.com/stretchr/testify/assert"
@@ -201,11 +203,18 @@ var jsonTestCases = []struct {
201203
func TestMarshalJSON(t *testing.T) {
202204
for _, testCase := range jsonTestCases {
203205
t.Run(testCase.title, func(t *testing.T) {
206+
expectedJSON := strings.Map(func(r rune) rune {
207+
if unicode.IsSpace(r) {
208+
return -1
209+
}
210+
return r
211+
}, testCase.json)
212+
204213
m := testCase.metadata()
205214

206215
mJSON, err := json.Marshal(m)
207216

208-
assert.JSONEq(t, testCase.json, string(mJSON))
217+
assert.Equal(t, expectedJSON, string(mJSON))
209218
assert.NoError(t, err)
210219
})
211220
}
@@ -218,7 +227,7 @@ func TestUnmarshalJSON(t *testing.T) {
218227

219228
// Need to use AsMap otherwise we can have inconsistent tests results.
220229
if assert.NoError(t, err) {
221-
assert.Equal(t, testCase.metadata().AsMap(), m.AsMap())
230+
assert.Equal(t, testCase.metadata(), m)
222231
}
223232
})
224233
}

0 commit comments

Comments
 (0)