Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion .github/workflows/testing.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
5 changes: 5 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -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
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we really need the benchmem? If so - ok.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

-benchmem will show allocation count, I think it's necessary information for us to use.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We are interested in number of allocation and this option displays allocation statistic.


.PHONY: coverage
coverage:
@echo "Running tests with coveralls"
Expand Down
363 changes: 363 additions & 0 deletions bench_ext_test.go
Original file line number Diff line number Diff line change
@@ -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) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does it a go-idiomic practice to name the tests in the manner? Why do we need the CamelCase here? Why not just "optional simple bench" or so on?

Up to @bigbes .

Copy link
Collaborator

@bigbes bigbes Oct 21, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well, I don't have any strong opinion about naming of subtests. On one hand I'm preferring to have name that I can copy to cli to run subtest, but go-idiomatic way is to write clear names with spaces in lowercase.

Let's stick with go-idiomatic way there.

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)
}
}
})
}
Loading
Loading