Skip to content

Commit 7ba8329

Browse files
committedMar 27, 2022
perf: various performance improvements for all options
Replace calls to reflect.DeepEqual with a custom comparison function that only operates on the types used by encoding/json. Replace calls to json.Marshal, used to calculate the length of patch operations when using the Rationalize option, with a custom Operation method that lookup the size of the value from the original target bytes. name old time/op new time/op delta Compare/differ_diff/default-8 6.96µs ± 0% 5.21µs ± 1% -25.11% Compare/differ_diff/invertible-8 7.80µs ± 0% 6.02µs ± 0% -22.87% Compare/differ_diff/factorize-8 11.4µs ± 1% 7.5µs ± 0% -33.73% Compare/differ_diff/rationalize-8 59.8µs ± 0% 30.6µs ± 1% -48.77% Compare/differ_diff/factor+ratio-8 63.8µs ± 1% 29.8µs ± 0% -53.19% Compare/differ_diff/all-options-8 89.3µs ± 0% 37.4µs ± 1% -58.16% name old alloc/op new alloc/op delta Compare/differ_diff/default-8 3.25kB ± 0% 3.28kB ± 0% +0.99% Compare/differ_diff/invertible-8 5.94kB ± 0% 5.97kB ± 0% +0.54% Compare/differ_diff/factorize-8 3.80kB ± 0% 3.85kB ± 0% +1.26% Compare/differ_diff/rationalize-8 16.3kB ± 0% 5.9kB ± 0% -63.98% Compare/differ_diff/factor+ratio-8 16.8kB ± 0% 6.3kB ± 0% -62.57% Compare/differ_diff/all-options-8 24.7kB ± 0% 7.8kB ± 0% -68.68% name old allocs/op new allocs/op delta Compare/differ_diff/default-8 32.0 ± 0% 32.0 ± 0% ~ Compare/differ_diff/invertible-8 33.0 ± 0% 33.0 ± 0% ~ Compare/differ_diff/factorize-8 53.0 ± 0% 47.0 ± 0% -11.32% Compare/differ_diff/rationalize-8 232 ± 0% 91 ± 0% -60.78% Compare/differ_diff/factor+ratio-8 253 ± 0% 102 ± 0% -59.68% Compare/differ_diff/all-options-8 348 ± 0% 108 ± 0% -68.97%
1 parent 5aa8829 commit 7ba8329

18 files changed

+387
-88
lines changed
 

‎.golangci.yaml

+46
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
run:
2+
timeout: 10m
3+
linters:
4+
disable-all: true
5+
enable:
6+
- asciicheck
7+
- bodyclose
8+
- deadcode
9+
- depguard
10+
- dogsled
11+
- dupl
12+
- errcheck
13+
- exportloopref
14+
- funlen
15+
- gocognit
16+
- goconst
17+
- gocritic
18+
- gocyclo
19+
- gofmt
20+
- goimports
21+
- goprintffuncname
22+
- gosec
23+
- gosimple
24+
- govet
25+
- ineffassign
26+
- misspell
27+
- nakedret
28+
- nolintlint
29+
- revive
30+
- rowserrcheck
31+
- staticcheck
32+
- structcheck
33+
- stylecheck
34+
- typecheck
35+
- unconvert
36+
- unparam
37+
- unused
38+
- varcheck
39+
- whitespace
40+
linters-settings:
41+
gofmt:
42+
simplify: true
43+
dupl:
44+
threshold: 400
45+
funlen:
46+
lines: 120

‎LICENSE

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
MIT License
22

3-
Copyright (c) 2020 William Poussier <william.poussier@gmail.com>
3+
Copyright (c) 2020-2022 William Poussier <william.poussier@gmail.com>
44

55
Permission is hereby granted, free of charge, to any person obtaining a copy
66
of this software and associated documentation files (the "Software"), to deal
@@ -18,4 +18,4 @@ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
1818
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
1919
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
2020
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21-
SOFTWARE.
21+
SOFTWARE.

‎README.md

+2-2
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
<p align="center"><strong>jsondiff</strong> is a Go package for computing the <i>diff</i> between two JSON documents as a series of <a href="https://tools.ietf.org/html/rfc6902">RFC6902</a> (JSON Patch) operations, which is particularly suitable to create the patch response of a Kubernetes Mutating Webhook for example.</p>
44
<p align="center">
55
<a href="https://pkg.go.dev/github.com/wI2L/jsondiff"><img src="https://img.shields.io/static/v1?label=godev&message=reference&color=00add8&logo=go"></a>
6-
<a href="https://goreportcard.com/report/wI2L/jsondiff"><img src="https://goreportcard.com/badge/github.com/wI2L/fizz"></a>
6+
<a href="https://goreportcard.com/report/wI2L/jsondiff"><img src="https://goreportcard.com/badge/github.com/wI2L/jsondiff"></a>
77
<a href="https://github.com/wI2L/jsondiff/actions"><img src="https://github.com/wI2L/jsondiff/workflows/CI/badge.svg"></a>
88
<a href="https://codecov.io/gh/wI2L/jsondiff"><img src="https://codecov.io/gh/wI2L/jsondiff/branch/master/graph/badge.svg"/></a>
99
<a href="https://github.com/wI2L/jsondiff/releases"><img src="https://img.shields.io/github/v/tag/wI2L/jsondiff?color=blueviolet&label=version&sort=semver"></a>
@@ -314,7 +314,7 @@ CompareJSONOpts/all-options-8 106µs ± 1%
314314

315315
## Credits
316316

317-
This package has been inspired by existing implementations of JSON Patch for various languages:
317+
This package has been inspired by existing implementations of JSON Patch in various languages:
318318

319319
- [cujojs/jiff](https://github.com/cujojs/jiff)
320320
- [Starcounter-Jack/JSON-Patch](https://github.com/Starcounter-Jack/JSON-Patch)

‎bench_test.go

+3-1
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,9 @@ func BenchmarkCompare(b *testing.B) {
5858
})
5959
b.Run("differ_diff/"+bb.name, func(b *testing.B) {
6060
for i := 0; i < b.N; i++ {
61-
d := differ{}
61+
d := differ{
62+
targetBytes: afterBytes,
63+
}
6264
for _, opt := range bb.opts {
6365
opt(&d)
6466
}

‎compare.go

+12-10
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,14 @@ type Option func(*differ)
1010
// given values and returns the differences relative
1111
// to the former as a list of JSON Patch operations.
1212
func Compare(source, target interface{}) (Patch, error) {
13-
d := differ{}
13+
var d differ
1414
return compare(&d, source, target)
1515
}
1616

1717
// CompareOpts is similar to Compare, but also accepts
1818
// a list of options to configure the diff behavior.
1919
func CompareOpts(source, target interface{}, opts ...Option) (Patch, error) {
20-
d := differ{}
20+
var d differ
2121
d.applyOpts(opts...)
2222

2323
return compare(&d, source, target)
@@ -27,14 +27,14 @@ func CompareOpts(source, target interface{}, opts ...Option) (Patch, error) {
2727
// returns the differences relative to the former as
2828
// a list of JSON Patch operations.
2929
func CompareJSON(source, target []byte) (Patch, error) {
30-
d := differ{}
30+
var d differ
3131
return compareJSON(&d, source, target)
3232
}
3333

3434
// CompareJSONOpts is similar to CompareJSON, but also
3535
// accepts a list of options to configure the diff behavior.
3636
func CompareJSONOpts(source, target []byte, opts ...Option) (Patch, error) {
37-
d := differ{}
37+
var d differ
3838
d.applyOpts(opts...)
3939

4040
return compareJSON(&d, source, target)
@@ -62,14 +62,15 @@ func Invertible() Option {
6262
}
6363

6464
func compare(d *differ, src, tgt interface{}) (Patch, error) {
65-
si, err := marshalUnmarshal(src)
65+
si, _, err := marshalUnmarshal(src)
6666
if err != nil {
6767
return nil, err
6868
}
69-
ti, err := marshalUnmarshal(tgt)
69+
ti, tb, err := marshalUnmarshal(tgt)
7070
if err != nil {
7171
return nil, err
7272
}
73+
d.targetBytes = tb
7374
d.diff(si, ti)
7475

7576
return d.patch, nil
@@ -83,21 +84,22 @@ func compareJSON(d *differ, src, tgt []byte) (Patch, error) {
8384
if err := json.Unmarshal(tgt, &ti); err != nil {
8485
return nil, err
8586
}
87+
d.targetBytes = tgt
8688
d.diff(si, ti)
8789

8890
return d.patch, nil
8991
}
9092

9193
// marshalUnmarshal returns the result of unmarshaling
9294
// the JSON representation of the given value.
93-
func marshalUnmarshal(i interface{}) (interface{}, error) {
95+
func marshalUnmarshal(i interface{}) (interface{}, []byte, error) {
9496
b, err := json.Marshal(i)
9597
if err != nil {
96-
return nil, err
98+
return nil, nil, err
9799
}
98100
var val interface{}
99101
if err := json.Unmarshal(b, &val); err != nil {
100-
return nil, err
102+
return nil, nil, err
101103
}
102-
return val, nil
104+
return val, b, nil
103105
}

‎differ.go

+35-33
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,18 @@
11
package jsondiff
22

33
import (
4-
"encoding/json"
5-
"reflect"
64
"sort"
75
"strings"
86
)
97

108
type differ struct {
119
patch Patch
1210
hasher hasher
13-
hashmap map[uint64]*jsonNode
11+
hashmap map[uint64]jsonNode
1412
factorize bool
1513
rationalize bool
1614
invertible bool
15+
targetBytes []byte
1716
}
1817

1918
func (d *differ) diff(src, tgt interface{}) {
@@ -24,7 +23,7 @@ func (d *differ) diff(src, tgt interface{}) {
2423
}
2524

2625
func (d *differ) compare(ptr pointer, src, tgt interface{}) {
27-
if reflect.DeepEqual(src, tgt) {
26+
if src == nil && tgt == nil {
2827
return
2928
}
3029
if !areComparable(src, tgt) {
@@ -40,6 +39,9 @@ func (d *differ) compare(ptr pointer, src, tgt interface{}) {
4039
}
4140
return
4241
}
42+
if deepValueEqual(src, tgt, typeSwitchKind(src)) {
43+
return
44+
}
4345
size := len(d.patch)
4446

4547
// Values are comparable, but are not
@@ -52,8 +54,10 @@ func (d *differ) compare(ptr pointer, src, tgt interface{}) {
5254
default:
5355
// Generate a replace operation for
5456
// scalar types.
55-
d.replace(ptr, src, tgt)
56-
return
57+
if !deepValueEqual(src, tgt, typeSwitchKind(src)) {
58+
d.replace(ptr, src, tgt)
59+
return
60+
}
5761
}
5862
// Rationalize any new operations.
5963
if d.rationalize && len(d.patch) > size {
@@ -67,29 +71,28 @@ func (d *differ) prepare(ptr pointer, src, tgt interface{}) {
6771
}
6872
// When both values are deeply equals, save
6973
// the location indexed by the value hash.
70-
if reflect.DeepEqual(src, tgt) {
74+
if !areComparable(src, tgt) {
75+
return
76+
} else if deepValueEqual(src, tgt, typeSwitchKind(src)) {
7177
k := d.hasher.digest(tgt)
7278
if d.hashmap == nil {
73-
d.hashmap = make(map[uint64]*jsonNode)
79+
d.hashmap = make(map[uint64]jsonNode)
7480
}
75-
d.hashmap[k] = &jsonNode{ptr: ptr, val: tgt}
76-
return
77-
}
78-
if !areComparable(src, tgt) {
81+
d.hashmap[k] = jsonNode{ptr: ptr, val: tgt}
7982
return
8083
}
8184
// At this point, the source and target values
8285
// are non-nil and have comparable types.
83-
switch src.(type) {
86+
switch vsrc := src.(type) {
8487
case []interface{}:
85-
oarr := src.([]interface{})
88+
oarr := vsrc
8689
narr := tgt.([]interface{})
8790

8891
for i := 0; i < min(len(oarr), len(narr)); i++ {
8992
d.prepare(ptr.appendIndex(i), oarr[i], narr[i])
9093
}
9194
case map[string]interface{}:
92-
oobj := src.(map[string]interface{})
95+
oobj := vsrc
9396
nobj := tgt.(map[string]interface{})
9497

9598
for k, v1 := range oobj {
@@ -103,10 +106,10 @@ func (d *differ) prepare(ptr pointer, src, tgt interface{}) {
103106
}
104107

105108
func (d *differ) rationalizeLastOps(ptr pointer, src, tgt interface{}, lastOpIdx int) {
106-
ops := make(Patch, 0, 2)
109+
newOps := make(Patch, 0, 2)
107110

108111
if d.invertible {
109-
ops = ops.append(OperationTest, emptyPtr, ptr, nil, src)
112+
newOps = newOps.append(OperationTest, emptyPtr, ptr, nil, src)
110113
}
111114
// replaceOp represents a single operation that
112115
// replace the source document with the target.
@@ -115,17 +118,18 @@ func (d *differ) rationalizeLastOps(ptr pointer, src, tgt interface{}, lastOpIdx
115118
Path: ptr,
116119
Value: tgt,
117120
}
118-
ops = append(ops, replaceOp)
121+
newOps = append(newOps, replaceOp)
122+
curOps := d.patch[lastOpIdx:]
119123

120-
b2, _ := json.Marshal(replaceOp)
121-
b1, _ := json.Marshal(d.patch[lastOpIdx:])
124+
newLen := replaceOp.jsonLength(d.targetBytes)
125+
curLen := curOps.jsonLength(d.targetBytes)
122126

123-
// If one operation is cheapest than many small
127+
// If one operation is cheaper than many small
124128
// operations that represents the changes between
125129
// the two objects, replace the last operations.
126-
if len(b1) > len(b2)+2 {
130+
if curLen > newLen {
127131
d.patch = d.patch[:lastOpIdx]
128-
d.patch = append(d.patch, ops...)
132+
d.patch = append(d.patch, newOps...)
129133
}
130134
}
131135

@@ -135,10 +139,10 @@ func (d *differ) compareObjects(ptr pointer, src, tgt map[string]interface{}) {
135139
cmpSet := make(map[string]uint8)
136140

137141
for k := range src {
138-
cmpSet[k] |= (1 << 0)
142+
cmpSet[k] |= 1 << 0
139143
}
140144
for k := range tgt {
141-
cmpSet[k] |= (1 << 1)
145+
cmpSet[k] |= 1 << 1
142146
}
143147
for _, k := range sortedObjectKeys(cmpSet) {
144148
v := cmpSet[k]
@@ -189,32 +193,30 @@ func (d *differ) add(ptr pointer, v interface{}) {
189193
idx := d.findRemoved(v)
190194
if idx != -1 {
191195
op := d.patch[idx]
196+
192197
// https://tools.ietf.org/html/rfc6902#section-4.4
193198
// The "from" location MUST NOT be a proper prefix
194199
// of the "path" location; i.e., a location cannot
195200
// be moved into one of its children.
196-
if !strings.HasPrefix(ptr.String(), op.Path.String()) {
201+
if !strings.HasPrefix(string(ptr), string(op.Path)) {
197202
d.patch = d.patch.remove(idx)
198203
d.patch = d.patch.append(OperationMove, op.Path, ptr, v, v)
199204
}
200205
return
201206
}
202207
uptr := d.findUnchanged(v)
203-
if uptr != emptyPtr && !d.invertible {
208+
if !uptr.isRoot() && !d.invertible {
204209
d.patch = d.patch.append(OperationCopy, uptr, ptr, nil, v)
205210
} else {
206211
d.patch = d.patch.append(OperationAdd, emptyPtr, ptr, nil, v)
207212
}
208213
}
209214

210-
// areComparable returns whether the interfaces values
215+
// areComparable returns whether the interface values
211216
// i1 and i2 can be compared. The values are comparable
212217
// only if they are both non-nil and share the same kind.
213218
func areComparable(i1, i2 interface{}) bool {
214-
typ1 := reflect.TypeOf(i1)
215-
typ2 := reflect.TypeOf(i2)
216-
217-
return typ1 != nil && typ2 != nil && typ1.Kind() == typ2.Kind()
219+
return typeSwitchKind(i1) == typeSwitchKind(i2)
218220
}
219221

220222
func (d *differ) replace(ptr pointer, src, tgt interface{}) {
@@ -245,7 +247,7 @@ func (d *differ) findUnchanged(v interface{}) pointer {
245247
func (d *differ) findRemoved(v interface{}) int {
246248
for i := 0; i < len(d.patch); i++ {
247249
op := d.patch[i]
248-
if op.Type == OperationRemove && reflect.DeepEqual(op.OldValue, v) {
250+
if op.Type == OperationRemove && deepEqual(op.OldValue, v) {
249251
return i
250252
}
251253
}

‎differ_test.go

+7-1
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,13 @@ func runTestCases(t *testing.T, cases []testcase, opts ...Option) {
6262
name := testNameReplacer.Replace(tc.Name)
6363

6464
t.Run(name, func(t *testing.T) {
65-
d := differ{}
65+
beforeBytes, err := json.Marshal(tc.Before)
66+
if err != nil {
67+
t.Error(err)
68+
}
69+
d := differ{
70+
targetBytes: beforeBytes,
71+
}
6672
d.applyOpts(opts...)
6773
d.diff(tc.Before, tc.After)
6874

0 commit comments

Comments
 (0)
Please sign in to comment.