Skip to content

Commit

Permalink
perf: reuse requests to reduce allocs (#127)
Browse files Browse the repository at this point in the history
  • Loading branch information
abemedia authored May 14, 2023
1 parent 415f4f1 commit 827209b
Show file tree
Hide file tree
Showing 9 changed files with 198 additions and 18 deletions.
3 changes: 1 addition & 2 deletions decoder/compile.go
Original file line number Diff line number Diff line change
Expand Up @@ -76,8 +76,7 @@ func compile(typ reflect.Type, tagKey string, isPtr bool) (decoder, error) {
case reflect.Bool:
decoders = append(decoders, decodeBool(set[bool](ptr, i, t), tag))
case reflect.Slice:
_, sk, _ := typeKind(t.Elem())
switch sk {
switch t.Elem().Kind() {
case reflect.String:
decoders = append(decoders, decodeStrings(set[[]string](ptr, i, t), tag))
case reflect.Uint8:
Expand Down
7 changes: 6 additions & 1 deletion decoder/decode.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,12 @@ type CachedDecoder[V any] struct {
}

func NewCached[V any](v V, tag string) (*CachedDecoder[V], error) {
t, k, ptr := typeKind(reflect.TypeOf(v))
t := reflect.TypeOf(v)
if t == nil {
return nil, ErrUnsupportedType
}

t, k, ptr := typeKind(t)
if k != reflect.Struct {
return nil, ErrUnsupportedType
}
Expand Down
4 changes: 4 additions & 0 deletions decoder/decode_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,10 @@ func TestDecodeError(t *testing.T) {
target any
error error
}{
{
target: nil,
error: decoder.ErrUnsupportedType,
},
{
target: "",
error: decoder.ErrUnsupportedType,
Expand Down
3 changes: 1 addition & 2 deletions encoding/protobuf/protobuf.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,7 @@ var messageType = reflect.TypeOf((*proto.Message)(nil)).Elem()
func unmarshal(b []byte, v any) error {
// TODO: Cache reflect results to improve performance.
elem := reflect.ValueOf(v).Elem()
if elem.Type().Implements(messageType) && elem.IsNil() {
elem.Set(reflect.New(elem.Type().Elem()))
if elem.Type().Implements(messageType) {
v = elem.Interface()
}

Expand Down
4 changes: 4 additions & 0 deletions export_test.go
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
package don

var MakeNilCheck = makeNilCheck

func NewRequestPool[T any](v T) pool[T] {
return newRequestPool(v)
}
11 changes: 5 additions & 6 deletions handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ type Handle[T, O any] func(ctx context.Context, request T) (O, error)

// H wraps your handler function with the Go generics magic.
func H[T, O any](handle Handle[T, O]) httprouter.Handle {
pool := newRequestPool(*new(T))
decodeRequest := newRequestDecoder(*new(T))
isNil := makeNilCheck(*new(O))

Expand All @@ -38,13 +39,10 @@ func H[T, O any](handle Handle[T, O]) httprouter.Handle {
return
}

var (
req = new(T)
res any
err error
)
var res any

err = decodeRequest(req, ctx, p)
req := pool.Get()
err := decodeRequest(req, ctx, p)
if err != nil {
res = Error(err, getStatusCode(err, http.StatusBadRequest))
} else {
Expand All @@ -53,6 +51,7 @@ func H[T, O any](handle Handle[T, O]) httprouter.Handle {
res = Error(err, 0)
}
}
pool.Put(req)

ctx.SetContentType(contentType + "; charset=utf-8")

Expand Down
19 changes: 12 additions & 7 deletions internal/test/encoding.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,18 +39,18 @@ func Encoding[T any](t *testing.T, opt EncodingOptions[T]) {
func Decode[T any](t *testing.T, opt EncodingOptions[T]) {
t.Helper()

var got T
var diff string

api := don.New(nil)
api.Post("/", don.H(func(ctx context.Context, req T) (don.Empty, error) {
got = req
return don.Empty{}, nil
api.Post("/", don.H(func(ctx context.Context, req T) (any, error) {
diff = cmp.Diff(opt.Parsed, req, ignoreUnexported[T]())
return nil, nil
}))

ctx := httptest.NewRequest(http.MethodPost, "/", opt.Raw, map[string]string{"Content-Type": opt.Mime})
api.RequestHandler()(ctx)

if diff := cmp.Diff(opt.Parsed, got, ignoreUnexported[T]()); diff != "" {
if diff != "" {
t.Error(diff)
}

Expand All @@ -63,7 +63,7 @@ func Encode[T any](t *testing.T, opt EncodingOptions[T]) {
t.Helper()

api := don.New(nil)
api.Post("/", don.H(func(ctx context.Context, req don.Empty) (T, error) {
api.Post("/", don.H(func(ctx context.Context, req any) (T, error) {
return opt.Parsed, nil
}))

Expand Down Expand Up @@ -113,9 +113,14 @@ func BenchmarkDecode[T any](b *testing.B, opt EncodingOptions[T]) {
ctx := httptest.NewRequest("POST", "/", "", nil)
ctx.Request.SetBodyStream(rd, len(opt.Raw))

v := new(T)
if val := reflect.ValueOf(v).Elem(); val.Kind() == reflect.Pointer {
val.Set(reflect.New(val.Type().Elem()))
}

for i := 0; i < b.N; i++ {
rd.Seek(0, io.SeekStart) //nolint:errcheck
dec(ctx, new(T)) //nolint:errcheck
dec(ctx, v) //nolint:errcheck
}
}

Expand Down
74 changes: 74 additions & 0 deletions pool.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
package don

import (
"reflect"
"sync"
)

type pool[T any] interface {
Get() *T
Put(*T)
}

type resetter interface {
Reset()
}

var resetterType = reflect.TypeOf((*resetter)(nil)).Elem()

type requestPool[T any] struct {
pool sync.Pool
reset func(*T)
}

func newRequestPool[T any](zero T) pool[T] {
typ := reflect.TypeOf(zero)
if typ == nil {
return &fakePool[T]{&zero}
}

p := &requestPool[T]{}

if typ.Kind() != reflect.Pointer {
p.pool.New = func() any {
return new(T)
}
p.reset = func(v *T) {
*v = zero
}
} else {
elem := typ.Elem()
p.pool.New = func() any {
v := reflect.New(elem).Interface().(T) //nolint:forcetypeassert
return &v
}

if typ.Implements(resetterType) {
p.reset = func(v *T) {
any(*v).(resetter).Reset() //nolint:forcetypeassert
}
} else {
zeroValue := reflect.New(elem).Elem()
p.reset = func(v *T) {
reflect.ValueOf(v).Elem().Elem().Set(zeroValue)
}
}
}

return p
}

func (p *requestPool[T]) Get() *T {
return p.pool.Get().(*T) //nolint:forcetypeassert
}

func (p *requestPool[T]) Put(v *T) {
p.reset(v)
p.pool.Put(v)
}

type fakePool[T any] struct{ v *T }

func (p *fakePool[T]) Get() *T { return p.v }

func (p *fakePool[T]) Put(*T) {}
91 changes: 91 additions & 0 deletions pool_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
package don_test

import (
"reflect"
"testing"

"github.com/abemedia/go-don"
)

func TestRequestPool(t *testing.T) {
type item struct {
String string
Pointer *string
}

t.Run("Nil", func(t *testing.T) {
var zero any
pool := don.NewRequestPool(zero)

pool.Put(pool.Get())

if !reflect.DeepEqual(&zero, pool.Get()) {
t.Fatal("should be zero value")
}
})

t.Run("Struct", func(t *testing.T) {
zero := item{}
pool := don.NewRequestPool(zero)

for i := 0; i < 100; i++ {
v := pool.Get()
v.String = "test"
v.Pointer = &v.String
pool.Put(v)
}

for i := 0; i < 100; i++ {
if !reflect.DeepEqual(&zero, pool.Get()) {
t.Fatal("should be zero value")
}
}
})

t.Run("Pointer", func(t *testing.T) {
zero := &item{}
pool := don.NewRequestPool(zero)

for i := 0; i < 100; i++ {
p := pool.Get()
v := *p
v.String = "test"
v.Pointer = &v.String
pool.Put(p)
}

for i := 0; i < 100; i++ {
if !reflect.DeepEqual(&zero, pool.Get()) {
t.Fatal("should be zero value")
}
}
})

t.Run("Resetter", func(t *testing.T) {
zero := &itemResetter{}
pool := don.NewRequestPool(zero)

for i := 0; i < 100; i++ {
p := pool.Get()
v := *p
v.String = "test"
v.Pointer = &v.String
pool.Put(p)
}

for i := 0; i < 100; i++ {
if !reflect.DeepEqual(&zero, pool.Get()) {
t.Fatal("should be zero value")
}
}
})
}

type itemResetter struct {
String string
Pointer *string
}

func (ir *itemResetter) Reset() {
*ir = itemResetter{}
}

0 comments on commit 827209b

Please sign in to comment.