Skip to content

Commit 827209b

Browse files
authored
perf: reuse requests to reduce allocs (#127)
1 parent 415f4f1 commit 827209b

File tree

9 files changed

+198
-18
lines changed

9 files changed

+198
-18
lines changed

decoder/compile.go

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -76,8 +76,7 @@ func compile(typ reflect.Type, tagKey string, isPtr bool) (decoder, error) {
7676
case reflect.Bool:
7777
decoders = append(decoders, decodeBool(set[bool](ptr, i, t), tag))
7878
case reflect.Slice:
79-
_, sk, _ := typeKind(t.Elem())
80-
switch sk {
79+
switch t.Elem().Kind() {
8180
case reflect.String:
8281
decoders = append(decoders, decodeStrings(set[[]string](ptr, i, t), tag))
8382
case reflect.Uint8:

decoder/decode.go

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,12 @@ type CachedDecoder[V any] struct {
6060
}
6161

6262
func NewCached[V any](v V, tag string) (*CachedDecoder[V], error) {
63-
t, k, ptr := typeKind(reflect.TypeOf(v))
63+
t := reflect.TypeOf(v)
64+
if t == nil {
65+
return nil, ErrUnsupportedType
66+
}
67+
68+
t, k, ptr := typeKind(t)
6469
if k != reflect.Struct {
6570
return nil, ErrUnsupportedType
6671
}

decoder/decode_test.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,10 @@ func TestDecodeError(t *testing.T) {
144144
target any
145145
error error
146146
}{
147+
{
148+
target: nil,
149+
error: decoder.ErrUnsupportedType,
150+
},
147151
{
148152
target: "",
149153
error: decoder.ErrUnsupportedType,

encoding/protobuf/protobuf.go

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,7 @@ var messageType = reflect.TypeOf((*proto.Message)(nil)).Elem()
1313
func unmarshal(b []byte, v any) error {
1414
// TODO: Cache reflect results to improve performance.
1515
elem := reflect.ValueOf(v).Elem()
16-
if elem.Type().Implements(messageType) && elem.IsNil() {
17-
elem.Set(reflect.New(elem.Type().Elem()))
16+
if elem.Type().Implements(messageType) {
1817
v = elem.Interface()
1918
}
2019

export_test.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
11
package don
22

33
var MakeNilCheck = makeNilCheck
4+
5+
func NewRequestPool[T any](v T) pool[T] {
6+
return newRequestPool(v)
7+
}

handler.go

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ type Handle[T, O any] func(ctx context.Context, request T) (O, error)
2626

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

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

41-
var (
42-
req = new(T)
43-
res any
44-
err error
45-
)
42+
var res any
4643

47-
err = decodeRequest(req, ctx, p)
44+
req := pool.Get()
45+
err := decodeRequest(req, ctx, p)
4846
if err != nil {
4947
res = Error(err, getStatusCode(err, http.StatusBadRequest))
5048
} else {
@@ -53,6 +51,7 @@ func H[T, O any](handle Handle[T, O]) httprouter.Handle {
5351
res = Error(err, 0)
5452
}
5553
}
54+
pool.Put(req)
5655

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

internal/test/encoding.go

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -39,18 +39,18 @@ func Encoding[T any](t *testing.T, opt EncodingOptions[T]) {
3939
func Decode[T any](t *testing.T, opt EncodingOptions[T]) {
4040
t.Helper()
4141

42-
var got T
42+
var diff string
4343

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

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

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

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

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

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

116+
v := new(T)
117+
if val := reflect.ValueOf(v).Elem(); val.Kind() == reflect.Pointer {
118+
val.Set(reflect.New(val.Type().Elem()))
119+
}
120+
116121
for i := 0; i < b.N; i++ {
117122
rd.Seek(0, io.SeekStart) //nolint:errcheck
118-
dec(ctx, new(T)) //nolint:errcheck
123+
dec(ctx, v) //nolint:errcheck
119124
}
120125
}
121126

pool.go

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
package don
2+
3+
import (
4+
"reflect"
5+
"sync"
6+
)
7+
8+
type pool[T any] interface {
9+
Get() *T
10+
Put(*T)
11+
}
12+
13+
type resetter interface {
14+
Reset()
15+
}
16+
17+
var resetterType = reflect.TypeOf((*resetter)(nil)).Elem()
18+
19+
type requestPool[T any] struct {
20+
pool sync.Pool
21+
reset func(*T)
22+
}
23+
24+
func newRequestPool[T any](zero T) pool[T] {
25+
typ := reflect.TypeOf(zero)
26+
if typ == nil {
27+
return &fakePool[T]{&zero}
28+
}
29+
30+
p := &requestPool[T]{}
31+
32+
if typ.Kind() != reflect.Pointer {
33+
p.pool.New = func() any {
34+
return new(T)
35+
}
36+
p.reset = func(v *T) {
37+
*v = zero
38+
}
39+
} else {
40+
elem := typ.Elem()
41+
p.pool.New = func() any {
42+
v := reflect.New(elem).Interface().(T) //nolint:forcetypeassert
43+
return &v
44+
}
45+
46+
if typ.Implements(resetterType) {
47+
p.reset = func(v *T) {
48+
any(*v).(resetter).Reset() //nolint:forcetypeassert
49+
}
50+
} else {
51+
zeroValue := reflect.New(elem).Elem()
52+
p.reset = func(v *T) {
53+
reflect.ValueOf(v).Elem().Elem().Set(zeroValue)
54+
}
55+
}
56+
}
57+
58+
return p
59+
}
60+
61+
func (p *requestPool[T]) Get() *T {
62+
return p.pool.Get().(*T) //nolint:forcetypeassert
63+
}
64+
65+
func (p *requestPool[T]) Put(v *T) {
66+
p.reset(v)
67+
p.pool.Put(v)
68+
}
69+
70+
type fakePool[T any] struct{ v *T }
71+
72+
func (p *fakePool[T]) Get() *T { return p.v }
73+
74+
func (p *fakePool[T]) Put(*T) {}

pool_test.go

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
package don_test
2+
3+
import (
4+
"reflect"
5+
"testing"
6+
7+
"github.com/abemedia/go-don"
8+
)
9+
10+
func TestRequestPool(t *testing.T) {
11+
type item struct {
12+
String string
13+
Pointer *string
14+
}
15+
16+
t.Run("Nil", func(t *testing.T) {
17+
var zero any
18+
pool := don.NewRequestPool(zero)
19+
20+
pool.Put(pool.Get())
21+
22+
if !reflect.DeepEqual(&zero, pool.Get()) {
23+
t.Fatal("should be zero value")
24+
}
25+
})
26+
27+
t.Run("Struct", func(t *testing.T) {
28+
zero := item{}
29+
pool := don.NewRequestPool(zero)
30+
31+
for i := 0; i < 100; i++ {
32+
v := pool.Get()
33+
v.String = "test"
34+
v.Pointer = &v.String
35+
pool.Put(v)
36+
}
37+
38+
for i := 0; i < 100; i++ {
39+
if !reflect.DeepEqual(&zero, pool.Get()) {
40+
t.Fatal("should be zero value")
41+
}
42+
}
43+
})
44+
45+
t.Run("Pointer", func(t *testing.T) {
46+
zero := &item{}
47+
pool := don.NewRequestPool(zero)
48+
49+
for i := 0; i < 100; i++ {
50+
p := pool.Get()
51+
v := *p
52+
v.String = "test"
53+
v.Pointer = &v.String
54+
pool.Put(p)
55+
}
56+
57+
for i := 0; i < 100; i++ {
58+
if !reflect.DeepEqual(&zero, pool.Get()) {
59+
t.Fatal("should be zero value")
60+
}
61+
}
62+
})
63+
64+
t.Run("Resetter", func(t *testing.T) {
65+
zero := &itemResetter{}
66+
pool := don.NewRequestPool(zero)
67+
68+
for i := 0; i < 100; i++ {
69+
p := pool.Get()
70+
v := *p
71+
v.String = "test"
72+
v.Pointer = &v.String
73+
pool.Put(p)
74+
}
75+
76+
for i := 0; i < 100; i++ {
77+
if !reflect.DeepEqual(&zero, pool.Get()) {
78+
t.Fatal("should be zero value")
79+
}
80+
}
81+
})
82+
}
83+
84+
type itemResetter struct {
85+
String string
86+
Pointer *string
87+
}
88+
89+
func (ir *itemResetter) Reset() {
90+
*ir = itemResetter{}
91+
}

0 commit comments

Comments
 (0)