From d23b92a910ae14d6058c42c7f38b88ce497b04c6 Mon Sep 17 00:00:00 2001 From: Tim Ramlot <42113979+inteon@users.noreply.github.com> Date: Thu, 11 Apr 2024 18:49:06 +0200 Subject: [PATCH 1/4] migrate github.com/json-iterator/go to sigs.k8s.io/json --- fieldpath/serialize-pe.go | 71 ++++-------- fieldpath/serialize.go | 227 ++++++++++++++++++++++---------------- go.mod | 5 +- go.sum | 17 +-- value/fields.go | 27 +++++ value/value.go | 50 ++------- 6 files changed, 192 insertions(+), 205 deletions(-) diff --git a/fieldpath/serialize-pe.go b/fieldpath/serialize-pe.go index cb18e7b1..0b91487d 100644 --- a/fieldpath/serialize-pe.go +++ b/fieldpath/serialize-pe.go @@ -23,7 +23,6 @@ import ( "strconv" "strings" - jsoniter "github.com/json-iterator/go" "sigs.k8s.io/structured-merge-diff/v4/value" ) @@ -73,29 +72,18 @@ func DeserializePathElement(s string) (PathElement, error) { FieldName: &str, }, nil case peValueSepBytes[0]: - iter := readPool.BorrowIterator(b) - defer readPool.ReturnIterator(iter) - v, err := value.ReadJSONIter(iter) + v, err := value.FromJSON(b) if err != nil { return PathElement{}, err } return PathElement{Value: &v}, nil case peKeySepBytes[0]: - iter := readPool.BorrowIterator(b) - defer readPool.ReturnIterator(iter) - fields := value.FieldList{} - - iter.ReadObjectCB(func(iter *jsoniter.Iterator, key string) bool { - v, err := value.ReadJSONIter(iter) - if err != nil { - iter.Error = err - return false - } - fields = append(fields, value.Field{Name: key, Value: v}) - return true - }) + fields, err := value.FieldListFromJSON(b) + if err != nil { + return PathElement{}, err + } fields.Sort() - return PathElement{Key: &fields}, iter.Error + return PathElement{Key: &fields}, nil case peIndexSepBytes[0]: i, err := strconv.Atoi(s[2:]) if err != nil { @@ -109,11 +97,6 @@ func DeserializePathElement(s string) (PathElement, error) { } } -var ( - readPool = jsoniter.NewIterator(jsoniter.ConfigCompatibleWithStandardLibrary).Pool() - writePool = jsoniter.NewStream(jsoniter.ConfigCompatibleWithStandardLibrary, nil, 1024).Pool() -) - // SerializePathElement serializes a path element func SerializePathElement(pe PathElement) (string, error) { buf := strings.Builder{} @@ -122,47 +105,37 @@ func SerializePathElement(pe PathElement) (string, error) { } func serializePathElementToWriter(w io.Writer, pe PathElement) error { - stream := writePool.BorrowStream(w) - defer writePool.ReturnStream(stream) switch { case pe.FieldName != nil: - if _, err := stream.Write(peFieldSepBytes); err != nil { + if _, err := w.Write(peFieldSepBytes); err != nil { return err } - stream.WriteRaw(*pe.FieldName) + fmt.Fprintf(w, "%s", *pe.FieldName) case pe.Key != nil: - if _, err := stream.Write(peKeySepBytes); err != nil { + if _, err := w.Write(peKeySepBytes); err != nil { return err } - stream.WriteObjectStart() - - for i, field := range *pe.Key { - if i > 0 { - stream.WriteMore() - } - stream.WriteObjectField(field.Name) - value.WriteJSONStream(field.Value, stream) + jsonVal, err := value.FieldListToJSON(*pe.Key) + if err != nil { + return err } - stream.WriteObjectEnd() + fmt.Fprintf(w, "%s", jsonVal) case pe.Value != nil: - if _, err := stream.Write(peValueSepBytes); err != nil { + if _, err := w.Write(peValueSepBytes); err != nil { + return err + } + jsonVal, err := value.ToJSON(*pe.Value) + if err != nil { return err } - value.WriteJSONStream(*pe.Value, stream) + fmt.Fprintf(w, "%s", jsonVal) case pe.Index != nil: - if _, err := stream.Write(peIndexSepBytes); err != nil { + if _, err := w.Write(peIndexSepBytes); err != nil { return err } - stream.WriteInt(*pe.Index) + fmt.Fprintf(w, "%d", *pe.Index) default: return errors.New("invalid PathElement") } - b := stream.Buffer() - err := stream.Flush() - // Help jsoniter manage its buffers--without this, the next - // use of the stream is likely to require an allocation. Look - // at the jsoniter stream code to understand why. They were probably - // optimizing for folks using the buffer directly. - stream.SetBuffer(b[:0]) - return err + return nil } diff --git a/fieldpath/serialize.go b/fieldpath/serialize.go index b992b93c..81154c75 100644 --- a/fieldpath/serialize.go +++ b/fieldpath/serialize.go @@ -18,10 +18,11 @@ package fieldpath import ( "bytes" + gojson "encoding/json" + "fmt" "io" - "unsafe" - jsoniter "github.com/json-iterator/go" + json "sigs.k8s.io/json" ) func (s *Set) ToJSON() ([]byte, error) { @@ -34,63 +35,81 @@ func (s *Set) ToJSON() ([]byte, error) { } func (s *Set) ToJSONStream(w io.Writer) error { - stream := writePool.BorrowStream(w) - defer writePool.ReturnStream(stream) - - var r reusableBuilder - - stream.WriteObjectStart() - err := s.emitContentsV1(false, stream, &r) + err := s.emitContentsV1(false, w) if err != nil { return err } - stream.WriteObjectEnd() - return stream.Flush() + return nil +} + +type orderedMapItemWriter struct { + w io.Writer + hasItems bool } -func manageMemory(stream *jsoniter.Stream) error { - // Help jsoniter manage its buffers--without this, it does a bunch of - // alloctaions that are not necessary. They were probably optimizing - // for folks using the buffer directly. - b := stream.Buffer() - if len(b) > 4096 || cap(b)-len(b) < 2048 { - if err := stream.Flush(); err != nil { +// writeKey writes a key to the writer, including a leading comma if necessary. +// The key is expected to be an already-serialized JSON string (including quotes). +// e.g. writeKey([]byte("\"foo\"")) +// After writing the key, the caller should write the encoded value, e.g. using +// writeEmptyValue or by directly writing the value to the writer. +func (om *orderedMapItemWriter) writeKey(key []byte) error { + if om.hasItems { + if _, err := om.w.Write([]byte{','}); err != nil { return err } - stream.SetBuffer(b[:0]) } + + if _, err := om.w.Write(key); err != nil { + return err + } + if _, err := om.w.Write([]byte{':'}); err != nil { + return err + } + om.hasItems = true return nil } -type reusableBuilder struct { - bytes.Buffer -} +// writePathKey writes a path element as a key to the writer, including a leading comma if necessary. +// The path will be serialized as a JSON string (including quotes) and passed to writeKey. +// After writing the key, the caller should write the encoded value, e.g. using +// writeEmptyValue or by directly writing the value to the writer. +func (om *orderedMapItemWriter) writePathKey(pe PathElement) error { + pev, err := SerializePathElement(pe) + if err != nil { + return err + } + key, err := gojson.Marshal(pev) + if err != nil { + return err + } -func (r *reusableBuilder) unsafeString() string { - b := r.Bytes() - return *(*string)(unsafe.Pointer(&b)) + return om.writeKey(key) } -func (r *reusableBuilder) reset() *bytes.Buffer { - r.Reset() - return &r.Buffer +// writeEmptyValue writes an empty JSON object to the writer. +// This should be used after writeKey. +func (om orderedMapItemWriter) writeEmptyValue() error { + if _, err := om.w.Write([]byte("{}")); err != nil { + return err + } + return nil } -func (s *Set) emitContentsV1(includeSelf bool, stream *jsoniter.Stream, r *reusableBuilder) error { +func (s *Set) emitContentsV1(includeSelf bool, w io.Writer) error { + om := orderedMapItemWriter{w: w} mi, ci := 0, 0 - first := true - preWrite := func() { - if first { - first = false - return - } - stream.WriteMore() + + if _, err := om.w.Write([]byte{'{'}); err != nil { + return err } if includeSelf && !(len(s.Members.members) == 0 && len(s.Children.members) == 0) { - preWrite() - stream.WriteObjectField(".") - stream.WriteEmptyObject() + if err := om.writeKey([]byte("\".\"")); err != nil { + return err + } + if err := om.writeEmptyValue(); err != nil { + return err + } } for mi < len(s.Members.members) && ci < len(s.Children.members) { @@ -98,37 +117,26 @@ func (s *Set) emitContentsV1(includeSelf bool, stream *jsoniter.Stream, r *reusa cpe := s.Children.members[ci].pathElement if c := mpe.Compare(cpe); c < 0 { - preWrite() - if err := serializePathElementToWriter(r.reset(), mpe); err != nil { - return err - } - stream.WriteObjectField(r.unsafeString()) - stream.WriteEmptyObject() - mi++ - } else if c > 0 { - preWrite() - if err := serializePathElementToWriter(r.reset(), cpe); err != nil { + if err := om.writePathKey(mpe); err != nil { return err } - stream.WriteObjectField(r.unsafeString()) - stream.WriteObjectStart() - if err := s.Children.members[ci].set.emitContentsV1(false, stream, r); err != nil { + if err := om.writeEmptyValue(); err != nil { return err } - stream.WriteObjectEnd() - ci++ + + mi++ } else { - preWrite() - if err := serializePathElementToWriter(r.reset(), cpe); err != nil { + if err := om.writePathKey(cpe); err != nil { return err } - stream.WriteObjectField(r.unsafeString()) - stream.WriteObjectStart() - if err := s.Children.members[ci].set.emitContentsV1(true, stream, r); err != nil { + if err := s.Children.members[ci].set.emitContentsV1(c == 0, om.w); err != nil { return err } - stream.WriteObjectEnd() - mi++ + + // If we also found a member with the same path, we skip this member. + if c == 0 { + mi++ + } ci++ } } @@ -136,74 +144,99 @@ func (s *Set) emitContentsV1(includeSelf bool, stream *jsoniter.Stream, r *reusa for mi < len(s.Members.members) { mpe := s.Members.members[mi] - preWrite() - if err := serializePathElementToWriter(r.reset(), mpe); err != nil { + if err := om.writePathKey(mpe); err != nil { return err } - stream.WriteObjectField(r.unsafeString()) - stream.WriteEmptyObject() + if err := om.writeEmptyValue(); err != nil { + return err + } + mi++ } for ci < len(s.Children.members) { cpe := s.Children.members[ci].pathElement - preWrite() - if err := serializePathElementToWriter(r.reset(), cpe); err != nil { + if err := om.writePathKey(cpe); err != nil { return err } - stream.WriteObjectField(r.unsafeString()) - stream.WriteObjectStart() - if err := s.Children.members[ci].set.emitContentsV1(false, stream, r); err != nil { + if err := s.Children.members[ci].set.emitContentsV1(false, om.w); err != nil { return err } - stream.WriteObjectEnd() + ci++ } - return manageMemory(stream) + if _, err := om.w.Write([]byte{'}'}); err != nil { + return err + } + + return nil } // FromJSON clears s and reads a JSON formatted set structure. func (s *Set) FromJSON(r io.Reader) error { - // The iterator pool is completely useless for memory management, grrr. - iter := jsoniter.Parse(jsoniter.ConfigCompatibleWithStandardLibrary, r, 4096) + b, err := io.ReadAll(r) + if err != nil { + return err + } - found, _ := readIterV1(iter) - if found == nil { + found, _, err := readIterV1(b) + if err != nil { + return err + } else if found == nil { *s = Set{} } else { *s = *found } - return iter.Error + return nil +} + +type setReader struct { + target *Set + isMember bool +} + +func (sr *setReader) UnmarshalJSON(data []byte) error { + children, isMember, err := readIterV1(data) + if err != nil { + return err + } + sr.target = children + sr.isMember = isMember + return nil } // returns true if this subtree is also (or only) a member of parent; s is nil // if there are no further children. -func readIterV1(iter *jsoniter.Iterator) (children *Set, isMember bool) { - iter.ReadMapCB(func(iter *jsoniter.Iterator, key string) bool { - if key == "." { +func readIterV1(data []byte) (children *Set, isMember bool, err error) { + m := map[string]setReader{} + + if err := json.UnmarshalCaseSensitivePreserveInts(data, &m); err != nil { + return nil, false, err + } + + for k, v := range m { + if k == "." { isMember = true - iter.Skip() - return true + continue } - pe, err := DeserializePathElement(key) + + pe, err := DeserializePathElement(k) if err == ErrUnknownPathElementType { // Ignore these-- a future version maybe knows what // they are. We drop these completely rather than try // to preserve things we don't understand. - iter.Skip() - return true + continue } else if err != nil { - iter.ReportError("parsing key as path element", err.Error()) - iter.Skip() - return true + return nil, false, fmt.Errorf("parsing key as path element: %v", err) } - grandchildren, childIsMember := readIterV1(iter) - if childIsMember { + + if v.isMember { if children == nil { children = &Set{} } + m := &children.Members.members // Since we expect that most of the time these will have been // serialized in the right order, we just verify that and append. @@ -214,25 +247,27 @@ func readIterV1(iter *jsoniter.Iterator) (children *Set, isMember bool) { children.Members.Insert(pe) } } - if grandchildren != nil { + + if v.target != nil { if children == nil { children = &Set{} } + // Since we expect that most of the time these will have been // serialized in the right order, we just verify that and append. m := &children.Children.members appendOK := len(*m) == 0 || (*m)[len(*m)-1].pathElement.Less(pe) if appendOK { - *m = append(*m, setNode{pe, grandchildren}) + *m = append(*m, setNode{pe, v.target}) } else { - *children.Children.Descend(pe) = *grandchildren + *children.Children.Descend(pe) = *v.target } } - return true - }) + } + if children == nil { isMember = true } - return children, isMember + return children, isMember, nil } diff --git a/go.mod b/go.mod index 2133f8c4..dcd76b83 100644 --- a/go.mod +++ b/go.mod @@ -3,10 +3,9 @@ module sigs.k8s.io/structured-merge-diff/v4 require gopkg.in/yaml.v2 v2.2.8 require ( - github.com/google/go-cmp v0.5.9 // indirect + github.com/google/go-cmp v0.5.9 github.com/google/gofuzz v1.0.0 - github.com/json-iterator/go v1.1.12 - github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd ) go 1.13 diff --git a/go.sum b/go.sum index a37122bb..2231be6b 100644 --- a/go.sum +++ b/go.sum @@ -1,23 +1,10 @@ -github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= -github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/gofuzz v1.0.0 h1:A8PeW59pxE9IoFRqBp37U+mSNaQoZ46F1f0f863XSXw= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= -github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= -github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= -github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= -github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= -github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= -github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= -github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= -github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= -github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd h1:EDPBXCAspyGV4jQlpZSudPeMmr1bNJefnuqLsRAsHZo= +sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0= diff --git a/value/fields.go b/value/fields.go index be3c6724..ca37f146 100644 --- a/value/fields.go +++ b/value/fields.go @@ -17,8 +17,11 @@ limitations under the License. package value import ( + gojson "encoding/json" "sort" "strings" + + "sigs.k8s.io/json" ) // Field is an individual key-value pair. @@ -31,6 +34,30 @@ type Field struct { // have a different name. type FieldList []Field +// FieldListFromJSON is a helper function for reading a JSON document. +func FieldListFromJSON(input []byte) (FieldList, error) { + v := map[string]interface{}{} + if err := json.UnmarshalCaseSensitivePreserveInts(input, &v); err != nil { + return nil, err + } + + fields := make(FieldList, 0, len(v)) + for k, raw := range v { + fields = append(fields, Field{Name: k, Value: NewValueInterface(raw)}) + } + + return fields, nil +} + +// FieldListToJSON is a helper function for producing a JSON document. +func FieldListToJSON(v FieldList) ([]byte, error) { + m := make(map[string]interface{}, len(v)) + for _, f := range v { + m[f.Name] = f.Value.Unstructured() + } + return gojson.Marshal(m) +} + // Sort sorts the field list by Name. func (f FieldList) Sort() { if len(f) < 2 { diff --git a/value/value.go b/value/value.go index ea79e3a0..9b4f83ac 100644 --- a/value/value.go +++ b/value/value.go @@ -17,18 +17,12 @@ limitations under the License. package value import ( - "bytes" + gojson "encoding/json" "fmt" - "io" "strings" - jsoniter "github.com/json-iterator/go" "gopkg.in/yaml.v2" -) - -var ( - readPool = jsoniter.NewIterator(jsoniter.ConfigCompatibleWithStandardLibrary).Pool() - writePool = jsoniter.NewStream(jsoniter.ConfigCompatibleWithStandardLibrary, nil, 1024).Pool() + "sigs.k8s.io/json" ) // A Value corresponds to an 'atom' in the schema. It should return true @@ -83,44 +77,16 @@ type Value interface { // FromJSON is a helper function for reading a JSON document. func FromJSON(input []byte) (Value, error) { - return FromJSONFast(input) -} - -// FromJSONFast is a helper function for reading a JSON document. -func FromJSONFast(input []byte) (Value, error) { - iter := readPool.BorrowIterator(input) - defer readPool.ReturnIterator(iter) - return ReadJSONIter(iter) -} - -// ToJSON is a helper function for producing a JSon document. -func ToJSON(v Value) ([]byte, error) { - buf := bytes.Buffer{} - stream := writePool.BorrowStream(&buf) - defer writePool.ReturnStream(stream) - WriteJSONStream(v, stream) - b := stream.Buffer() - err := stream.Flush() - // Help jsoniter manage its buffers--without this, the next - // use of the stream is likely to require an allocation. Look - // at the jsoniter stream code to understand why. They were probably - // optimizing for folks using the buffer directly. - stream.SetBuffer(b[:0]) - return buf.Bytes(), err -} - -// ReadJSONIter reads a Value from a JSON iterator. -func ReadJSONIter(iter *jsoniter.Iterator) (Value, error) { - v := iter.Read() - if iter.Error != nil && iter.Error != io.EOF { - return nil, iter.Error + var v interface{} + if err := json.UnmarshalCaseSensitivePreserveInts(input, &v); err != nil { + return nil, err } return NewValueInterface(v), nil } -// WriteJSONStream writes a value into a JSON stream. -func WriteJSONStream(v Value, stream *jsoniter.Stream) { - stream.WriteVal(v.Unstructured()) +// ToJSON is a helper function for producing a JSON document. +func ToJSON(v Value) ([]byte, error) { + return gojson.Marshal(v.Unstructured()) } // ToYAML marshals a value as YAML. From 59b48609980cc1fdede9bb648650281fd55a13ba Mon Sep 17 00:00:00 2001 From: Tim Ramlot <42113979+inteon@users.noreply.github.com> Date: Fri, 12 Apr 2024 11:33:44 +0200 Subject: [PATCH 2/4] Reduce number of allocations during serializing --- fieldpath/path.go | 2 +- fieldpath/serialize-pe.go | 21 ++++------ fieldpath/serialize.go | 81 ++++++++++++++++++++----------------- internal/builder/builder.go | 67 ++++++++++++++++++++++++++++++ value/fields.go | 22 +++++++--- value/value.go | 6 +-- 6 files changed, 140 insertions(+), 59 deletions(-) create mode 100644 internal/builder/builder.go diff --git a/fieldpath/path.go b/fieldpath/path.go index 0413130b..fd13875e 100644 --- a/fieldpath/path.go +++ b/fieldpath/path.go @@ -80,7 +80,7 @@ func (fp Path) Copy() Path { // MakePath constructs a Path. The parts may be PathElements, ints, strings. func MakePath(parts ...interface{}) (Path, error) { - var fp Path + fp := make(Path, 0, len(parts)) for _, p := range parts { switch t := p.(type) { case PathElement: diff --git a/fieldpath/serialize-pe.go b/fieldpath/serialize-pe.go index 0b91487d..eb8464cb 100644 --- a/fieldpath/serialize-pe.go +++ b/fieldpath/serialize-pe.go @@ -19,10 +19,9 @@ package fieldpath import ( "errors" "fmt" - "io" "strconv" - "strings" + "sigs.k8s.io/structured-merge-diff/v4/internal/builder" "sigs.k8s.io/structured-merge-diff/v4/value" ) @@ -57,7 +56,7 @@ var ( func DeserializePathElement(s string) (PathElement, error) { b := []byte(s) if len(b) < 2 { - return PathElement{}, errors.New("key must be 2 characters long:") + return PathElement{}, errors.New("key must be 2 characters long") } typeSep, b := b[:2], b[2:] if typeSep[1] != peSepBytes[0] { @@ -99,41 +98,37 @@ func DeserializePathElement(s string) (PathElement, error) { // SerializePathElement serializes a path element func SerializePathElement(pe PathElement) (string, error) { - buf := strings.Builder{} + buf := builder.JSONBuilder{} err := serializePathElementToWriter(&buf, pe) return buf.String(), err } -func serializePathElementToWriter(w io.Writer, pe PathElement) error { +func serializePathElementToWriter(w *builder.JSONBuilder, pe PathElement) error { switch { case pe.FieldName != nil: if _, err := w.Write(peFieldSepBytes); err != nil { return err } - fmt.Fprintf(w, "%s", *pe.FieldName) + w.WriteString(*pe.FieldName) case pe.Key != nil: if _, err := w.Write(peKeySepBytes); err != nil { return err } - jsonVal, err := value.FieldListToJSON(*pe.Key) - if err != nil { + if err := value.FieldListToJSON(*pe.Key, w); err != nil { return err } - fmt.Fprintf(w, "%s", jsonVal) case pe.Value != nil: if _, err := w.Write(peValueSepBytes); err != nil { return err } - jsonVal, err := value.ToJSON(*pe.Value) - if err != nil { + if err := value.ToJSON(*pe.Value, w); err != nil { return err } - fmt.Fprintf(w, "%s", jsonVal) case pe.Index != nil: if _, err := w.Write(peIndexSepBytes); err != nil { return err } - fmt.Fprintf(w, "%d", *pe.Index) + w.WriteString(strconv.Itoa(*pe.Index)) default: return errors.New("invalid PathElement") } diff --git a/fieldpath/serialize.go b/fieldpath/serialize.go index 81154c75..f4a28de1 100644 --- a/fieldpath/serialize.go +++ b/fieldpath/serialize.go @@ -17,34 +17,36 @@ limitations under the License. package fieldpath import ( - "bytes" - gojson "encoding/json" "fmt" "io" + "sort" json "sigs.k8s.io/json" + "sigs.k8s.io/structured-merge-diff/v4/internal/builder" ) func (s *Set) ToJSON() ([]byte, error) { - buf := bytes.Buffer{} - err := s.ToJSONStream(&buf) - if err != nil { + buf := builder.JSONBuilder{} + if err := s.emitContentsV1(false, &buf, &builder.ReusableBuilder{}); err != nil { return nil, err } return buf.Bytes(), nil } func (s *Set) ToJSONStream(w io.Writer) error { - err := s.emitContentsV1(false, w) - if err != nil { + buf := builder.JSONBuilder{} + if err := s.emitContentsV1(false, &buf, &builder.ReusableBuilder{}); err != nil { return err } - return nil + _, err := buf.WriteTo(w) + return err } type orderedMapItemWriter struct { - w io.Writer + w *builder.JSONBuilder hasItems bool + + builder *builder.ReusableBuilder } // writeKey writes a key to the writer, including a leading comma if necessary. @@ -74,16 +76,24 @@ func (om *orderedMapItemWriter) writeKey(key []byte) error { // After writing the key, the caller should write the encoded value, e.g. using // writeEmptyValue or by directly writing the value to the writer. func (om *orderedMapItemWriter) writePathKey(pe PathElement) error { - pev, err := SerializePathElement(pe) - if err != nil { + if om.hasItems { + if _, err := om.w.Write([]byte{','}); err != nil { + return err + } + } + + if err := serializePathElementToWriter(om.builder.Reset(), pe); err != nil { return err } - key, err := gojson.Marshal(pev) - if err != nil { + if err := om.w.WriteJSON(om.builder.UnsafeString()); err != nil { return err } - return om.writeKey(key) + if _, err := om.w.Write([]byte{':'}); err != nil { + return err + } + om.hasItems = true + return nil } // writeEmptyValue writes an empty JSON object to the writer. @@ -95,11 +105,11 @@ func (om orderedMapItemWriter) writeEmptyValue() error { return nil } -func (s *Set) emitContentsV1(includeSelf bool, w io.Writer) error { - om := orderedMapItemWriter{w: w} +func (s *Set) emitContentsV1(includeSelf bool, w *builder.JSONBuilder, r *builder.ReusableBuilder) error { + om := orderedMapItemWriter{w: w, builder: r} mi, ci := 0, 0 - if _, err := om.w.Write([]byte{'{'}); err != nil { + if _, err := w.Write([]byte{'{'}); err != nil { return err } @@ -129,7 +139,7 @@ func (s *Set) emitContentsV1(includeSelf bool, w io.Writer) error { if err := om.writePathKey(cpe); err != nil { return err } - if err := s.Children.members[ci].set.emitContentsV1(c == 0, om.w); err != nil { + if err := s.Children.members[ci].set.emitContentsV1(c == 0, w, r); err != nil { return err } @@ -160,14 +170,14 @@ func (s *Set) emitContentsV1(includeSelf bool, w io.Writer) error { if err := om.writePathKey(cpe); err != nil { return err } - if err := s.Children.members[ci].set.emitContentsV1(false, om.w); err != nil { + if err := s.Children.members[ci].set.emitContentsV1(false, w, r); err != nil { return err } ci++ } - if _, err := om.w.Write([]byte{'}'}); err != nil { + if _, err := w.Write([]byte{'}'}); err != nil { return err } @@ -237,15 +247,9 @@ func readIterV1(data []byte) (children *Set, isMember bool, err error) { children = &Set{} } + // Append the member to the members list, we will sort it later m := &children.Members.members - // Since we expect that most of the time these will have been - // serialized in the right order, we just verify that and append. - appendOK := len(*m) == 0 || (*m)[len(*m)-1].Less(pe) - if appendOK { - *m = append(*m, pe) - } else { - children.Members.Insert(pe) - } + *m = append(*m, pe) } if v.target != nil { @@ -253,18 +257,23 @@ func readIterV1(data []byte) (children *Set, isMember bool, err error) { children = &Set{} } - // Since we expect that most of the time these will have been - // serialized in the right order, we just verify that and append. + // Append the child to the children list, we will sort it later m := &children.Children.members - appendOK := len(*m) == 0 || (*m)[len(*m)-1].pathElement.Less(pe) - if appendOK { - *m = append(*m, setNode{pe, v.target}) - } else { - *children.Children.Descend(pe) = *v.target - } + *m = append(*m, setNode{pe, v.target}) } } + // Sort the members and children + if children != nil { + sort.Slice(children.Members.members, func(i, j int) bool { + return children.Members.members[i].Less(children.Members.members[j]) + }) + + sort.Slice(children.Children.members, func(i, j int) bool { + return children.Children.members[i].pathElement.Less(children.Children.members[j].pathElement) + }) + } + if children == nil { isMember = true } diff --git a/internal/builder/builder.go b/internal/builder/builder.go new file mode 100644 index 00000000..6d429ff9 --- /dev/null +++ b/internal/builder/builder.go @@ -0,0 +1,67 @@ +package builder + +import ( + "bytes" + gojson "encoding/json" + "runtime" + "unsafe" +) + +type ReusableBuilder struct { + JSONBuilder +} + +func (r *ReusableBuilder) Reset() *JSONBuilder { + r.JSONBuilder.Reset() + return &r.JSONBuilder +} + +func (r *ReusableBuilder) UnsafeString() string { + b := r.Bytes() + return *(*string)(unsafe.Pointer(&b)) +} + +type JSONBuilder struct { + initialised bool + bytes.Buffer + gojson.Encoder +} + +type noNewlineWriter struct { + *bytes.Buffer +} + +func (w noNewlineWriter) Write(p []byte) (n int, err error) { + if len(p) > 0 && p[len(p)-1] == '\n' { + p = p[:len(p)-1] + } + return w.Buffer.Write(p) +} + +// noescape hides a pointer from escape analysis. It is the identity function +// but escape analysis doesn't think the output depends on the input. +// noescape is inlined and currently compiles down to zero instructions. +// USE CAREFULLY! +// This was copied from the runtime; see issues 23382 and 7921. +// +//go:nosplit +//go:nocheckptr +func noescape(p unsafe.Pointer) unsafe.Pointer { + x := uintptr(p) + return unsafe.Pointer(x ^ 0) //nolint:unsafeptr +} + +func (r *JSONBuilder) WriteJSON(v interface{}) error { + if !r.initialised { + r.Encoder = *gojson.NewEncoder(noNewlineWriter{&r.Buffer}) + r.initialised = true + } + + if err := r.Encoder.Encode((*interface{})(noescape(unsafe.Pointer(&v)))); err != nil { + return err + } + + runtime.KeepAlive(v) + + return nil +} diff --git a/value/fields.go b/value/fields.go index ca37f146..baa78399 100644 --- a/value/fields.go +++ b/value/fields.go @@ -17,11 +17,11 @@ limitations under the License. package value import ( - gojson "encoding/json" "sort" "strings" "sigs.k8s.io/json" + "sigs.k8s.io/structured-merge-diff/v4/internal/builder" ) // Field is an individual key-value pair. @@ -50,12 +50,22 @@ func FieldListFromJSON(input []byte) (FieldList, error) { } // FieldListToJSON is a helper function for producing a JSON document. -func FieldListToJSON(v FieldList) ([]byte, error) { - m := make(map[string]interface{}, len(v)) - for _, f := range v { - m[f.Name] = f.Value.Unstructured() +func FieldListToJSON(v FieldList, w *builder.JSONBuilder) error { + w.WriteByte('{') + for i, f := range v { + if err := w.WriteJSON(f.Name); err != nil { + return err + } + w.WriteByte(':') + if err := w.WriteJSON(f.Value.Unstructured()); err != nil { + return err + } + if i < len(v)-1 { + w.WriteByte(',') + } } - return gojson.Marshal(m) + w.WriteByte('}') + return nil } // Sort sorts the field list by Name. diff --git a/value/value.go b/value/value.go index 9b4f83ac..359795f5 100644 --- a/value/value.go +++ b/value/value.go @@ -17,12 +17,12 @@ limitations under the License. package value import ( - gojson "encoding/json" "fmt" "strings" "gopkg.in/yaml.v2" "sigs.k8s.io/json" + "sigs.k8s.io/structured-merge-diff/v4/internal/builder" ) // A Value corresponds to an 'atom' in the schema. It should return true @@ -85,8 +85,8 @@ func FromJSON(input []byte) (Value, error) { } // ToJSON is a helper function for producing a JSON document. -func ToJSON(v Value) ([]byte, error) { - return gojson.Marshal(v.Unstructured()) +func ToJSON(v Value, w *builder.JSONBuilder) error { + return w.WriteJSON(v.Unstructured()) } // ToYAML marshals a value as YAML. From 4f4fbffba59b2050d7301a3f9661f089f8f0c70a Mon Sep 17 00:00:00 2001 From: Tim Ramlot <42113979+inteon@users.noreply.github.com> Date: Fri, 12 Apr 2024 16:19:25 +0200 Subject: [PATCH 3/4] Reduce number of allocations during deserialize --- fieldpath/serialize-pe.go | 27 ++-- fieldpath/serialize.go | 61 ++++--- internal/builder/fastobjparse.go | 224 ++++++++++++++++++++++++++ internal/builder/fastobjparse_test.go | 49 ++++++ value/fields.go | 38 ++++- value/value.go | 6 +- 6 files changed, 349 insertions(+), 56 deletions(-) create mode 100644 internal/builder/fastobjparse.go create mode 100644 internal/builder/fastobjparse_test.go diff --git a/fieldpath/serialize-pe.go b/fieldpath/serialize-pe.go index eb8464cb..24fb5f89 100644 --- a/fieldpath/serialize-pe.go +++ b/fieldpath/serialize-pe.go @@ -29,40 +29,39 @@ var ErrUnknownPathElementType = errors.New("unknown path element type") const ( // Field indicates that the content of this path element is a field's name - peField = "f" + peField byte = 'f' // Value indicates that the content of this path element is a field's value - peValue = "v" + peValue byte = 'v' // Index indicates that the content of this path element is an index in an array - peIndex = "i" + peIndex byte = 'i' // Key indicates that the content of this path element is a key value map - peKey = "k" + peKey byte = 'k' // Separator separates the type of a path element from the contents - peSeparator = ":" + peSeparator byte = ':' ) var ( - peFieldSepBytes = []byte(peField + peSeparator) - peValueSepBytes = []byte(peValue + peSeparator) - peIndexSepBytes = []byte(peIndex + peSeparator) - peKeySepBytes = []byte(peKey + peSeparator) - peSepBytes = []byte(peSeparator) + peFieldSepBytes = []byte{peField, peSeparator} + peValueSepBytes = []byte{peValue, peSeparator} + peIndexSepBytes = []byte{peIndex, peSeparator} + peKeySepBytes = []byte{peKey, peSeparator} ) // DeserializePathElement parses a serialized path element func DeserializePathElement(s string) (PathElement, error) { - b := []byte(s) + b := builder.StringToReadOnlyByteSlice(s) if len(b) < 2 { return PathElement{}, errors.New("key must be 2 characters long") } - typeSep, b := b[:2], b[2:] - if typeSep[1] != peSepBytes[0] { + typeSep0, typeSep1, b := b[0], b[1], b[2:] + if typeSep1 != peSeparator { return PathElement{}, fmt.Errorf("missing colon: %v", s) } - switch typeSep[0] { + switch typeSep0 { case peFieldSepBytes[0]: // Slice s rather than convert b, to save on // allocations. diff --git a/fieldpath/serialize.go b/fieldpath/serialize.go index f4a28de1..b220195d 100644 --- a/fieldpath/serialize.go +++ b/fieldpath/serialize.go @@ -21,7 +21,6 @@ import ( "io" "sort" - json "sigs.k8s.io/json" "sigs.k8s.io/structured-merge-diff/v4/internal/builder" ) @@ -202,31 +201,31 @@ func (s *Set) FromJSON(r io.Reader) error { return nil } -type setReader struct { - target *Set - isMember bool -} - -func (sr *setReader) UnmarshalJSON(data []byte) error { - children, isMember, err := readIterV1(data) - if err != nil { - return err - } - sr.target = children - sr.isMember = isMember - return nil -} - // returns true if this subtree is also (or only) a member of parent; s is nil // if there are no further children. func readIterV1(data []byte) (children *Set, isMember bool, err error) { - m := map[string]setReader{} + parser := builder.NewFastObjParser(data) - if err := json.UnmarshalCaseSensitivePreserveInts(data, &m); err != nil { - return nil, false, err - } + for { + rawKey, err := parser.Parse() + if err == io.EOF { + break + } else if err != nil { + return nil, false, fmt.Errorf("parsing JSON: %v", err) + } + + rawValue, err := parser.Parse() + if err == io.EOF { + return nil, false, fmt.Errorf("unexpected EOF") + } else if err != nil { + return nil, false, fmt.Errorf("parsing JSON: %v", err) + } + + k, err := builder.UnmarshalString(rawKey) + if err != nil { + return nil, false, fmt.Errorf("decoding key: %v", err) + } - for k, v := range m { if k == "." { isMember = true continue @@ -242,7 +241,12 @@ func readIterV1(data []byte) (children *Set, isMember bool, err error) { return nil, false, fmt.Errorf("parsing key as path element: %v", err) } - if v.isMember { + grandChildren, isChildMember, err := readIterV1(rawValue) + if err != nil { + return nil, false, fmt.Errorf("parsing value as set: %v", err) + } + + if isChildMember { if children == nil { children = &Set{} } @@ -252,26 +256,21 @@ func readIterV1(data []byte) (children *Set, isMember bool, err error) { *m = append(*m, pe) } - if v.target != nil { + if grandChildren != nil { if children == nil { children = &Set{} } // Append the child to the children list, we will sort it later m := &children.Children.members - *m = append(*m, setNode{pe, v.target}) + *m = append(*m, setNode{pe, grandChildren}) } } // Sort the members and children if children != nil { - sort.Slice(children.Members.members, func(i, j int) bool { - return children.Members.members[i].Less(children.Members.members[j]) - }) - - sort.Slice(children.Children.members, func(i, j int) bool { - return children.Children.members[i].pathElement.Less(children.Children.members[j].pathElement) - }) + sort.Sort(children.Members.members) + sort.Sort(children.Children.members) } if children == nil { diff --git a/internal/builder/fastobjparse.go b/internal/builder/fastobjparse.go new file mode 100644 index 00000000..86551342 --- /dev/null +++ b/internal/builder/fastobjparse.go @@ -0,0 +1,224 @@ +package builder + +import ( + gojson "encoding/json" + "fmt" + "io" + "reflect" + "runtime" + "unsafe" + + "sigs.k8s.io/json" +) + +type parserState int + +const ( + stateLookingForObj parserState = iota + stateLookingForItem + stateLookingForKeyValueSep + stateLookingForItemSep + stateLookingForValue + stateEnd +) + +type FastObjParser struct { + input []byte + pos int + + state parserState +} + +func NewFastObjParser(input []byte) FastObjParser { + return FastObjParser{ + input: input, + state: stateLookingForObj, + } +} + +var whitespace = [256]bool{ + ' ': true, + '\t': true, + '\n': true, + '\r': true, +} + +func isWhitespace(c byte) bool { + return whitespace[c] +} + +func (p *FastObjParser) getValue(startPos int) ([]byte, error) { + foundRootValue := false + isQuoted := false + isEscaped := false + level := 0 + i := startPos +Loop: + for ; i < len(p.input); i++ { + if isQuoted { + // Skip escaped character + if isEscaped { + isEscaped = false + continue + } + + switch p.input[i] { + case '\\': + isEscaped = true + case '"': + isQuoted = false + } + + continue + } + + // Skip whitespace + if isWhitespace(p.input[i]) { + continue + } + + // If we are at the top level and find the next object, we are done + if level == 0 && foundRootValue { + switch p.input[i] { + case ',', '}', ']', ':', '{', '[': + break Loop + } + } + + switch p.input[i] { + // Keep track of the nesting level + case '{': + level++ + case '}': + level-- + case '[': + level++ + case ']': + level-- + + // Start of a string + case '"': + isQuoted = true + } + + foundRootValue = true + } + + if level != 0 { + return nil, fmt.Errorf("expected '}' or ']' but reached end of input") + } + + if isQuoted { + return nil, fmt.Errorf("expected '\"' but reached end of input") + } + + if !foundRootValue { + return nil, fmt.Errorf("expected value but reached end of input") + } + + return p.input[startPos:i], nil +} + +func (p *FastObjParser) Parse() ([]byte, error) { + for { + if p.pos >= len(p.input) { + return nil, io.EOF + } + + // Skip whitespace + if isWhitespace(p.input[p.pos]) { + p.pos++ + continue + } + + switch p.state { + case stateLookingForObj: + if p.input[p.pos] != '{' { + return nil, fmt.Errorf("expected '{' at position %d", p.pos) + } + + p.state = stateLookingForItem + + case stateLookingForItem: + if p.input[p.pos] == '}' { + p.state = stateEnd + return nil, io.EOF + } + + strSlice, err := p.getValue(p.pos) + if err != nil { + return nil, err + } + + p.pos += len(strSlice) + p.state = stateLookingForKeyValueSep + return strSlice, nil + + case stateLookingForKeyValueSep: + if p.input[p.pos] != ':' { + return nil, fmt.Errorf("expected ':' at position %d", p.pos) + } + + p.state = stateLookingForValue + + case stateLookingForValue: + valueSlice, err := p.getValue(p.pos) + if err != nil { + return nil, err + } + + p.pos += len(valueSlice) + p.state = stateLookingForItemSep + return valueSlice, nil + + case stateLookingForItemSep: + if p.input[p.pos] == ',' { + p.state = stateLookingForItem + } else if p.input[p.pos] == '}' { + p.state = stateEnd + } else { + return nil, fmt.Errorf("expected ',' or '}' at position %d", p.pos) + } + + case stateEnd: + return nil, io.EOF + } + + p.pos++ + } +} + +func UnmarshalString(input []byte) (string, error) { + var v string + // No need to enable case sensitivity or int preservation here, as we are only unmarshalling strings. + if err := gojson.Unmarshal(input, (*string)(noescape(unsafe.Pointer(&v)))); err != nil { + return "", err + } + + runtime.KeepAlive(v) + + return v, nil +} + +func UnmarshalInterface(input []byte) (interface{}, error) { + var v interface{} + if err := json.UnmarshalCaseSensitivePreserveInts(input, (*interface{})(noescape(unsafe.Pointer(&v)))); err != nil { + return "", err + } + + runtime.KeepAlive(v) + + return v, nil +} + +// Create a read-only byte array from a string +func StringToReadOnlyByteSlice(s string) []byte { + // Get StringHeader from string + stringHeader := (*reflect.StringHeader)(unsafe.Pointer(&s)) + + // Construct SliceHeader with capacity equal to the length + sliceHeader := reflect.SliceHeader{Data: stringHeader.Data, Len: stringHeader.Len, Cap: stringHeader.Len} + + // Convert SliceHeader to a byte slice + return *(*[]byte)(unsafe.Pointer(&sliceHeader)) +} diff --git a/internal/builder/fastobjparse_test.go b/internal/builder/fastobjparse_test.go new file mode 100644 index 00000000..18fada0d --- /dev/null +++ b/internal/builder/fastobjparse_test.go @@ -0,0 +1,49 @@ +package builder + +import ( + "io" + "testing" +) + +func TestFastObjParse(t *testing.T) { + testCases := map[string][]string{ + `{}`: {}, + `{"a": 1, "b": {}}`: {`"a"`, `1`, `"b"`, `{}`}, + `{"a": 1, "b": 2}`: {`"a"`, `1`, `"b"`, `2`}, + `{"a": 1, "b": 2, "c": 3}`: {`"a"`, `1`, `"b"`, `2`, `"c"`, `3`}, + `{"a": "1", "b": "2", "c": "3"}`: {`"a"`, `"1"`, `"b"`, `"2"`, `"c"`, `"3"`}, + `{"a": "1", "b": {"c": 3}}`: {`"a"`, `"1"`, `"b"`, `{"c": 3}`}, + `{"a": "1", "b": {"c": []}, "d": "4"}`: {`"a"`, `"1"`, `"b"`, `{"c": []}`, `"d"`, `"4"`}, + `{"port":443,"protocol":"tcp"}`: {`"port"`, `443`, `"protocol"`, `"tcp"`}, + } + + for tc, ans := range testCases { + tc := tc + ans := ans + t.Run(tc, func(t *testing.T) { + parser := NewFastObjParser([]byte(tc)) + + results := []string{} + for { + v, err := parser.Parse() + if err == io.EOF { + break + } else if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + results = append(results, string(v)) + } + + if len(results) != len(ans) { + t.Fatalf("unexpected results: %v", results) + } + + for i := 0; i < len(results); i++ { + if results[i] != ans[i] { + t.Fatalf("unexpected results: got %v, want %v", results, ans) + } + } + }) + } +} diff --git a/value/fields.go b/value/fields.go index baa78399..0f4cae74 100644 --- a/value/fields.go +++ b/value/fields.go @@ -17,10 +17,11 @@ limitations under the License. package value import ( + "fmt" + "io" "sort" "strings" - "sigs.k8s.io/json" "sigs.k8s.io/structured-merge-diff/v4/internal/builder" ) @@ -36,14 +37,35 @@ type FieldList []Field // FieldListFromJSON is a helper function for reading a JSON document. func FieldListFromJSON(input []byte) (FieldList, error) { - v := map[string]interface{}{} - if err := json.UnmarshalCaseSensitivePreserveInts(input, &v); err != nil { - return nil, err - } + parser := builder.NewFastObjParser(input) + + var fields FieldList + for { + rawKey, err := parser.Parse() + if err == io.EOF { + break + } else if err != nil { + return nil, fmt.Errorf("parsing JSON: %v", err) + } + + rawValue, err := parser.Parse() + if err == io.EOF { + return nil, fmt.Errorf("unexpected EOF") + } else if err != nil { + return nil, fmt.Errorf("parsing JSON: %v", err) + } + + k, err := builder.UnmarshalString(rawKey) + if err != nil { + return nil, fmt.Errorf("parsing JSON: %v", err) + } + + v, err := builder.UnmarshalInterface(rawValue) + if err != nil { + return nil, fmt.Errorf("parsing JSON: %v", err) + } - fields := make(FieldList, 0, len(v)) - for k, raw := range v { - fields = append(fields, Field{Name: k, Value: NewValueInterface(raw)}) + fields = append(fields, Field{Name: k, Value: NewValueInterface(v)}) } return fields, nil diff --git a/value/value.go b/value/value.go index 359795f5..11d90c82 100644 --- a/value/value.go +++ b/value/value.go @@ -21,7 +21,6 @@ import ( "strings" "gopkg.in/yaml.v2" - "sigs.k8s.io/json" "sigs.k8s.io/structured-merge-diff/v4/internal/builder" ) @@ -77,10 +76,11 @@ type Value interface { // FromJSON is a helper function for reading a JSON document. func FromJSON(input []byte) (Value, error) { - var v interface{} - if err := json.UnmarshalCaseSensitivePreserveInts(input, &v); err != nil { + v, err := builder.UnmarshalInterface(input) + if err != nil { return nil, err } + return NewValueInterface(v), nil } From 26b574790dfc0f5b10c649b4bbf7fe2dce5cf483 Mon Sep 17 00:00:00 2001 From: Tim Ramlot <42113979+inteon@users.noreply.github.com> Date: Fri, 12 Apr 2024 16:55:47 +0200 Subject: [PATCH 4/4] wip: restore API --- fieldpath/serialize-pe.go | 18 +++++++++++++++--- internal/builder/builder.go | 24 +++++++++++++++++++++++- value/fields.go | 19 ------------------- value/value.go | 4 ++-- 4 files changed, 40 insertions(+), 25 deletions(-) diff --git a/fieldpath/serialize-pe.go b/fieldpath/serialize-pe.go index 24fb5f89..0025c844 100644 --- a/fieldpath/serialize-pe.go +++ b/fieldpath/serialize-pe.go @@ -113,14 +113,26 @@ func serializePathElementToWriter(w *builder.JSONBuilder, pe PathElement) error if _, err := w.Write(peKeySepBytes); err != nil { return err } - if err := value.FieldListToJSON(*pe.Key, w); err != nil { - return err + w.WriteByte('{') + nrKeys := len(*pe.Key) + for i, f := range *pe.Key { + if err := w.WriteJSON(f.Name); err != nil { + return err + } + w.WriteByte(':') + if err := w.WriteJSON(f.Value.Unstructured()); err != nil { + return err + } + if i < nrKeys-1 { + w.WriteByte(',') + } } + w.WriteByte('}') case pe.Value != nil: if _, err := w.Write(peValueSepBytes); err != nil { return err } - if err := value.ToJSON(*pe.Value, w); err != nil { + if err := w.WriteJSON((*pe.Value).Unstructured()); err != nil { return err } case pe.Index != nil: diff --git a/internal/builder/builder.go b/internal/builder/builder.go index 6d429ff9..887ea9fd 100644 --- a/internal/builder/builder.go +++ b/internal/builder/builder.go @@ -48,7 +48,7 @@ func (w noNewlineWriter) Write(p []byte) (n int, err error) { //go:nocheckptr func noescape(p unsafe.Pointer) unsafe.Pointer { x := uintptr(p) - return unsafe.Pointer(x ^ 0) //nolint:unsafeptr + return unsafe.Pointer(x ^ 0) } func (r *JSONBuilder) WriteJSON(v interface{}) error { @@ -65,3 +65,25 @@ func (r *JSONBuilder) WriteJSON(v interface{}) error { return nil } + +func MarshalString(input string) ([]byte, error) { + out, err := gojson.Marshal((*string)(noescape(unsafe.Pointer(&input)))) + if err != nil { + return nil, err + } + + runtime.KeepAlive(input) + + return out, nil +} + +func MarshalInterface(input interface{}) ([]byte, error) { + out, err := gojson.Marshal((*interface{})(noescape(unsafe.Pointer(&input)))) + if err != nil { + return nil, err + } + + runtime.KeepAlive(input) + + return out, nil +} diff --git a/value/fields.go b/value/fields.go index 0f4cae74..65ed57d6 100644 --- a/value/fields.go +++ b/value/fields.go @@ -71,25 +71,6 @@ func FieldListFromJSON(input []byte) (FieldList, error) { return fields, nil } -// FieldListToJSON is a helper function for producing a JSON document. -func FieldListToJSON(v FieldList, w *builder.JSONBuilder) error { - w.WriteByte('{') - for i, f := range v { - if err := w.WriteJSON(f.Name); err != nil { - return err - } - w.WriteByte(':') - if err := w.WriteJSON(f.Value.Unstructured()); err != nil { - return err - } - if i < len(v)-1 { - w.WriteByte(',') - } - } - w.WriteByte('}') - return nil -} - // Sort sorts the field list by Name. func (f FieldList) Sort() { if len(f) < 2 { diff --git a/value/value.go b/value/value.go index 11d90c82..9431355d 100644 --- a/value/value.go +++ b/value/value.go @@ -85,8 +85,8 @@ func FromJSON(input []byte) (Value, error) { } // ToJSON is a helper function for producing a JSON document. -func ToJSON(v Value, w *builder.JSONBuilder) error { - return w.WriteJSON(v.Unstructured()) +func ToJSON(v Value) ([]byte, error) { + return builder.MarshalInterface(v.Unstructured()) } // ToYAML marshals a value as YAML.