Skip to content

Commit b378cae

Browse files
Steven Scottgaryburd
authored andcommitted
Add write buffer pooling
Add WriteBufferPool to Dialer and Upgrader. This field specifies a pool to use for write operations on a connection. Use of the pool can reduce memory use when there is a modest write volume over a large number of connections. Use larger of hijacked buffer and buffer allocated for connection (if any) as buffer for building handshake response. This decreases possible allocations when building the handshake response. Modify bufio reuse test to call Upgrade instead of the internal newConnBRW. Move the test from conn_test.go to server_test.go because it's a serer test. Update newConn and newConnBRW: - Move the bufio "hacks" from newConnBRW to separate functions and call these functions directly from Upgrade. - Rename newConn to newTestConn and move to conn_test.go. Shorten argument list to common use case. - Rename newConnBRW to newConn. - Add pool code to newConn.
1 parent 5fb9417 commit b378cae

File tree

9 files changed

+326
-115
lines changed

9 files changed

+326
-115
lines changed

client.go

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,17 @@ type Dialer struct {
6969
// do not limit the size of the messages that can be sent or received.
7070
ReadBufferSize, WriteBufferSize int
7171

72+
// WriteBufferPool is a pool of buffers for write operations. If the value
73+
// is not set, then write buffers are allocated to the connection for the
74+
// lifetime of the connection.
75+
//
76+
// A pool is most useful when the application has a modest volume of writes
77+
// across a large number of connections.
78+
//
79+
// Applications should use a single pool for each unique value of
80+
// WriteBufferSize.
81+
WriteBufferPool BufferPool
82+
7283
// Subprotocols specifies the client's requested subprotocols.
7384
Subprotocols []string
7485

@@ -277,7 +288,7 @@ func (d *Dialer) Dial(urlStr string, requestHeader http.Header) (*Conn, *http.Re
277288
}
278289
}
279290

280-
conn := newConn(netConn, false, d.ReadBufferSize, d.WriteBufferSize)
291+
conn := newConn(netConn, false, d.ReadBufferSize, d.WriteBufferSize, d.WriteBufferPool, nil, nil)
281292

282293
if err := req.Write(netConn); err != nil {
283294
return nil, nil, err

compression_test.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ func textMessages(num int) [][]byte {
4343

4444
func BenchmarkWriteNoCompression(b *testing.B) {
4545
w := ioutil.Discard
46-
c := newConn(fakeNetConn{Reader: nil, Writer: w}, false, 1024, 1024)
46+
c := newTestConn(nil, w, false)
4747
messages := textMessages(100)
4848
b.ResetTimer()
4949
for i := 0; i < b.N; i++ {
@@ -54,7 +54,7 @@ func BenchmarkWriteNoCompression(b *testing.B) {
5454

5555
func BenchmarkWriteWithCompression(b *testing.B) {
5656
w := ioutil.Discard
57-
c := newConn(fakeNetConn{Reader: nil, Writer: w}, false, 1024, 1024)
57+
c := newTestConn(nil, w, false)
5858
messages := textMessages(100)
5959
c.enableWriteCompression = true
6060
c.newCompressionWriter = compressNoContextTakeover
@@ -66,7 +66,7 @@ func BenchmarkWriteWithCompression(b *testing.B) {
6666
}
6767

6868
func TestValidCompressionLevel(t *testing.T) {
69-
c := newConn(fakeNetConn{}, false, 1024, 1024)
69+
c := newTestConn(nil, nil, false)
7070
for _, level := range []int{minCompressionLevel - 1, maxCompressionLevel + 1} {
7171
if err := c.SetCompressionLevel(level); err == nil {
7272
t.Errorf("no error for level %d", level)

conn.go

Lines changed: 45 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -223,6 +223,20 @@ func isValidReceivedCloseCode(code int) bool {
223223
return validReceivedCloseCodes[code] || (code >= 3000 && code <= 4999)
224224
}
225225

226+
// BufferPool represents a pool of buffers. The *sync.Pool type satisfies this
227+
// interface. The type of the value stored in a pool is not specified.
228+
type BufferPool interface {
229+
// Get gets a value from the pool or returns nil if the pool is empty.
230+
Get() interface{}
231+
// Put adds a value to the pool.
232+
Put(interface{})
233+
}
234+
235+
// writePoolData is the type added to the write buffer pool. This wrapper is
236+
// used to prevent applications from peeking at and depending on the values
237+
// added to the pool.
238+
type writePoolData struct{ buf []byte }
239+
226240
// The Conn type represents a WebSocket connection.
227241
type Conn struct {
228242
conn net.Conn
@@ -232,6 +246,8 @@ type Conn struct {
232246
// Write fields
233247
mu chan bool // used as mutex to protect write to conn
234248
writeBuf []byte // frame is constructed in this buffer.
249+
writePool BufferPool
250+
writeBufSize int
235251
writeDeadline time.Time
236252
writer io.WriteCloser // the current writer returned to the application
237253
isWriting bool // for best-effort concurrent write detection
@@ -263,71 +279,38 @@ type Conn struct {
263279
newDecompressionReader func(io.Reader) io.ReadCloser
264280
}
265281

266-
func newConn(conn net.Conn, isServer bool, readBufferSize, writeBufferSize int) *Conn {
267-
return newConnBRW(conn, isServer, readBufferSize, writeBufferSize, nil)
268-
}
269-
270-
type writeHook struct {
271-
p []byte
272-
}
273-
274-
func (wh *writeHook) Write(p []byte) (int, error) {
275-
wh.p = p
276-
return len(p), nil
277-
}
278-
279-
func newConnBRW(conn net.Conn, isServer bool, readBufferSize, writeBufferSize int, brw *bufio.ReadWriter) *Conn {
280-
mu := make(chan bool, 1)
281-
mu <- true
282+
func newConn(conn net.Conn, isServer bool, readBufferSize, writeBufferSize int, writeBufferPool BufferPool, br *bufio.Reader, writeBuf []byte) *Conn {
282283

283-
var br *bufio.Reader
284-
if readBufferSize == 0 && brw != nil && brw.Reader != nil {
285-
// Reuse the supplied bufio.Reader if the buffer has a useful size.
286-
// This code assumes that peek on a reader returns
287-
// bufio.Reader.buf[:0].
288-
brw.Reader.Reset(conn)
289-
if p, err := brw.Reader.Peek(0); err == nil && cap(p) >= 256 {
290-
br = brw.Reader
291-
}
292-
}
293284
if br == nil {
294285
if readBufferSize == 0 {
295286
readBufferSize = defaultReadBufferSize
296-
}
297-
if readBufferSize < maxControlFramePayloadSize {
287+
} else if readBufferSize < maxControlFramePayloadSize {
288+
// must be large enough for control frame
298289
readBufferSize = maxControlFramePayloadSize
299290
}
300291
br = bufio.NewReaderSize(conn, readBufferSize)
301292
}
302293

303-
var writeBuf []byte
304-
if writeBufferSize == 0 && brw != nil && brw.Writer != nil {
305-
// Use the bufio.Writer's buffer if the buffer has a useful size. This
306-
// code assumes that bufio.Writer.buf[:1] is passed to the
307-
// bufio.Writer's underlying writer.
308-
var wh writeHook
309-
brw.Writer.Reset(&wh)
310-
brw.Writer.WriteByte(0)
311-
brw.Flush()
312-
if cap(wh.p) >= maxFrameHeaderSize+256 {
313-
writeBuf = wh.p[:cap(wh.p)]
314-
}
294+
if writeBufferSize <= 0 {
295+
writeBufferSize = defaultWriteBufferSize
315296
}
297+
writeBufferSize += maxFrameHeaderSize
316298

317-
if writeBuf == nil {
318-
if writeBufferSize == 0 {
319-
writeBufferSize = defaultWriteBufferSize
320-
}
321-
writeBuf = make([]byte, writeBufferSize+maxFrameHeaderSize)
299+
if writeBuf == nil && writeBufferPool == nil {
300+
writeBuf = make([]byte, writeBufferSize)
322301
}
323302

303+
mu := make(chan bool, 1)
304+
mu <- true
324305
c := &Conn{
325306
isServer: isServer,
326307
br: br,
327308
conn: conn,
328309
mu: mu,
329310
readFinal: true,
330311
writeBuf: writeBuf,
312+
writePool: writeBufferPool,
313+
writeBufSize: writeBufferSize,
331314
enableWriteCompression: true,
332315
compressionLevel: defaultCompressionLevel,
333316
}
@@ -484,7 +467,19 @@ func (c *Conn) prepWrite(messageType int) error {
484467
c.writeErrMu.Lock()
485468
err := c.writeErr
486469
c.writeErrMu.Unlock()
487-
return err
470+
if err != nil {
471+
return err
472+
}
473+
474+
if c.writeBuf == nil {
475+
wpd, ok := c.writePool.Get().(writePoolData)
476+
if ok {
477+
c.writeBuf = wpd.buf
478+
} else {
479+
c.writeBuf = make([]byte, c.writeBufSize)
480+
}
481+
}
482+
return nil
488483
}
489484

490485
// NextWriter returns a writer for the next message to send. The writer's Close
@@ -610,6 +605,10 @@ func (w *messageWriter) flushFrame(final bool, extra []byte) error {
610605

611606
if final {
612607
c.writer = nil
608+
if c.writePool != nil {
609+
c.writePool.Put(writePoolData{buf: c.writeBuf})
610+
c.writeBuf = nil
611+
}
613612
return nil
614613
}
615614

conn_broadcast_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ func (b *broadcastBench) makeConns(numConns int) {
7070
conns := make([]*broadcastConn, numConns)
7171

7272
for i := 0; i < numConns; i++ {
73-
c := newConn(fakeNetConn{Reader: nil, Writer: b.w}, true, 1024, 1024)
73+
c := newTestConn(nil, b.w, true)
7474
if b.compression {
7575
c.enableWriteCompression = true
7676
c.newCompressionWriter = compressNoContextTakeover

0 commit comments

Comments
 (0)