From 1768c324803230c8c28bf21420b7c5be9a6bddf6 Mon Sep 17 00:00:00 2001 From: Hari Mukti Date: Wed, 17 Apr 2024 10:27:48 +0700 Subject: [PATCH] refactor: simplified proto marshal-unmarshal related items (#193) --- README.md | 16 +- decoder/decoder.go | 11 +- decoder/raw_test.go | 3 - encoder/encoder.go | 68 ++---- internal/cmd/benchfit/benchfit_test.go | 5 +- kit/byteorder/byteorder.go | 14 -- kit/byteorder/byteorder_test.go | 21 -- proto/proto_marshal.go | 141 ++++------- proto/proto_marshal_test.go | 38 ++- proto/value_marshal.go | 321 +++++++++++++------------ proto/value_marshal_test.go | 97 +++----- proto/value_unmarshal.go | 94 ++++++-- proto/value_unmarshal_test.go | 80 +++--- 13 files changed, 439 insertions(+), 470 deletions(-) delete mode 100644 kit/byteorder/byteorder.go delete mode 100644 kit/byteorder/byteorder_test.go diff --git a/README.md b/README.md index 0c15078d..5def1105 100644 --- a/README.md +++ b/README.md @@ -326,19 +326,19 @@ goos: darwin goarch: amd64 pkg: benchfit cpu: Intel(R) Core(TM) i5-5257U CPU @ 2.70GHz -BenchmarkDecode/muktihari/fit_raw-4 10 110165214 ns/op 77077057 B/op 100047 allocs/op -BenchmarkDecode/muktihari/fit-4 9 121938626 ns/op 97035515 B/op 200066 allocs/op -BenchmarkDecode/tormoder/fit-4 9 112157057 ns/op 84108961 B/op 700051 allocs/op -BenchmarkEncode/muktihari/fit_raw-4 13 87579657 ns/op 12444 B/op 16 allocs/op -BenchmarkEncode/muktihari/fit-4 7 151469391 ns/op 44065838 B/op 100021 allocs/op -BenchmarkEncode/tormoder/fit-4 1 1300131309 ns/op 101992736 B/op 12100314 allocs/op +BenchmarkDecode/muktihari/fit_raw-4 12 95655408 ns/op 77092784 B/op 100047 allocs/op +BenchmarkDecode/muktihari/fit-4 13 87700988 ns/op 52699547 B/op 101064 allocs/op +BenchmarkDecode/tormoder/fit-4 10 106331934 ns/op 84108932 B/op 700051 allocs/op +BenchmarkEncode/muktihari/fit_raw-4 15 75523944 ns/op 131522 B/op 14 allocs/op +BenchmarkEncode/muktihari/fit-4 8 147434340 ns/op 44139516 B/op 100018 allocs/op +BenchmarkEncode/tormoder/fit-4 1 1301732705 ns/op 101992544 B/op 12100312 allocs/op PASS -ok benchfit 10.811s +ok benchfit 10.958s ``` NOTE: The `1st` on the list, "raw", means we decode the file into the original FIT protocol message structure (similar to the Official FIT SDK implementation in other languages). While the `2nd` decodes messages to **Activity File** struct, which should be equivalent to what the `3rd` does. -The time spent is more or less the same for decoding, but we allocate way fewer objects on the heap for both decoding and encoding. We achieve significantly faster encoding. +We decode slightly faster and encode significantly faster. We allocate far fewer objects on the heap and have a smaller memory footprint for both decoding and encoding. ## Contributing diff --git a/decoder/decoder.go b/decoder/decoder.go index 62444ef2..635604ea 100644 --- a/decoder/decoder.go +++ b/decoder/decoder.go @@ -14,7 +14,6 @@ import ( "sync" "github.com/muktihari/fit/factory" - "github.com/muktihari/fit/kit/byteorder" "github.com/muktihari/fit/kit/hash" "github.com/muktihari/fit/kit/hash/crc16" "github.com/muktihari/fit/kit/scaleoffset" @@ -38,6 +37,8 @@ var ( ErrByteSizeMismatch = errors.New("byte size mismath") ) +const littleEndian = 0 + // Decoder is FIT file decoder. See New() for details. type Decoder struct { readBuffer *readBuffer // read from io.Reader with buffer without extra copying. @@ -552,7 +553,11 @@ func (d *Decoder) decodeMessageDefinition(header byte) error { mesgDef.Header = header mesgDef.Reserved = b[0] mesgDef.Architecture = b[1] - mesgDef.MesgNum = typedef.MesgNum(byteorder.Select(b[1]).Uint16(b[2:4])) + if mesgDef.Architecture == littleEndian { + mesgDef.MesgNum = typedef.MesgNum(binary.LittleEndian.Uint16(b[2:4])) + } else { + mesgDef.MesgNum = typedef.MesgNum(binary.BigEndian.Uint16(b[2:4])) + } n := int(b[4]) b, err = d.readN(n * 3) // 3 byte per field @@ -927,7 +932,7 @@ func (d *Decoder) readValue(size byte, baseType basetype.BaseType, isArray bool, if err != nil { return val, err } - return proto.Unmarshal(b, byteorder.Select(arch), baseType, isArray) + return proto.UnmarshalValue(b, arch, baseType, isArray) } // log logs only if logWriter is not nil. diff --git a/decoder/raw_test.go b/decoder/raw_test.go index 8e778f7f..83095a14 100644 --- a/decoder/raw_test.go +++ b/decoder/raw_test.go @@ -8,12 +8,10 @@ import ( "bytes" "encoding/binary" "errors" - "fmt" "io" "os" "testing" "time" - "unsafe" "github.com/google/go-cmp/cmp" "github.com/muktihari/fit/factory" @@ -500,7 +498,6 @@ func BenchmarkRawDecoderDecode(b *testing.B) { buf := bytes.NewBuffer(all) dec := NewRaw() - fmt.Println(unsafe.Sizeof(*dec)) b.StartTimer() for i := 0; i < b.N; i++ { buf.Reset() diff --git a/encoder/encoder.go b/encoder/encoder.go index 11e33eb5..3f3b144d 100644 --- a/encoder/encoder.go +++ b/encoder/encoder.go @@ -5,7 +5,6 @@ package encoder import ( - "bytes" "context" "encoding/binary" "errors" @@ -61,12 +60,9 @@ type Encoder struct { lastHeaderPos int64 // The byte position of the last header. crc16 hash.Hash16 // Calculate the CRC-16 checksum for ensuring header and message integrity. - // A wrapper that act as a multi writer for writing message definitions and messages to w and crc16 simultaneously. - // We don't use io.MultiWriter since we need to change w on every encode message. - wrapWriterAndCrc16 *wrapWriterAndCrc16 - options *options // Encoder's options. protocolValidator *proto.Validator // Validates message's properties should match the targeted protocol version requirements. + localMesgNumLRU *lru // LRU cache for writing local message definition dataSize uint32 // Data size of messages in bytes for a single FIT file. @@ -75,12 +71,8 @@ type Encoder struct { // and will change every RolloverEvent occurrence. timestampReference uint32 - localMesgNumLRU *lru // LRU cache for writing local message definition - - mesgDef *proto.MessageDefinition // Temporary message definition to reduce alloc. - buf *bytes.Buffer // Temporary bytes buffer to reduce alloc. - - bytesArray [proto.MaxBytesPerMessageDefinition]byte // Underlying array for buf as well as general purpose array for encoding process. + mesgDef *proto.MessageDefinition // Temporary message definition to reduce alloc. + bytesArray [proto.MaxBytesPerMessage]byte // General purpose array for encoding process. defaultFileHeader proto.FileHeader // Default header to encode when not specified. } @@ -206,10 +198,8 @@ func New(w io.Writer, opts ...Option) *Encoder { DataType: proto.DataTypeFIT, CRC: 0, // calculated during encoding }, - mesgDef: &proto.MessageDefinition{}, - wrapWriterAndCrc16: &wrapWriterAndCrc16{writer: w, crc16: crc16}, + mesgDef: &proto.MessageDefinition{}, } - e.buf = bytes.NewBuffer(e.bytesArray[:]) return e } @@ -285,9 +275,7 @@ func (e *Encoder) updateHeader(header *proto.FileHeader) error { header.DataSize = e.dataSize - e.buf.Reset() - _, _ = header.WriteTo(e.buf) - b := e.buf.Bytes() + b, _ := header.MarshalAppend(e.bytesArray[:0]) if header.Size >= 14 { _, _ = e.crc16.Write(b[:12]) // recalculate CRC Checksum since header is changed. @@ -346,9 +334,7 @@ func (e *Encoder) encodeHeader(header *proto.FileHeader) error { } header.ProtocolVersion = byte(e.options.protocolVersion) - e.buf.Reset() - _, _ = header.WriteTo(e.buf) - b := e.buf.Bytes() + b, _ := header.MarshalAppend(e.bytesArray[:0]) if header.Size < 14 { n, err := e.w.Write(b[:header.Size]) @@ -406,30 +392,31 @@ func (e *Encoder) encodeMessage(w io.Writer, mesg *proto.Message) error { return err } - e.buf.Reset() - _, _ = e.mesgDef.WriteTo(e.buf) - mesgDefBytes := e.buf.Bytes() - localMesgNum, isNewMesgDef := e.localMesgNumLRU.Put(mesgDefBytes) // This might alloc memory since we need to copy the item. - mesgDefBytes[0] = (mesgDefBytes[0] &^ proto.LocalMesgNumMask) | localMesgNum // Update the message definition header. + b, _ := e.mesgDef.MarshalAppend(e.bytesArray[:0]) + localMesgNum, isNewMesgDef := e.localMesgNumLRU.Put(b) // This might alloc memory since we need to copy the item. + b[0] = (b[0] &^ proto.LocalMesgNumMask) | localMesgNum // Update the message definition header. mesg.Header = (mesg.Header &^ proto.LocalMesgNumMask) | localMesgNum - e.wrapWriterAndCrc16.writer = w // Change writer - if isNewMesgDef { - n, err := e.wrapWriterAndCrc16.Write(mesgDefBytes) - e.dataSize += uint32(n) - e.n += int64(n) + n, err := w.Write(b) + e.n, e.dataSize = e.n+int64(n), e.dataSize+uint32(n) if err != nil { return fmt.Errorf("write message definition failed: %w", err) } + _, _ = e.crc16.Write(b) } - n, err := mesg.WriteTo(e.wrapWriterAndCrc16) - e.dataSize += uint32(n) - e.n += int64(n) + b, err := mesg.MarshalAppend(e.bytesArray[:0]) + if err != nil { + return fmt.Errorf("marshal mesg failed: %w", err) + } + + n, err := w.Write(b) + e.n, e.dataSize = e.n+int64(n), e.dataSize+uint32(n) if err != nil { return fmt.Errorf("write message failed: %w", err) } + _, _ = e.crc16.Write(b) return nil } @@ -464,20 +451,6 @@ func (e *Encoder) compressTimestampIntoHeader(mesg *proto.Message) { mesg.RemoveFieldByNum(proto.FieldNumTimestamp) } -type wrapWriterAndCrc16 struct { - writer io.Writer - crc16 hash.Hash16 -} - -func (w *wrapWriterAndCrc16) Write(p []byte) (n int, err error) { - n, err = w.writer.Write(p) - if err != nil { - return - } - _, _ = w.crc16.Write(p) - return -} - func (e *Encoder) encodeCRC() error { b := e.bytesArray[:2] binary.LittleEndian.PutUint16(b, e.crc16.Sum16()) @@ -523,7 +496,6 @@ func (e *Encoder) Reset(w io.Writer, opts ...Option) { e.protocolValidator.SetProtocolVersion(e.options.protocolVersion) e.defaultFileHeader.ProtocolVersion = byte(e.options.protocolVersion) - e.wrapWriterAndCrc16.writer = w } // EncodeWithContext is similar to Encode but with respect to context propagation. diff --git a/internal/cmd/benchfit/benchfit_test.go b/internal/cmd/benchfit/benchfit_test.go index e6ff0371..0dbef892 100644 --- a/internal/cmd/benchfit/benchfit_test.go +++ b/internal/cmd/benchfit/benchfit_test.go @@ -34,8 +34,7 @@ func BenchmarkDecode(b *testing.B) { b.StartTimer() for i := 0; i < b.N; i++ { - // NOTE: We wrap it with *bufio.Reader since tormoder's fit is already implementing similar concept under the hood while we don't. - dec := decoder.New(bufio.NewReader(bytes.NewReader(f))) + dec := decoder.New(bytes.NewReader(f)) _, err = dec.Decode() if err != nil { b.Fatalf("decode error: %v", err) @@ -52,7 +51,7 @@ func BenchmarkDecode(b *testing.B) { for i := 0; i < b.N; i++ { al := filedef.NewListener() - dec := decoder.New(bufio.NewReader(bytes.NewReader(f)), + dec := decoder.New(bytes.NewReader(f), decoder.WithMesgListener(al), decoder.WithBroadcastOnly(), ) diff --git a/kit/byteorder/byteorder.go b/kit/byteorder/byteorder.go deleted file mode 100644 index 53ac7530..00000000 --- a/kit/byteorder/byteorder.go +++ /dev/null @@ -1,14 +0,0 @@ -// Copyright 2023 The FIT SDK for Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -package byteorder - -import "encoding/binary" - -func Select(arch byte) binary.ByteOrder { - if arch == 0 { - return binary.LittleEndian - } - return binary.BigEndian -} diff --git a/kit/byteorder/byteorder_test.go b/kit/byteorder/byteorder_test.go deleted file mode 100644 index 6fadee2f..00000000 --- a/kit/byteorder/byteorder_test.go +++ /dev/null @@ -1,21 +0,0 @@ -// Copyright 2023 The FIT SDK for Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -package byteorder_test - -import ( - "encoding/binary" - "testing" - - "github.com/muktihari/fit/kit/byteorder" -) - -func TestSelect(t *testing.T) { - if byteorder.Select(0) != binary.LittleEndian { - t.Fatalf("expected little endian") - } - if byteorder.Select(1) != binary.BigEndian { - t.Fatalf("expected big endian") - } -} diff --git a/proto/proto_marshal.go b/proto/proto_marshal.go index 74aaabed..80f316d2 100644 --- a/proto/proto_marshal.go +++ b/proto/proto_marshal.go @@ -5,16 +5,14 @@ package proto import ( - "bytes" "encoding" "encoding/binary" "fmt" - "io" "sync" - - "github.com/muktihari/fit/kit/byteorder" ) +const littleEndian = 0 + // Marshaler should only do one thing: marshaling to its bytes representation, any validation should be done outside. // Header + ((max n Fields) * (n value)) + ((max n DeveloperFields) * (n value)) @@ -23,93 +21,61 @@ const MaxBytesPerMessage = 1 + (255*255)*2 // Header + Reserved + Architecture + MesgNum (2 bytes) + n Fields + (Max n Fields * 3) + n DevFields + (Max n DevFields * 3). const MaxBytesPerMessageDefinition = 5 + 1 + (255 * 3) + 1 + (255 * 3) -var arrayPool = sync.Pool{ - New: func() any { - b := [MaxBytesPerMessage]byte{} - return &b - }, -} - -var bufPool = sync.Pool{ - New: func() any { - return bytes.NewBuffer(make([]byte, MaxBytesPerMessage)) - }, -} +var pool = sync.Pool{New: func() any { return new([MaxBytesPerMessage]byte) }} var ( - // Zero alloc marshaler for efficient marshaling. - _ io.WriterTo = &FileHeader{} - _ io.WriterTo = &Message{} - _ io.WriterTo = &MessageDefinition{} - _ encoding.BinaryMarshaler = &FileHeader{} _ encoding.BinaryMarshaler = &MessageDefinition{} _ encoding.BinaryMarshaler = &Message{} ) -func (h *FileHeader) MarshalBinary() ([]byte, error) { - buf := bufPool.Get().(*bytes.Buffer) - defer bufPool.Put(buf) - buf.Reset() - - _, _ = h.WriteTo(buf) +// MarshalBinary returns the FIT format encoding of FileHeader and nil error. +func (h FileHeader) MarshalBinary() ([]byte, error) { + arr := pool.Get().(*[MaxBytesPerMessage]byte) + defer pool.Put(arr) + b := arr[:0] - b := make([]byte, buf.Len()) - copy(b, buf.Bytes()) + b, _ = h.MarshalAppend(b) - return b, nil + return append([]byte{}, b...), nil } -func (h *FileHeader) WriteTo(w io.Writer) (n int64, err error) { - arr := arrayPool.Get().(*[MaxBytesPerMessage]byte) - defer arrayPool.Put(arr) - - b := (*arr)[:h.Size] - - b[0] = h.Size - b[1] = h.ProtocolVersion - - binary.LittleEndian.PutUint16(b[2:4], h.ProfileVersion) - binary.LittleEndian.PutUint32(b[4:8], h.DataSize) - - copy(b[8:12], h.DataType) - +// MarshalAppend appends the FIT format encoding of FileHeader to b, returning the result. +func (h FileHeader) MarshalAppend(b []byte) ([]byte, error) { + b = append(b, h.Size, h.ProtocolVersion) + b = binary.LittleEndian.AppendUint16(b, h.ProfileVersion) + b = binary.LittleEndian.AppendUint32(b, h.DataSize) + b = append(b, h.DataType[:4]...) if h.Size >= 14 { - binary.LittleEndian.PutUint16(b[12:14], h.CRC) + b = binary.LittleEndian.AppendUint16(b, h.CRC) } - - nn, err := w.Write(b) - return int64(nn), err + return b, nil } -func (m *MessageDefinition) MarshalBinary() ([]byte, error) { - buf := bufPool.Get().(*bytes.Buffer) - defer bufPool.Put(buf) - buf.Reset() - - _, _ = m.WriteTo(buf) +// MarshalBinary returns the FIT format encoding of MessageDefinition and nil error. +func (m MessageDefinition) MarshalBinary() ([]byte, error) { + arr := pool.Get().(*[MaxBytesPerMessage]byte) + defer pool.Put(arr) + b := arr[:0] - b := make([]byte, buf.Len()) - copy(b, buf.Bytes()) + b, _ = m.MarshalAppend(b) - return b, nil + return append([]byte{}, b...), nil } -// WriteTo zero alloc marshal then copy it to w. -func (m *MessageDefinition) WriteTo(w io.Writer) (n int64, err error) { - arr := arrayPool.Get().(*[MaxBytesPerMessage]byte) - defer arrayPool.Put(arr) - b := (*arr)[:0] - +// MarshalAppend appends the FIT format encoding of MessageDefinition to b, returning the result. +func (m MessageDefinition) MarshalAppend(b []byte) ([]byte, error) { b = append(b, m.Header) b = append(b, m.Reserved) b = append(b, m.Architecture) - b = append(b, 0, 0) - byteorder.Select(m.Architecture).PutUint16(b[len(b)-2:], uint16(m.MesgNum)) + if m.Architecture == littleEndian { + b = binary.LittleEndian.AppendUint16(b, uint16(m.MesgNum)) + } else { + b = binary.BigEndian.AppendUint16(b, uint16(m.MesgNum)) + } b = append(b, byte(len(m.FieldDefinitions))) - for i := range m.FieldDefinitions { b = append(b, m.FieldDefinitions[i].Num, @@ -129,50 +95,43 @@ func (m *MessageDefinition) WriteTo(w io.Writer) (n int64, err error) { } } - nn, err := w.Write(b) - return int64(nn), err + return b, nil } -func (m *Message) MarshalBinary() ([]byte, error) { - buf := bufPool.Get().(*bytes.Buffer) - defer bufPool.Put(buf) - buf.Reset() +// MarshalBinary returns the FIT format encoding of Message and any error encountered during marshal. +func (m Message) MarshalBinary() ([]byte, error) { + arr := pool.Get().(*[MaxBytesPerMessage]byte) + defer pool.Put(arr) + b := arr[:0] - _, err := m.WriteTo(buf) + b, err := m.MarshalAppend(b) if err != nil { return nil, err } - b := make([]byte, buf.Len()) - copy(b, buf.Bytes()) - - return b, nil + return append([]byte{}, b...), nil } -// WriteTo zero alloc marshal then copy it to w. -func (m *Message) WriteTo(w io.Writer) (n int64, err error) { - arr := arrayPool.Get().(*[MaxBytesPerMessage]byte) - defer arrayPool.Put(arr) - b := (*arr)[:0] - +// MarshalAppend appends the FIT format encoding of Message to b, returning the result. +func (m Message) MarshalAppend(b []byte) ([]byte, error) { b = append(b, m.Header) + var err error for i := range m.Fields { - field := &m.Fields[i] - err = MarshalTo(&b, field.Value, byteorder.Select(m.Architecture)) + b, err = m.Fields[i].Value.MarshalAppend(b, m.Architecture) if err != nil { - return 0, fmt.Errorf("field: [num: %d, value: %v]: %w", field.Num, field.Value.Any(), err) + return nil, fmt.Errorf("field: [num: %d, value: %v]: %w", + m.Fields[i].Num, m.Fields[i].Value.Any(), err) } } for i := range m.DeveloperFields { - developerField := &m.DeveloperFields[i] - err = MarshalTo(&b, developerField.Value, byteorder.Select(m.Architecture)) + b, err = m.DeveloperFields[i].Value.MarshalAppend(b, m.Architecture) if err != nil { - return 0, fmt.Errorf("developer field: [num: %d, value: %v]: %w", developerField.Num, developerField.Value.Any(), err) + return nil, fmt.Errorf("developer field: [num: %d, value: %v]: %w", + m.DeveloperFields[i].Num, m.DeveloperFields[i].Value.Any(), err) } } - nn, err := w.Write(b) - return int64(nn), err + return b, nil } diff --git a/proto/proto_marshal_test.go b/proto/proto_marshal_test.go index 5620cd47..3fc7dc5e 100644 --- a/proto/proto_marshal_test.go +++ b/proto/proto_marshal_test.go @@ -6,7 +6,7 @@ package proto_test import ( "errors" - "io" + "fmt" "testing" "github.com/google/go-cmp/cmp" @@ -115,7 +115,7 @@ func TestMessageDefinitionMarshaler(t *testing.T) { name: "mesg def fields and developer fields", mesgdef: &proto.MessageDefinition{ Header: 64 | 32, - Architecture: 0, + Architecture: 1, MesgNum: typedef.MesgNumFileId, FieldDefinitions: []proto.FieldDefinition{ {Num: 0, Size: 1, BaseType: basetype.Enum}, @@ -131,7 +131,7 @@ func TestMessageDefinitionMarshaler(t *testing.T) { b: []byte{ 64 | 32, // Header 0, // Reserved - 0, // Architecture + 1, // Architecture 0, 0, // MesgNum 6, // len(FieldDefinitions) 0, 1, 0, @@ -146,8 +146,8 @@ func TestMessageDefinitionMarshaler(t *testing.T) { }, } - for _, tc := range tt { - t.Run(tc.name, func(t *testing.T) { + for i, tc := range tt { + t.Run(fmt.Sprintf("[%d] %s", i, tc.name), func(t *testing.T) { b, _ := tc.mesgdef.MarshalBinary() if diff := cmp.Diff(b, tc.b); diff != "" { t.Fatal(diff) @@ -271,6 +271,24 @@ func BenchmarkHeaderMarshalBinary(b *testing.B) { } } +func BenchmarkHeaderMarshalAppend(b *testing.B) { + b.StopTimer() + header := proto.FileHeader{ + Size: 14, + ProtocolVersion: 32, + ProfileVersion: 2132, + DataSize: 642262, + DataType: ".FIT", + CRC: 12856, + } + arr := [proto.MaxBytesPerMessageDefinition]byte{} + b.StartTimer() + + for i := 0; i < b.N; i++ { + _, _ = header.MarshalAppend(arr[:0]) + } +} + func BenchmarkMessageDefinitionMarshalBinary(b *testing.B) { b.StopTimer() mesg := factory.CreateMesg(mesgnum.Record) @@ -282,14 +300,15 @@ func BenchmarkMessageDefinitionMarshalBinary(b *testing.B) { } } -func BenchmarkMessageDefinitionWriteTo(b *testing.B) { +func BenchmarkMessageDefinitionMarshalAppend(b *testing.B) { b.StopTimer() mesg := factory.CreateMesg(mesgnum.Record) mesgDef := proto.CreateMessageDefinition(&mesg) + arr := [proto.MaxBytesPerMessageDefinition]byte{} b.StartTimer() for i := 0; i < b.N; i++ { - _, _ = mesgDef.WriteTo(io.Discard) + _, _ = mesgDef.MarshalAppend(arr[:0]) } } @@ -307,16 +326,17 @@ func BenchmarkMessageMarshalBinary(b *testing.B) { } } -func BenchmarkMessageWriterTo(b *testing.B) { +func BenchmarkMessageMarshalAppend(b *testing.B) { b.StopTimer() mesg := factory.CreateMesg(mesgnum.Record).WithFieldValues(map[byte]any{ fieldnum.RecordPositionLat: proto.Int32(1000), fieldnum.RecordPositionLong: proto.Int32(1000), fieldnum.RecordSpeed: proto.Uint16(1000), }) + arr := [proto.MaxBytesPerMessage]byte{} b.StartTimer() for i := 0; i < b.N; i++ { - _, _ = mesg.WriteTo(io.Discard) + _, _ = mesg.MarshalAppend(arr[:0]) } } diff --git a/proto/value_marshal.go b/proto/value_marshal.go index 83e35b0d..570f670f 100644 --- a/proto/value_marshal.go +++ b/proto/value_marshal.go @@ -12,205 +12,208 @@ import ( ) var ( - ErrNilDest = errors.New("nil dest") ErrTypeNotSupported = errors.New("type is not supported") ) -// Marshal v into []byte. It returns an error when v is an invalid value. -func Marshal(v Value, bo binary.ByteOrder) (b []byte, err error) { - if err := MarshalTo(&b, v, bo); err != nil { - return nil, err - } - return b, nil -} - -// MarshalTo is a zero-alloc marshal function that will marshal v into []byte and append it to given dest. -// It returns an error when v is an invalid value. -func MarshalTo(dest *[]byte, value Value, bo binary.ByteOrder) error { - if dest == nil { - return fmt.Errorf("dest could not be: %w", ErrNilDest) - } - - switch value.Type() { // Fast path +// MarshalAppend appends the FIT format encoding of Value to b. Returning the result. +// If arch is 0, marshal in Little-Endian, otherwise marshal in Big-Endian. +func (v Value) MarshalAppend(b []byte, arch byte) ([]byte, error) { + switch v.Type() { case TypeBool: - var boolean byte - if value.Bool() { - boolean = 1 + if v.Bool() { + b = append(b, 1) + } else { + b = append(b, 0) } - *dest = append(*dest, boolean) - return nil + return b, nil case TypeSliceBool: - val := value.SliceBool() - for i := range val { - if val[i] { - *dest = append(*dest, 1) + vals := v.SliceBool() + for i := range vals { + if vals[i] { + b = append(b, 1) } else { - *dest = append(*dest, 0) + b = append(b, 0) } } - return nil + return b, nil case TypeInt8: - *dest = append(*dest, uint8(value.Int8())) - return nil + b = append(b, uint8(v.Int8())) + return b, nil case TypeSliceInt8: - val := value.SliceInt8() - for i := range val { - *dest = append(*dest, uint8(val[i])) + vals := v.SliceInt8() + for i := range vals { + b = append(b, uint8(vals[i])) } - return nil + return b, nil case TypeUint8: - *dest = append(*dest, uint8(value.Uint8())) - return nil + b = append(b, uint8(v.Uint8())) + return b, nil case TypeSliceUint8: - *dest = append(*dest, value.SliceUint8()...) - return nil + b = append(b, v.SliceUint8()...) + return b, nil case TypeInt16: - cur := len(*dest) - *dest = append(*dest, 0, 0) - bo.PutUint16((*dest)[cur:], uint16(value.Int16())) - return nil + if arch == 0 { + b = binary.LittleEndian.AppendUint16(b, uint16(v.Int16())) + } else { + b = binary.BigEndian.AppendUint16(b, uint16(v.Int16())) + } + return b, nil case TypeSliceUint16: - val := value.SliceUint16() - const n = 2 - cur := len(*dest) - for i := range val { - *dest = append(*dest, 0, 0) - bo.PutUint16((*dest)[cur:cur+n], uint16(val[i])) - cur += n - } - return nil + vals := v.SliceUint16() + for i := range vals { + if arch == 0 { + b = binary.LittleEndian.AppendUint16(b, uint16(vals[i])) + } else { + b = binary.BigEndian.AppendUint16(b, uint16(vals[i])) + } + } + return b, nil case TypeUint16: - cur := len(*dest) - *dest = append(*dest, 0, 0) - bo.PutUint16((*dest)[cur:], value.Uint16()) - return nil + if arch == 0 { + b = binary.LittleEndian.AppendUint16(b, v.Uint16()) + } else { + b = binary.BigEndian.AppendUint16(b, v.Uint16()) + } + return b, nil case TypeSliceInt16: - val := value.SliceInt16() - const n = 2 - cur := len(*dest) - for i := range val { - *dest = append(*dest, 0, 0) - bo.PutUint16((*dest)[cur:cur+n], uint16(val[i])) - cur += n - } - return nil + vals := v.SliceInt16() + for i := range vals { + if arch == 0 { + b = binary.LittleEndian.AppendUint16(b, uint16(vals[i])) + } else { + b = binary.BigEndian.AppendUint16(b, uint16(vals[i])) + } + } + return b, nil case TypeInt32: - cur := len(*dest) - *dest = append(*dest, 0, 0, 0, 0) - bo.PutUint32((*dest)[cur:], uint32(value.Int32())) - return nil + if arch == 0 { + b = binary.LittleEndian.AppendUint32(b, uint32(v.Int32())) + } else { + b = binary.BigEndian.AppendUint32(b, uint32(v.Int32())) + } + return b, nil case TypeSliceInt32: - val := value.SliceInt32() - const n = 4 - cur := len(*dest) - for i := range val { - *dest = append(*dest, 0, 0, 0, 0) - bo.PutUint32((*dest)[cur:cur+n], uint32(val[i])) - cur += n - } - return nil + vals := v.SliceInt32() + for i := range vals { + if arch == 0 { + b = binary.LittleEndian.AppendUint32(b, uint32(vals[i])) + } else { + b = binary.BigEndian.AppendUint32(b, uint32(vals[i])) + } + } + return b, nil case TypeUint32: - cur := len(*dest) - *dest = append(*dest, 0, 0, 0, 0) - bo.PutUint32((*dest)[cur:], value.Uint32()) - return nil + if arch == 0 { + b = binary.LittleEndian.AppendUint32(b, v.Uint32()) + } else { + b = binary.BigEndian.AppendUint32(b, v.Uint32()) + } + return b, nil case TypeSliceUint32: - val := value.SliceUint32() - const n = 4 - cur := len(*dest) - for i := range val { - *dest = append(*dest, 0, 0, 0, 0) - bo.PutUint32((*dest)[cur:cur+n], val[i]) - cur += n - } - return nil + vals := v.SliceUint32() + for i := range vals { + if arch == 0 { + b = binary.LittleEndian.AppendUint32(b, vals[i]) + } else { + b = binary.BigEndian.AppendUint32(b, vals[i]) + } + } + return b, nil case TypeInt64: - cur := len(*dest) - *dest = append(*dest, 0, 0, 0, 0, 0, 0, 0, 0) - bo.PutUint64((*dest)[cur:], uint64(value.Int64())) - return nil + if arch == 0 { + b = binary.LittleEndian.AppendUint64(b, uint64(v.Int64())) + } else { + b = binary.BigEndian.AppendUint64(b, uint64(v.Int64())) + } + return b, nil case TypeSliceInt64: - val := value.SliceInt64() - const n = 8 - cur := len(*dest) - for i := range val { - *dest = append(*dest, 0, 0, 0, 0, 0, 0, 0, 0) - bo.PutUint64((*dest)[cur:cur+n], uint64(val[i])) - cur += n - } - return nil + vals := v.SliceInt64() + for i := range vals { + if arch == 0 { + b = binary.LittleEndian.AppendUint64(b, uint64(vals[i])) + } else { + b = binary.BigEndian.AppendUint64(b, uint64(vals[i])) + } + } + return b, nil case TypeUint64: - cur := len(*dest) - *dest = append(*dest, 0, 0, 0, 0, 0, 0, 0, 0) - bo.PutUint64((*dest)[cur:], value.Uint64()) - return nil + if arch == 0 { + b = binary.LittleEndian.AppendUint64(b, v.Uint64()) + } else { + b = binary.BigEndian.AppendUint64(b, v.Uint64()) + } + return b, nil case TypeSliceUint64: - val := value.SliceUint64() - const n = 8 - cur := len(*dest) - for i := range val { - *dest = append(*dest, 0, 0, 0, 0, 0, 0, 0, 0) - bo.PutUint64((*dest)[cur:cur+n], val[i]) - cur += n - } - return nil + vals := v.SliceUint64() + for i := range vals { + if arch == 0 { + b = binary.LittleEndian.AppendUint64(b, vals[i]) + } else { + b = binary.BigEndian.AppendUint64(b, vals[i]) + } + } + return b, nil case TypeFloat32: - cur := len(*dest) - v := math.Float32bits(value.Float32()) - *dest = append(*dest, 0, 0, 0, 0) - bo.PutUint32((*dest)[cur:], v) - return nil + v := math.Float32bits(v.Float32()) + if arch == 0 { + b = binary.LittleEndian.AppendUint32(b, v) + } else { + b = binary.BigEndian.AppendUint32(b, v) + } + return b, nil case TypeSliceFloat32: - val := value.SliceFloat32() - const n = 4 - cur := len(*dest) - for i := range val { - *dest = append(*dest, 0, 0, 0, 0) - bo.PutUint32((*dest)[cur:cur+n], math.Float32bits(val[i])) - cur += n - } - return nil + vals := v.SliceFloat32() + for i := range vals { + if arch == 0 { + b = binary.LittleEndian.AppendUint32(b, math.Float32bits(vals[i])) + } else { + b = binary.BigEndian.AppendUint32(b, math.Float32bits(vals[i])) + } + } + return b, nil case TypeFloat64: - v := math.Float64bits(value.Float64()) - cur := len(*dest) - *dest = append(*dest, 0, 0, 0, 0, 0, 0, 0, 0) - bo.PutUint64((*dest)[cur:], v) - return nil + v := math.Float64bits(v.Float64()) + if arch == 0 { + b = binary.LittleEndian.AppendUint64(b, v) + } else { + b = binary.BigEndian.AppendUint64(b, v) + } + return b, nil case TypeSliceFloat64: - val := value.SliceFloat64() - const n = 8 - cur := len(*dest) - for i := range val { - *dest = append(*dest, 0, 0, 0, 0, 0, 0, 0, 0) - bo.PutUint64((*dest)[cur:cur+n], math.Float64bits(val[i])) - cur += n - } - return nil + vals := v.SliceFloat64() + for i := range vals { + if arch == 0 { + b = binary.LittleEndian.AppendUint64(b, math.Float64bits(vals[i])) + } else { + b = binary.BigEndian.AppendUint64(b, math.Float64bits(vals[i])) + } + } + return b, nil case TypeString: - val := value.String() + val := v.String() if len(val) == 0 { - *dest = append(*dest, 0x00) - return nil + b = append(b, 0x00) + return b, nil } - *dest = append(*dest, val...) + b = append(b, val...) if val[len(val)-1] != '\x00' { - *dest = append(*dest, '\x00') // add utf-8 null-terminated string + b = append(b, '\x00') // add utf-8 null-terminated string } - return nil + return b, nil case TypeSliceString: - val := value.SliceString() - for i := range val { - if len(val[i]) == 0 { - *dest = append(*dest, '\x00') + vals := v.SliceString() + for i := range vals { + if len(vals[i]) == 0 { + b = append(b, '\x00') continue } - *dest = append(*dest, val[i]...) - if val[i][len(val[i])-1] != '\x00' { - *dest = append(*dest, '\x00') + b = append(b, vals[i]...) + if vals[i][len(vals[i])-1] != '\x00' { + b = append(b, '\x00') } } - return nil + return b, nil default: - return fmt.Errorf("type Value(%T) is not supported: %w", value.Type(), ErrTypeNotSupported) + return b, fmt.Errorf("type Value(%T) is not supported: %w", v.Type(), ErrTypeNotSupported) } } diff --git a/proto/value_marshal_test.go b/proto/value_marshal_test.go index 7720ebbb..c06b3907 100644 --- a/proto/value_marshal_test.go +++ b/proto/value_marshal_test.go @@ -17,37 +17,7 @@ import ( "github.com/muktihari/fit/kit" ) -func TestMarshal(t *testing.T) { - tt := []struct { - value Value - err error - }{ - {value: Float32(819293429.192321), err: nil}, - {value: Value{}, err: ErrTypeNotSupported}, - } - - for _, tc := range tt { - t.Run(fmt.Sprintf("%T(%v))", tc.value.Any(), tc.value.Any()), func(t *testing.T) { - b, err := Marshal(tc.value, binary.LittleEndian) - if !errors.Is(err, tc.err) { - t.Fatalf("expected err %s nil, got: %v", tc.err, err) - } - if err != nil { - return - } - buf := new(bytes.Buffer) - err = marshalWithReflectionForTest(buf, tc.value) - if err != nil { - t.Fatal(err) - } - if diff := cmp.Diff(b, buf.Bytes()); diff != "" { - t.Fatal(diff) - } - }) - } -} - -func TestMarshalTo(t *testing.T) { +func TestValueMarshalAppend(t *testing.T) { tt := []struct { b *[]byte value Value @@ -96,39 +66,44 @@ func TestMarshalTo(t *testing.T) { {b: kit.Ptr([]byte{}), value: SliceFloat64([]float64{8192934298908979.192321})}, {b: kit.Ptr([]byte{}), value: SliceFloat64([]float64{-897912398989898.546734})}, {b: kit.Ptr([]byte{}), value: Value{}, err: ErrTypeNotSupported}, - {b: nil, value: Value{}, err: ErrNilDest}, } for i, tc := range tt { - t.Run(fmt.Sprintf("[%d] %T(%v))", i, tc.value.Any(), tc.value.Any()), func(t *testing.T) { - err := MarshalTo(tc.b, tc.value, binary.LittleEndian) - if !errors.Is(err, tc.err) { - t.Fatalf("expected err: %v, got: %v", tc.err, err) - } - if err != nil { - return - } + for arch := byte(0); arch <= 1; arch++ { + t.Run(fmt.Sprintf("[%d] %T(%v))", i, tc.value.Any(), tc.value.Any()), func(t *testing.T) { + arr := pool.Get().(*[MaxBytesPerMessage]byte) + defer pool.Put(arr) + b := arr[:0] - buf := new(bytes.Buffer) - if err := marshalWithReflectionForTest(buf, tc.value); err != nil { - t.Fatalf("marshalWithReflectionForTest: %v", err) - } + var err error + *tc.b, err = tc.value.MarshalAppend(b, arch) + if !errors.Is(err, tc.err) { + t.Fatalf("expected err: %v, got: %v", tc.err, err) + } + if err != nil { + return + } - if len(*tc.b) == 0 && len(buf.Bytes()) == 0 { - return - } + buf := new(bytes.Buffer) + if err := marshalValueWithReflectionForTest(buf, tc.value, arch); err != nil { + t.Fatalf("marshalWithReflectionForTest: %v", err) + } - if diff := cmp.Diff(*tc.b, buf.Bytes()); diff != "" { - fmt.Printf("value: %v, b: %v, buf: %v\n", tc.value.Any(), *tc.b, buf.Bytes()) - t.Fatal(diff) - } + if len(*tc.b) == 0 && len(buf.Bytes()) == 0 { + return + } + + if diff := cmp.Diff(*tc.b, buf.Bytes()); diff != "" { + fmt.Printf("value: %v, b: %v, buf: %v\n", tc.value.Any(), *tc.b, buf.Bytes()) + t.Fatal(diff) + } - }) + }) + } } } -// using little-endian -func marshalWithReflectionForTest(w io.Writer, value Value) error { +func marshalValueWithReflectionForTest(w io.Writer, value Value, arch byte) error { if value.Type() == TypeInvalid { return fmt.Errorf("can't interface '%T': %w", value, ErrTypeNotSupported) } @@ -150,7 +125,7 @@ func marshalWithReflectionForTest(w io.Writer, value Value) error { } iface := rv.Index(i).Interface() val := Any(iface) - if err := marshalWithReflectionForTest(w, val); err != nil { + if err := marshalValueWithReflectionForTest(w, val, arch); err != nil { return err } } @@ -164,8 +139,14 @@ func marshalWithReflectionForTest(w io.Writer, value Value) error { } else if b[len(b)-1] != '\x00' { b = append([]byte(b), '\x00') } - return binary.Write(w, binary.LittleEndian, b) + if arch == 0 { + return binary.Write(w, binary.LittleEndian, b) + } else { + return binary.Write(w, binary.BigEndian, b) + } } - - return binary.Write(w, binary.LittleEndian, rv.Interface()) + if arch == 0 { + return binary.Write(w, binary.LittleEndian, rv.Interface()) + } + return binary.Write(w, binary.BigEndian, rv.Interface()) } diff --git a/proto/value_unmarshal.go b/proto/value_unmarshal.go index 6aa3d693..533b6982 100755 --- a/proto/value_unmarshal.go +++ b/proto/value_unmarshal.go @@ -12,9 +12,9 @@ import ( "github.com/muktihari/fit/profile/basetype" ) -// Unmarshal unmarshals b into a proto.Value. -// The caller should ensure that the length of the given b matches its corresponding base type's size, otherwise it might panic. -func Unmarshal(b []byte, bo binary.ByteOrder, ref basetype.BaseType, isArray bool) (Value, error) { +// UnmarshalValue unmarshals b into a proto.Value. +// The caller should ensure that the len(b) matches its corresponding base type's size, otherwise it might panic. +func UnmarshalValue(b []byte, arch byte, ref basetype.BaseType, isArray bool) (Value, error) { switch ref { case basetype.Sint8: if isArray { @@ -38,81 +38,137 @@ func Unmarshal(b []byte, bo binary.ByteOrder, ref basetype.BaseType, isArray boo const n = 2 vs := make([]int16, 0, size(len(b), n)) for i := 0; i < len(b); i += n { - vs = append(vs, int16(bo.Uint16(b[i:i+n]))) + if arch == littleEndian { + vs = append(vs, int16(binary.LittleEndian.Uint16(b[i:i+n]))) + } else { + vs = append(vs, int16(binary.BigEndian.Uint16(b[i:i+n]))) + } } return SliceInt16(vs), nil } - return Int16(int16(bo.Uint16(b))), nil + if arch == littleEndian { + return Int16(int16(binary.LittleEndian.Uint16(b))), nil + } + return Int16(int16(binary.BigEndian.Uint16(b))), nil case basetype.Uint16, basetype.Uint16z: if isArray { const n = 2 vs := make([]uint16, 0, size(len(b), n)) for i := 0; i < len(b); i += n { - vs = append(vs, bo.Uint16(b[i:i+n])) + if arch == littleEndian { + vs = append(vs, binary.LittleEndian.Uint16(b[i:i+n])) + } else { + vs = append(vs, binary.BigEndian.Uint16(b[i:i+n])) + } } return SliceUint16(vs), nil } - return Uint16(bo.Uint16(b)), nil + if arch == littleEndian { + return Uint16(binary.LittleEndian.Uint16(b)), nil + } + return Uint16(binary.BigEndian.Uint16(b)), nil case basetype.Sint32: if isArray { const n = 4 vs := make([]int32, 0, size(len(b), n)) for i := 0; i < len(b); i += n { - vs = append(vs, int32(bo.Uint32(b[i:i+n]))) + if arch == littleEndian { + vs = append(vs, int32(binary.LittleEndian.Uint32(b[i:i+n]))) + } else { + vs = append(vs, int32(binary.BigEndian.Uint32(b[i:i+n]))) + } } return SliceInt32(vs), nil } - return Int32(int32(bo.Uint32(b))), nil + if arch == littleEndian { + return Int32(int32(binary.LittleEndian.Uint32(b))), nil + } + return Int32(int32(binary.BigEndian.Uint32(b))), nil case basetype.Uint32, basetype.Uint32z: if isArray { const n = 4 vs := make([]uint32, 0, size(len(b), n)) for i := 0; i < len(b); i += n { - vs = append(vs, bo.Uint32(b[i:i+n])) + if arch == littleEndian { + vs = append(vs, binary.LittleEndian.Uint32(b[i:i+n])) + } else { + vs = append(vs, binary.BigEndian.Uint32(b[i:i+n])) + } } return SliceUint32(vs), nil } - return Uint32(bo.Uint32(b)), nil + if arch == littleEndian { + return Uint32(binary.LittleEndian.Uint32(b)), nil + } + return Uint32(binary.BigEndian.Uint32(b)), nil case basetype.Sint64: if isArray { const n = 8 vs := make([]int64, 0, size(len(b), n)) for i := 0; i < len(b); i += n { - vs = append(vs, int64(bo.Uint64(b[i:i+n]))) + if arch == littleEndian { + vs = append(vs, int64(binary.LittleEndian.Uint64(b[i:i+n]))) + } else { + vs = append(vs, int64(binary.BigEndian.Uint64(b[i:i+n]))) + } } return SliceInt64(vs), nil } - return Int64(int64(bo.Uint64(b))), nil + if arch == littleEndian { + return Int64(int64(binary.LittleEndian.Uint64(b))), nil + } + return Int64(int64(binary.BigEndian.Uint64(b))), nil case basetype.Uint64, basetype.Uint64z: if isArray { const n = 8 vs := make([]uint64, 0, size(len(b), n)) for i := 0; i < len(b); i += n { - vs = append(vs, bo.Uint64(b[i:i+n])) + if arch == littleEndian { + vs = append(vs, binary.LittleEndian.Uint64(b[i:i+n])) + } else { + vs = append(vs, binary.BigEndian.Uint64(b[i:i+n])) + } } return SliceUint64(vs), nil } - return Uint64(bo.Uint64(b)), nil + if arch == littleEndian { + return Uint64(binary.LittleEndian.Uint64(b)), nil + } + return Uint64(binary.BigEndian.Uint64(b)), nil case basetype.Float32: if isArray { const n = 4 vs := make([]float32, 0, size(len(b), n)) for i := 0; i < len(b); i += n { - vs = append(vs, math.Float32frombits(bo.Uint32(b[i:i+n]))) + if arch == littleEndian { + vs = append(vs, math.Float32frombits(binary.LittleEndian.Uint32(b[i:i+n]))) + } else { + vs = append(vs, math.Float32frombits(binary.BigEndian.Uint32(b[i:i+n]))) + } } return SliceFloat32(vs), nil } - return Float32(math.Float32frombits(bo.Uint32(b))), nil + if arch == littleEndian { + return Float32(math.Float32frombits(binary.LittleEndian.Uint32(b))), nil + } + return Float32(math.Float32frombits(binary.BigEndian.Uint32(b))), nil case basetype.Float64: if isArray { const n = 8 vs := make([]float64, 0, size(len(b), n)) for i := 0; i < len(b); i += n { - vs = append(vs, math.Float64frombits(bo.Uint64(b[i:i+n]))) + if arch == littleEndian { + vs = append(vs, math.Float64frombits(binary.LittleEndian.Uint64(b[i:i+n]))) + } else { + vs = append(vs, math.Float64frombits(binary.BigEndian.Uint64(b[i:i+n]))) + } } return SliceFloat64(vs), nil } - return Float64(math.Float64frombits(bo.Uint64(b))), nil + if arch == littleEndian { + return Float64(math.Float64frombits(binary.LittleEndian.Uint64(b))), nil + } + return Float64(math.Float64frombits(binary.BigEndian.Uint64(b))), nil case basetype.String: if isArray { var size byte diff --git a/proto/value_unmarshal_test.go b/proto/value_unmarshal_test.go index b1ef8493..f78bb506 100644 --- a/proto/value_unmarshal_test.go +++ b/proto/value_unmarshal_test.go @@ -5,7 +5,6 @@ package proto_test import ( - "encoding/binary" "errors" "fmt" "testing" @@ -80,46 +79,48 @@ func TestUnmarshal(t *testing.T) { } for i, tc := range tt { - t.Run(fmt.Sprintf("[%d] %T(%v)", i, tc.value.Any(), tc.value.Any()), func(t *testing.T) { - b, err := proto.Marshal(tc.value, binary.LittleEndian) - if err != nil { - t.Fatalf("marshal failed: %v", err) - } - - v, err := proto.Unmarshal(b, binary.LittleEndian, tc.ref, tc.isArray) - if err != nil { - if !errors.Is(err, tc.err) { - t.Fatalf("expected err: %v, got: %v", tc.err, err) + for arch := byte(0); arch <= 1; arch++ { + t.Run(fmt.Sprintf("[%d] %T(%v)", i, tc.value.Any(), tc.value.Any()), func(t *testing.T) { + b, err := tc.value.MarshalAppend(nil, arch) + if err != nil { + t.Fatalf("marshal failed: %v", err) } - return - } - - if tc.expected.Type() == proto.TypeInvalid { - tc.expected = tc.value - } - if diff := cmp.Diff(v, tc.expected, - cmp.Transformer("Value", func(val proto.Value) any { - return val.Any() - }), - ); diff != "" { - t.Fatal(diff) - } - // Extra check for bytes, the value should be copied - if in := tc.value.SliceUint8(); in != nil { - out := v.SliceUint8() - if out == nil { + v, err := proto.UnmarshalValue(b, arch, tc.ref, tc.isArray) + if err != nil { + if !errors.Is(err, tc.err) { + t.Fatalf("expected err: %v, got: %v", tc.err, err) + } return } - in[0] = 255 - out[0] = 100 + if tc.expected.Type() == proto.TypeInvalid { + tc.expected = tc.value + } + if diff := cmp.Diff(v, tc.expected, + cmp.Transformer("Value", func(val proto.Value) any { + return val.Any() + }), + ); diff != "" { + t.Fatal(diff) + } + + // Extra check for bytes, the value should be copied + if in := tc.value.SliceUint8(); in != nil { + out := v.SliceUint8() + if out == nil { + return + } - if in[0] == out[0] { - t.Fatalf("slice of bytes should not be referenced") + in[0] = 255 + out[0] = 100 + + if in[0] == out[0] { + t.Fatalf("slice of bytes should not be referenced") + } } - } - }) + }) + } } } @@ -130,3 +131,14 @@ func stringsToBytes(vals ...string) []byte { } return b } + +func BenchmarkUnmarshalValue(b *testing.B) { + b.StopTimer() + v := proto.Uint32(100) + buf, _ := v.MarshalAppend(nil, 0) + b.StartTimer() + + for i := 0; i < b.N; i++ { + _, _ = proto.UnmarshalValue(buf, 0, basetype.Uint32, false) + } +}