diff --git a/.github/workflows/testing.yaml b/.github/workflows/testing.yaml index 0281a0b..7f7ec3c 100644 --- a/.github/workflows/testing.yaml +++ b/.github/workflows/testing.yaml @@ -51,10 +51,18 @@ jobs: run: make coverage coveralls-deps coveralls run-benchmarks: - if: false + if: (github.event_name == 'push') || + (github.event_name == 'pull_request' && + github.event.pull_request.head.repo.full_name != github.repository) || + (github.event_name == 'workflow_dispatch') runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + golang: ['1.24', 'stable'] + steps: - uses: actions/checkout@v4 - uses: actions/setup-go@v5 diff --git a/Makefile b/Makefile index 0e6d57d..2a7bc6d 100644 --- a/Makefile +++ b/Makefile @@ -17,6 +17,11 @@ testrace: @echo "Running tests with race flag" @go test ./... -count=100 -race +.PHONY: bench +bench: + @echo "Running benchmarks" + @go test ./... -count=1 -bench=. -benchmem + .PHONY: coverage coverage: @echo "Running tests with coveralls" diff --git a/bench_ext_test.go b/bench_ext_test.go new file mode 100644 index 0000000..d92f072 --- /dev/null +++ b/bench_ext_test.go @@ -0,0 +1,363 @@ +package option_test + +import ( + "bytes" + "errors" + "slices" + "testing" + + "github.com/tarantool/go-option" + "github.com/vmihailenco/msgpack/v5" + "github.com/vmihailenco/msgpack/v5/msgpcode" +) + +type BenchExt struct { + data []byte +} + +func (e *BenchExt) MarshalMsgpack() ([]byte, error) { + return e.data, nil +} + +func (e *BenchExt) UnmarshalMsgpack(b []byte) error { + e.data = slices.Clone(b) + return nil +} + +func (e *BenchExt) ExtType() int8 { + return 8 +} + +type Optional1BenchExt struct { + value BenchExt + set bool +} + +func SomeOptional1BenchExt(value BenchExt) Optional1BenchExt { + return Optional1BenchExt{value: value, set: true} +} + +func NoneOptional1BenchExt() Optional1BenchExt { + return Optional1BenchExt{} +} + +var ( + NilBytes = []byte{msgpcode.Nil} +) + +func (o Optional1BenchExt) MarshalMsgpack() ([]byte, error) { + if o.set { + return o.value.MarshalMsgpack() + } + return NilBytes, nil +} + +func (o *Optional1BenchExt) UnmarshalMsgpack(b []byte) error { + if b[0] == msgpcode.Nil { + o.set = false + return nil + } + o.set = true + return o.value.UnmarshalMsgpack(b) +} + +func (o Optional1BenchExt) EncodeMsgpack(enc *msgpack.Encoder) error { + switch { + case !o.set: + return enc.EncodeNil() + default: + mpdata, err := o.value.MarshalMsgpack() + if err != nil { + return err + } + + err = enc.EncodeExtHeader(o.value.ExtType(), len(mpdata)) + if err != nil { + return err + } + + mpdataLen, err := enc.Writer().Write(mpdata) + switch { + case err != nil: + return err + case mpdataLen != len(mpdata): + return errors.New("failed to write mpdata") + } + + return nil + } +} + +func (o *Optional1BenchExt) DecodeMsgpack(dec *msgpack.Decoder) error { + code, err := dec.PeekCode() + if err != nil { + return err + } + + switch { + case code == msgpcode.Nil: + o.set = false + case msgpcode.IsExt(code): + extID, extLen, err := dec.DecodeExtHeader() + switch { + case err != nil: + return err + case extID != o.value.ExtType(): + return errors.New("unexpected extension type") + default: + ext := make([]byte, extLen) + + err := dec.ReadFull(ext) + if err != nil { + return err + } + + err = o.value.UnmarshalMsgpack(ext) + if err != nil { + return err + } + } + default: + return errors.New("unexpected code") + } + return nil +} + +type Optional2BenchExt struct { + value BenchExt + set bool +} + +func SomeOptional2BenchExt(value BenchExt) Optional2BenchExt { + return Optional2BenchExt{value: value, set: true} +} + +func NoneOptional2BenchExt() Optional2BenchExt { + return Optional2BenchExt{} +} + +func (o Optional2BenchExt) MarshalMsgpack() ([]byte, error) { + if o.set { + return o.value.MarshalMsgpack() + } + return NilBytes, nil +} + +func (o *Optional2BenchExt) UnmarshalMsgpack(b []byte) error { + if b[0] == msgpcode.Nil { + o.set = false + return nil + } + o.set = true + return o.value.UnmarshalMsgpack(b) +} + +func (o Optional2BenchExt) EncodeMsgpack(enc *msgpack.Encoder) error { + switch { + case !o.set: + return enc.EncodeNil() + default: + return enc.Encode(&o.value) + } +} + +func (o *Optional2BenchExt) DecodeMsgpack(dec *msgpack.Decoder) error { + code, err := dec.PeekCode() + if err != nil { + return err + } + + switch { + case code == msgpcode.Nil: + o.set = false + return nil + case msgpcode.IsExt(code): + return dec.Decode(&o.value) + default: + return errors.New("unexpected code") + } +} + +type MsgpackExtInterface interface { + ExtType() int8 + msgpack.Marshaler + msgpack.Unmarshaler +} + +type OptionalGenericStructWithInterface[T MsgpackExtInterface] struct { + value T + set bool +} + +func NewOptionalGenericStructWithInterface[T MsgpackExtInterface](value T) *OptionalGenericStructWithInterface[T] { + return &OptionalGenericStructWithInterface[T]{ + value: value, + set: true, + } +} + +func NewEmptyOptionalGenericStructWithInterface[T MsgpackExtInterface]() *OptionalGenericStructWithInterface[T] { + return &OptionalGenericStructWithInterface[T]{ + set: false, + } +} + +func (o *OptionalGenericStructWithInterface[T]) DecodeMsgpack(d *msgpack.Decoder) error { + code, err := d.PeekCode() + if err != nil { + return err + } + + switch { + case code == msgpcode.Nil: + o.set = false + case msgpcode.IsExt(code): + o.set = true + + extID, extLen, err := d.DecodeExtHeader() + switch { + case err != nil: + return err + case extID != o.value.ExtType(): + return errors.New("unexpected extension type") + default: + ext := make([]byte, extLen) + + err := d.ReadFull(ext) + if err != nil { + return err + } + + err = o.value.UnmarshalMsgpack(ext) + if err != nil { + return err + } + } + + default: + return errors.New("unexpected type") + } + + return nil +} + +func (o *OptionalGenericStructWithInterface[T]) EncodeMsgpack(e *msgpack.Encoder) error { + switch { + case !o.set: + return e.EncodeNil() + default: + mpdata, err := o.value.MarshalMsgpack() + if err != nil { + return err + } + + err = e.EncodeExtHeader(o.value.ExtType(), len(mpdata)) + if err != nil { + return err + } + + mpdataLen, err := e.Writer().Write(mpdata) + switch { + case err != nil: + return err + case mpdataLen != len(mpdata): + return errors.New("failed to write mpdata") + } + + return nil + } + +} + +func BenchmarkExtension(b *testing.B) { + msgpack.RegisterExt(8, &BenchExt{}) + + var buf bytes.Buffer + buf.Grow(4096) + + enc := msgpack.GetEncoder() + enc.Reset(&buf) + + dec := msgpack.GetDecoder() + dec.Reset(&buf) + + b.Run("Optional1Bench", func(b *testing.B) { + for b.Loop() { + o := SomeOptional1BenchExt(BenchExt{[]byte{1, 2, 3}}) + + err := o.EncodeMsgpack(enc) + if err != nil { + b.Fatal(err) + } + + err = o.DecodeMsgpack(dec) + if err != nil { + b.Fatal(err) + } + } + }) + + b.Run("Optional2Bench", func(b *testing.B) { + for b.Loop() { + o := SomeOptional2BenchExt(BenchExt{[]byte{1, 2, 3}}) + + err := o.EncodeMsgpack(enc) + if err != nil { + b.Fatal(err) + } + + err = o.DecodeMsgpack(dec) + if err != nil { + b.Fatal(err) + } + } + }) + + b.Run("OptionalBenchGeneric", func(b *testing.B) { + for b.Loop() { + o := option.Some(BenchExt{[]byte{1, 2, 3}}) + + err := o.EncodeMsgpack(enc) + if err != nil { + b.Fatal(err) + } + + err = o.DecodeMsgpack(dec) + if err != nil { + b.Fatal(err) + } + } + }) + + b.Run("OptionalGenericStructWithInterface", func(b *testing.B) { + for b.Loop() { + o := NewOptionalGenericStructWithInterface(&BenchExt{[]byte{1, 2, 3}}) + + err := o.EncodeMsgpack(enc) + if err != nil { + b.Fatal(err) + } + + err = o.DecodeMsgpack(dec) + if err != nil { + b.Fatal(err) + } + } + }) + + b.Run("Default", func(b *testing.B) { + for b.Loop() { + o := BenchExt{[]byte{1, 2, 3}} + + err := enc.Encode(&o) + if err != nil { + b.Fatal(err) + } + + err = dec.Decode(&o) + if err != nil { + b.Fatal(err) + } + } + }) +} diff --git a/bench_generic_over_ptr_test.go b/bench_generic_over_ptr_test.go new file mode 100644 index 0000000..ed0d7b8 --- /dev/null +++ b/bench_generic_over_ptr_test.go @@ -0,0 +1,60 @@ +package option_test + +import ( + "github.com/vmihailenco/msgpack/v5" + "github.com/vmihailenco/msgpack/v5/msgpcode" +) + +type GenericOverPtr[T any] struct { + val *T +} + +func SomeOverPtr[T any](value T) GenericOverPtr[T] { + return GenericOverPtr[T]{val: &value} +} + +func NoneOverPtr[T any]() GenericOverPtr[T] { + return GenericOverPtr[T]{val: nil} +} + +func (o *GenericOverPtr[T]) Get() (T, bool) { + if o.val == nil { + return zero[T](), false + } + + return *o.val, true +} + +func (o *GenericOverPtr[T]) IsSome() bool { + return o.val != nil +} + +func (o *GenericOverPtr[T]) EncodeMsgpack(encoder *msgpack.Encoder) error { + if !o.IsSome() { + return encoder.EncodeNil() + } + + return encoder.Encode(*o.val) +} + +func (o *GenericOverPtr[T]) DecodeMsgpack(decoder *msgpack.Decoder) error { + code, err := decoder.PeekCode() + switch { + case err != nil: + return err + case code == msgpcode.Nil: + o.val = nil + return nil + } + + var val T + + err = decoder.Decode(&val) + if err != nil { + return err + } + + o.val = &val + + return nil +} diff --git a/bench_generic_over_slice_test.go b/bench_generic_over_slice_test.go new file mode 100644 index 0000000..51d9c26 --- /dev/null +++ b/bench_generic_over_slice_test.go @@ -0,0 +1,57 @@ +package option_test + +import ( + "github.com/vmihailenco/msgpack/v5" + "github.com/vmihailenco/msgpack/v5/msgpcode" +) + +type GenericOverSlice[T any] []T + +func SomeOverSlice[T any](value T) GenericOverSlice[T] { + return []T{value} +} + +func NoneOverSlice[T any]() GenericOverSlice[T] { + return []T{} +} + +func (o *GenericOverSlice[T]) Get() (T, bool) { + if len(*o) == 0 { + return zero[T](), false + } + + return (*o)[0], true +} + +func (o *GenericOverSlice[T]) IsSome() bool { + return len(*o) > 0 +} + +func (o *GenericOverSlice[T]) EncodeMsgpack(encoder *msgpack.Encoder) error { + if !o.IsSome() { + return encoder.EncodeNil() + } + return encoder.Encode((*o)[0]) +} + +func (o *GenericOverSlice[T]) DecodeMsgpack(decoder *msgpack.Decoder) error { + code, err := decoder.PeekCode() + if err != nil { + return err + } + + if code == msgpcode.Nil { + *o = nil + return nil + } + + var val T + + err = decoder.Decode(&val) + if err != nil { + return err + } + *o = []T{val} + + return nil +} diff --git a/bench_test.go b/bench_test.go new file mode 100644 index 0000000..c3d3f07 --- /dev/null +++ b/bench_test.go @@ -0,0 +1,261 @@ +package option_test + +import ( + "bytes" + "fmt" + "testing" + + "github.com/tarantool/go-option" + "github.com/vmihailenco/msgpack/v5" +) + +func BenchmarkNoneInt(b *testing.B) { + var val int + var ok bool + + b.Run("option.Int", func(b *testing.B) { + for b.Loop() { + o := option.NoneInt() + val, ok = o.Get() + if ok { + b.Fatal("Get() returned true") + } + } + }) + + b.Run("option.Generic[int]", func(b *testing.B) { + for b.Loop() { + o := option.None[int]() + val, ok = o.Get() + if ok { + b.Fatal("Get() returned true") + } + } + }) + + b.Run("GenericOverPtr[int]", func(b *testing.B) { + for b.Loop() { + o := NoneOverPtr[int]() + val, ok = o.Get() + if ok { + b.Fatal("Get() returned true") + } + } + }) + + b.Run("GenericOverSlice[int]", func(b *testing.B) { + for b.Loop() { + o := NoneOverSlice[int]() + val, ok = o.Get() + if ok { + b.Fatal("Get() returned true") + } + } + }) + + fmt.Println(val, ok) +} + +func BenchmarkNoneStruct(b *testing.B) { + var val ValueType + var ok bool + + b.Run("option.Generic[struct]", func(b *testing.B) { + for b.Loop() { + o := option.None[ValueType]() + val, ok = o.Get() + if ok { + b.Fatal("Get() returned true") + } + } + }) + + b.Run("GenericOverPtr[struct]", func(b *testing.B) { + for b.Loop() { + o := NoneOverPtr[ValueType]() + val, ok = o.Get() + if ok { + b.Fatal("Get() returned true") + } + } + }) + + b.Run("GenericOverSlice[struct]", func(b *testing.B) { + for b.Loop() { + o := NoneOverSlice[ValueType]() + val, ok = o.Get() + if ok { + b.Fatal("Get() returned true") + } + } + }) + + fmt.Println(val, ok) +} + +func BenchmarkIntSome(b *testing.B) { + var val int + var ok bool + + b.Run("option.Int", func(b *testing.B) { + for b.Loop() { + o := option.SomeInt(42) + val, ok = o.Get() + if !ok { + b.Fatal("Get() returned false") + } + } + }) + + b.Run("option.Generic[int]", func(b *testing.B) { + for b.Loop() { + o := option.Some[int](42) + val, ok = o.Get() + if !ok { + b.Fatal("Get() returned false") + } + } + }) + + b.Run("GenericOverPtr[int]", func(b *testing.B) { + for b.Loop() { + o := SomeOverPtr[int](42) + val, ok = o.Get() + if !ok { + b.Fatal("Get() returned false") + } + } + }) + + b.Run("GenericOverSlice[int]", func(b *testing.B) { + for b.Loop() { + o := SomeOverSlice[int](42) + val, ok = o.Get() + if !ok { + b.Fatal("Get() returned false") + } + } + }) + + fmt.Println(val, ok) +} + +func BenchmarkSomeStruct(b *testing.B) { + var val ValueType + var ok bool + + b.Run("option.Generic[struct]", func(b *testing.B) { + for b.Loop() { + o := option.Some[ValueType](ValueType{"foo", 42}) + val, ok = o.Get() + if !ok { + b.Fatal("Get() returned false") + } + } + }) + + b.Run("GenericOverPtr[struct]", func(b *testing.B) { + for b.Loop() { + o := SomeOverPtr[ValueType](ValueType{"foo", 42}) + val, ok = o.Get() + if !ok { + b.Fatal("Get() returned false") + } + } + }) + + b.Run("GenericOverSlice[struct]", func(b *testing.B) { + for b.Loop() { + o := SomeOverSlice[ValueType](ValueType{"foo", 42}) + val, ok = o.Get() + if !ok { + b.Fatal("Get() returned false") + } + } + }) + + fmt.Println(val, ok) +} + +func BenchmarkEncodeDecode(b *testing.B) { + var buf bytes.Buffer + buf.Grow(4096) + + enc := msgpack.GetEncoder() + enc.Reset(&buf) + + dec := msgpack.GetDecoder() + dec.Reset(&buf) + + b.Run("option.Int", func(b *testing.B) { + for b.Loop() { + o := option.SomeInt(42) + + err := o.EncodeMsgpack(enc) + if err != nil { + b.Errorf("EncodeMsgpack() failed: %v", err) + } + + err = o.DecodeMsgpack(dec) + if err != nil { + b.Errorf("DecodeMsgpack() failed: %v", err) + } + + buf.Reset() + } + }) + + b.Run("option.Generic[int]", func(b *testing.B) { + for b.Loop() { + o := option.Some[int](42) + + err := o.EncodeMsgpack(enc) + if err != nil { + b.Errorf("EncodeMsgpack() failed: %v", err) + } + + err = o.DecodeMsgpack(dec) + if err != nil { + b.Errorf("DecodeMsgpack() failed: %v", err) + } + + buf.Reset() + } + }) + + b.Run("GenericOverSlice[int]", func(b *testing.B) { + for b.Loop() { + o := SomeOverSlice[int](42) + + err := o.EncodeMsgpack(enc) + if err != nil { + b.Errorf("EncodeMsgpack() failed: %v", err) + } + + err = o.DecodeMsgpack(dec) + if err != nil { + b.Errorf("DecodeMsgpack() failed: %v", err) + } + + buf.Reset() + } + }) + + b.Run("GenericOverPtr[int]", func(b *testing.B) { + for b.Loop() { + o := SomeOverPtr[int](42) + + err := o.EncodeMsgpack(enc) + if err != nil { + b.Errorf("EncodeMsgpack() failed: %v", err) + } + + err = o.DecodeMsgpack(dec) + if err != nil { + b.Errorf("DecodeMsgpack() failed: %v", err) + } + + buf.Reset() + } + }) +} diff --git a/bench_typed_test.go b/bench_typed_test.go new file mode 100644 index 0000000..89c3b3e --- /dev/null +++ b/bench_typed_test.go @@ -0,0 +1,155 @@ +package option_test + +import ( + "fmt" + + "github.com/vmihailenco/msgpack/v5" + "github.com/vmihailenco/msgpack/v5/msgpcode" +) + +type OptionalInt struct { + value int + set bool +} + +func SomeOptionalInt(value int) OptionalInt { + return OptionalInt{value: value, set: true} +} + +func NoneInt() OptionalInt { + return OptionalInt{set: false} +} + +func (o *OptionalInt) Get() (int, bool) { + return o.value, o.set +} + +func (o *OptionalInt) IsSome() bool { + return o.set +} + +func (o *OptionalInt) EncodeMsgpack(encoder *msgpack.Encoder) error { + if !o.set { + return encoder.EncodeNil() + } + return encoder.EncodeInt(int64(o.value)) +} + +func (o *OptionalInt) DecodeMsgpack(decoder *msgpack.Decoder) error { + val, err := decoder.DecodeInt() + if err != nil { + return err + } + o.value = val + o.set = true + return nil +} + +type ValueType struct { + Value1 string + Value2 int +} + +type OptionalStruct struct { + value ValueType + set bool +} + +func SomeOptionalStruct(value ValueType) OptionalStruct { + return OptionalStruct{value: value, set: true} +} + +func NoneOptionalStruct() OptionalStruct { + return OptionalStruct{set: false} +} + +func (o *OptionalStruct) Get() (ValueType, bool) { + return o.value, o.set +} + +func (o *OptionalStruct) HasValue() bool { + return o.set +} + +func (o *OptionalStruct) EncodeMsgpack(encoder *msgpack.Encoder) error { + var err error + if !o.set { + return encoder.EncodeNil() + } + err = encoder.EncodeMapLen(2) + if err != nil { + return err + } + err = encoder.EncodeString("Value1") + if err != nil { + return err + } + err = encoder.EncodeString(o.value.Value1) + if err != nil { + return err + } + err = encoder.EncodeString("Value2") + if err != nil { + return err + } + err = encoder.EncodeInt(int64(o.value.Value2)) + if err != nil { + return err + } + return nil +} + +func (o *OptionalStruct) DecodeMsgpack(decoder *msgpack.Decoder) error { + code, err := decoder.PeekCode() + if err != nil { + return err + } + + switch { + case code == msgpcode.Nil: + o.set = false + case code >= msgpcode.FixedMapLow && code <= msgpcode.FixedMapHigh: + fallthrough + case code == msgpcode.Map16 || code == msgpcode.Map32: + // ok + default: + return fmt.Errorf("unexpected code: %d", code) + } + + mapLen, err := decoder.DecodeMapLen() + switch { + case err != nil: + return err + case mapLen != 2: + return fmt.Errorf("unexpected map length: %d", mapLen) + } + + var ( + isValue1Set bool + isValue2Set bool + ) + + for i := range 2 { + key, err := decoder.DecodeString() + switch { + case err != nil: + return err + case key == "Value1" && !isValue1Set: + isValue1Set = true + o.value.Value1, err = decoder.DecodeString() + if err != nil { + return err + } + case key == "Value2" && !isValue2Set: + isValue2Set = true + o.value.Value2, err = decoder.DecodeInt() + if err != nil { + return err + } + default: + return fmt.Errorf("unexpected key: %s", key) + } + } + + return nil +} diff --git a/bench_utils_test.go b/bench_utils_test.go new file mode 100644 index 0000000..7183112 --- /dev/null +++ b/bench_utils_test.go @@ -0,0 +1,7 @@ +package option_test + +func zero[T any]() T { + var zero T + + return zero +} diff --git a/go.mod b/go.mod index 3dcb760..3faf874 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/tarantool/go-option -go 1.23.0 +go 1.24.0 require ( github.com/stretchr/testify v1.11.1