Skip to content

Commit 845ca51

Browse files
authored
internal: Add and use a generic pool instead of using sync.Pool directly (#1262)
1 parent 42b7ef0 commit 845ca51

File tree

10 files changed

+211
-53
lines changed

10 files changed

+211
-53
lines changed

buffer/pool.go

+12-8
Original file line numberDiff line numberDiff line change
@@ -20,25 +20,29 @@
2020

2121
package buffer
2222

23-
import "sync"
23+
import (
24+
"go.uber.org/zap/internal/pool"
25+
)
2426

2527
// A Pool is a type-safe wrapper around a sync.Pool.
2628
type Pool struct {
27-
p *sync.Pool
29+
p *pool.Pool[*Buffer]
2830
}
2931

3032
// NewPool constructs a new Pool.
3133
func NewPool() Pool {
32-
return Pool{p: &sync.Pool{
33-
New: func() interface{} {
34-
return &Buffer{bs: make([]byte, 0, _size)}
35-
},
36-
}}
34+
return Pool{
35+
p: pool.New(func() *Buffer {
36+
return &Buffer{
37+
bs: make([]byte, 0, _size),
38+
}
39+
}),
40+
}
3741
}
3842

3943
// Get retrieves a Buffer from the pool, creating one if necessary.
4044
func (p Pool) Get() *Buffer {
41-
buf := p.p.Get().(*Buffer)
45+
buf := p.p.Get()
4246
buf.Reset()
4347
buf.pool = p
4448
return buf

error.go

+4-5
Original file line numberDiff line numberDiff line change
@@ -21,14 +21,13 @@
2121
package zap
2222

2323
import (
24-
"sync"
25-
24+
"go.uber.org/zap/internal/pool"
2625
"go.uber.org/zap/zapcore"
2726
)
2827

29-
var _errArrayElemPool = sync.Pool{New: func() interface{} {
28+
var _errArrayElemPool = pool.New(func() *errArrayElem {
3029
return &errArrayElem{}
31-
}}
30+
})
3231

3332
// Error is shorthand for the common idiom NamedError("error", err).
3433
func Error(err error) Field {
@@ -60,7 +59,7 @@ func (errs errArray) MarshalLogArray(arr zapcore.ArrayEncoder) error {
6059
// potentially an "errorVerbose" attribute, we need to wrap it in a
6160
// type that implements LogObjectMarshaler. To prevent this from
6261
// allocating, pool the wrapper type.
63-
elem := _errArrayElemPool.Get().(*errArrayElem)
62+
elem := _errArrayElemPool.Get()
6463
elem.error = errs[i]
6564
arr.AppendObject(elem)
6665
elem.error = nil

internal/pool/pool.go

+58
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
// Copyright (c) 2023 Uber Technologies, Inc.
2+
//
3+
// Permission is hereby granted, free of charge, to any person obtaining a copy
4+
// of this software and associated documentation files (the "Software"), to deal
5+
// in the Software without restriction, including without limitation the rights
6+
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7+
// copies of the Software, and to permit persons to whom the Software is
8+
// furnished to do so, subject to the following conditions:
9+
//
10+
// The above copyright notice and this permission notice shall be included in
11+
// all copies or substantial portions of the Software.
12+
//
13+
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14+
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15+
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16+
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17+
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18+
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19+
// THE SOFTWARE.
20+
21+
// Package pool provides internal pool utilities.
22+
package pool
23+
24+
import (
25+
"sync"
26+
)
27+
28+
// A Pool is a generic wrapper around [sync.Pool] to provide strongly-typed
29+
// object pooling.
30+
//
31+
// Note that SA6002 (ref: https://staticcheck.io/docs/checks/#SA6002) will
32+
// not be detected, so all internal pool use must take care to only store
33+
// pointer types.
34+
type Pool[T any] struct {
35+
pool sync.Pool
36+
}
37+
38+
// New returns a new [Pool] for T, and will use fn to construct new Ts when
39+
// the pool is empty.
40+
func New[T any](fn func() T) *Pool[T] {
41+
return &Pool[T]{
42+
pool: sync.Pool{
43+
New: func() any {
44+
return fn()
45+
},
46+
},
47+
}
48+
}
49+
50+
// Get gets a T from the pool, or creates a new one if the pool is empty.
51+
func (p *Pool[T]) Get() T {
52+
return p.pool.Get().(T)
53+
}
54+
55+
// Put returns x into the pool.
56+
func (p *Pool[T]) Put(x T) {
57+
p.pool.Put(x)
58+
}

internal/pool/pool_test.go

+106
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
// Copyright (c) 2023 Uber Technologies, Inc.
2+
//
3+
// Permission is hereby granted, free of charge, to any person obtaining a copy
4+
// of this software and associated documentation files (the "Software"), to deal
5+
// in the Software without restriction, including without limitation the rights
6+
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7+
// copies of the Software, and to permit persons to whom the Software is
8+
// furnished to do so, subject to the following conditions:
9+
//
10+
// The above copyright notice and this permission notice shall be included in
11+
// all copies or substantial portions of the Software.
12+
//
13+
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14+
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15+
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16+
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17+
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18+
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19+
// THE SOFTWARE.
20+
21+
package pool_test
22+
23+
import (
24+
"runtime/debug"
25+
"sync"
26+
"testing"
27+
28+
"github.com/stretchr/testify/require"
29+
"go.uber.org/zap/internal/pool"
30+
)
31+
32+
type pooledValue[T any] struct {
33+
value T
34+
}
35+
36+
func TestNew(t *testing.T) {
37+
// Disable GC to avoid the victim cache during the test.
38+
defer debug.SetGCPercent(debug.SetGCPercent(-1))
39+
40+
p := pool.New(func() *pooledValue[string] {
41+
return &pooledValue[string]{
42+
value: "new",
43+
}
44+
})
45+
46+
// Probabilistically, 75% of sync.Pool.Put calls will succeed when -race
47+
// is enabled (see ref below); attempt to make this quasi-deterministic by
48+
// brute force (i.e., put significantly more objects in the pool than we
49+
// will need for the test) in order to avoid testing without race enabled.
50+
//
51+
// ref: https://cs.opensource.google/go/go/+/refs/tags/go1.20.2:src/sync/pool.go;l=100-103
52+
for i := 0; i < 1_000; i++ {
53+
p.Put(&pooledValue[string]{
54+
value: t.Name(),
55+
})
56+
}
57+
58+
// Ensure that we always get the expected value. Note that this must only
59+
// run a fraction of the number of times that Put is called above.
60+
for i := 0; i < 10; i++ {
61+
func() {
62+
x := p.Get()
63+
defer p.Put(x)
64+
require.Equal(t, t.Name(), x.value)
65+
}()
66+
}
67+
68+
// Depool all objects that might be in the pool to ensure that it's empty.
69+
for i := 0; i < 1_000; i++ {
70+
p.Get()
71+
}
72+
73+
// Now that the pool is empty, it should use the value specified in the
74+
// underlying sync.Pool.New func.
75+
require.Equal(t, "new", p.Get().value)
76+
}
77+
78+
func TestNew_Race(t *testing.T) {
79+
p := pool.New(func() *pooledValue[int] {
80+
return &pooledValue[int]{
81+
value: -1,
82+
}
83+
})
84+
85+
var wg sync.WaitGroup
86+
defer wg.Wait()
87+
88+
// Run a number of goroutines that read and write pool object fields to
89+
// tease out races.
90+
for i := 0; i < 1_000; i++ {
91+
i := i
92+
93+
wg.Add(1)
94+
go func() {
95+
defer wg.Done()
96+
97+
x := p.Get()
98+
defer p.Put(x)
99+
100+
// Must both read and write the field.
101+
if n := x.value; n >= -1 {
102+
x.value = i
103+
}
104+
}()
105+
}
106+
}

stacktrace.go

+7-9
Original file line numberDiff line numberDiff line change
@@ -22,19 +22,17 @@ package zap
2222

2323
import (
2424
"runtime"
25-
"sync"
2625

2726
"go.uber.org/zap/buffer"
2827
"go.uber.org/zap/internal/bufferpool"
28+
"go.uber.org/zap/internal/pool"
2929
)
3030

31-
var _stacktracePool = sync.Pool{
32-
New: func() interface{} {
33-
return &stacktrace{
34-
storage: make([]uintptr, 64),
35-
}
36-
},
37-
}
31+
var _stacktracePool = pool.New(func() *stacktrace {
32+
return &stacktrace{
33+
storage: make([]uintptr, 64),
34+
}
35+
})
3836

3937
type stacktrace struct {
4038
pcs []uintptr // program counters; always a subslice of storage
@@ -68,7 +66,7 @@ const (
6866
//
6967
// The caller must call Free on the returned stacktrace after using it.
7068
func captureStacktrace(skip int, depth stacktraceDepth) *stacktrace {
71-
stack := _stacktracePool.Get().(*stacktrace)
69+
stack := _stacktracePool.Get()
7270

7371
switch depth {
7472
case stacktraceFirst:

zapcore/console_encoder.go

+7-7
Original file line numberDiff line numberDiff line change
@@ -22,20 +22,20 @@ package zapcore
2222

2323
import (
2424
"fmt"
25-
"sync"
2625

2726
"go.uber.org/zap/buffer"
2827
"go.uber.org/zap/internal/bufferpool"
28+
"go.uber.org/zap/internal/pool"
2929
)
3030

31-
var _sliceEncoderPool = sync.Pool{
32-
New: func() interface{} {
33-
return &sliceArrayEncoder{elems: make([]interface{}, 0, 2)}
34-
},
35-
}
31+
var _sliceEncoderPool = pool.New(func() *sliceArrayEncoder {
32+
return &sliceArrayEncoder{
33+
elems: make([]interface{}, 0, 2),
34+
}
35+
})
3636

3737
func getSliceEncoder() *sliceArrayEncoder {
38-
return _sliceEncoderPool.Get().(*sliceArrayEncoder)
38+
return _sliceEncoderPool.Get()
3939
}
4040

4141
func putSliceEncoder(e *sliceArrayEncoder) {

zapcore/entry.go

+8-10
Original file line numberDiff line numberDiff line change
@@ -24,25 +24,23 @@ import (
2424
"fmt"
2525
"runtime"
2626
"strings"
27-
"sync"
2827
"time"
2928

3029
"go.uber.org/multierr"
3130
"go.uber.org/zap/internal/bufferpool"
3231
"go.uber.org/zap/internal/exit"
32+
"go.uber.org/zap/internal/pool"
3333
)
3434

35-
var (
36-
_cePool = sync.Pool{New: func() interface{} {
37-
// Pre-allocate some space for cores.
38-
return &CheckedEntry{
39-
cores: make([]Core, 4),
40-
}
41-
}}
42-
)
35+
var _cePool = pool.New(func() *CheckedEntry {
36+
// Pre-allocate some space for cores.
37+
return &CheckedEntry{
38+
cores: make([]Core, 4),
39+
}
40+
})
4341

4442
func getCheckedEntry() *CheckedEntry {
45-
ce := _cePool.Get().(*CheckedEntry)
43+
ce := _cePool.Get()
4644
ce.reset()
4745
return ce
4846
}

zapcore/error.go

+5-4
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,8 @@ package zapcore
2323
import (
2424
"fmt"
2525
"reflect"
26-
"sync"
26+
27+
"go.uber.org/zap/internal/pool"
2728
)
2829

2930
// Encodes the given error into fields of an object. A field with the given
@@ -103,17 +104,17 @@ func (errs errArray) MarshalLogArray(arr ArrayEncoder) error {
103104
return nil
104105
}
105106

106-
var _errArrayElemPool = sync.Pool{New: func() interface{} {
107+
var _errArrayElemPool = pool.New(func() *errArrayElem {
107108
return &errArrayElem{}
108-
}}
109+
})
109110

110111
// Encodes any error into a {"error": ...} re-using the same errors logic.
111112
//
112113
// May be passed in place of an array to build a single-element array.
113114
type errArrayElem struct{ err error }
114115

115116
func newErrArrayElem(err error) *errArrayElem {
116-
e := _errArrayElemPool.Get().(*errArrayElem)
117+
e := _errArrayElemPool.Get()
117118
e.err = err
118119
return e
119120
}

zapcore/json_encoder.go

+4-8
Original file line numberDiff line numberDiff line change
@@ -23,24 +23,20 @@ package zapcore
2323
import (
2424
"encoding/base64"
2525
"math"
26-
"sync"
2726
"time"
2827
"unicode/utf8"
2928

3029
"go.uber.org/zap/buffer"
3130
"go.uber.org/zap/internal/bufferpool"
31+
"go.uber.org/zap/internal/pool"
3232
)
3333

3434
// For JSON-escaping; see jsonEncoder.safeAddString below.
3535
const _hex = "0123456789abcdef"
3636

37-
var _jsonPool = sync.Pool{New: func() interface{} {
37+
var _jsonPool = pool.New(func() *jsonEncoder {
3838
return &jsonEncoder{}
39-
}}
40-
41-
func getJSONEncoder() *jsonEncoder {
42-
return _jsonPool.Get().(*jsonEncoder)
43-
}
39+
})
4440

4541
func putJSONEncoder(enc *jsonEncoder) {
4642
if enc.reflectBuf != nil {
@@ -354,7 +350,7 @@ func (enc *jsonEncoder) Clone() Encoder {
354350
}
355351

356352
func (enc *jsonEncoder) clone() *jsonEncoder {
357-
clone := getJSONEncoder()
353+
clone := _jsonPool.Get()
358354
clone.EncoderConfig = enc.EncoderConfig
359355
clone.spaced = enc.spaced
360356
clone.openNamespaces = enc.openNamespaces

0 commit comments

Comments
 (0)