Skip to content

Commit 9e4b7cd

Browse files
committed
conn: avoid frame timeout and cleanup overhead
#565 Every frame read registered a context.AfterFunc callback, even when the context could not be canceled, and returned a cleanup closure that forced the caller's error and captured state onto the heap. Skip timeout setup for contexts with a nil Done channel and move read cleanup into a direct method call, while tracking whether a callback was installed so cancelable operations retain the same close-on-cancellation behavior. Writes use the same background-context fast path, and BenchmarkConn now joins its writer goroutine so repeated runs finish cleanly. On an Apple M4 Max with GOMAXPROCS=1, a 10-sample in-package benchmark against d099e16 reduced background header reads from 168.36 to 22.85 ns/op and 512-byte payload reads from 175.04 to 26.65 ns/op. Both fell from 216 B/op and 6 allocs/op to zero. Cancelable header reads improved from 203.40 to 160.50 ns/op and payload reads from 215.18 to 162.52 ns/op, with both falling to 152 B/op and 4 allocs/op. Comparing against the closure-only change confirms that skipping the dead timeout registration accounts for exactly 152 bytes and 4 allocations on background reads without changing cancelable allocations. BenchmarkConn/disabledCompress averaged 3.968 us/op, 1539 B/op, and 42 allocs/op before the change and 3.817 us/op, 1219 B/op, and 32 allocs/op after it. This is a 3.8 percent time reduction, 20.8 percent fewer bytes, and 23.8 percent fewer allocations per operation.
1 parent d099e16 commit 9e4b7cd

4 files changed

Lines changed: 55 additions & 35 deletions

File tree

conn.go

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -168,24 +168,34 @@ func (c *Conn) close() error {
168168
return err
169169
}
170170

171-
func (c *Conn) setupWriteTimeout(ctx context.Context) {
171+
func (c *Conn) setupWriteTimeout(ctx context.Context) bool {
172+
if ctx.Done() == nil {
173+
return false
174+
}
175+
172176
stop := context.AfterFunc(ctx, func() {
173177
c.clearWriteTimeout()
174178
c.close()
175179
})
176180
swapTimeoutStop(&c.writeTimeoutStop, &stop)
181+
return true
177182
}
178183

179184
func (c *Conn) clearWriteTimeout() {
180185
swapTimeoutStop(&c.writeTimeoutStop, nil)
181186
}
182187

183-
func (c *Conn) setupReadTimeout(ctx context.Context) {
188+
func (c *Conn) setupReadTimeout(ctx context.Context) bool {
189+
if ctx.Done() == nil {
190+
return false
191+
}
192+
184193
stop := context.AfterFunc(ctx, func() {
185194
c.clearReadTimeout()
186195
c.close()
187196
})
188197
swapTimeoutStop(&c.readTimeoutStop, &stop)
198+
return true
189199
}
190200

191201
func (c *Conn) clearReadTimeout() {

conn_test.go

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -592,10 +592,11 @@ func BenchmarkConn(b *testing.B) {
592592
msg := []byte(strings.Repeat("1234", 128))
593593
readBuf := make([]byte, len(msg))
594594
writes := make(chan struct{})
595-
defer close(writes)
596595
werrs := make(chan error)
596+
writerDone := make(chan struct{})
597597

598598
go func() {
599+
defer close(writerDone)
599600
for range writes {
600601
select {
601602
case werrs <- c1.Write(bb.ctx, websocket.MessageText, msg):
@@ -650,6 +651,13 @@ func BenchmarkConn(b *testing.B) {
650651
}
651652
b.StopTimer()
652653

654+
close(writes)
655+
select {
656+
case <-writerDone:
657+
case <-bb.ctx.Done():
658+
b.Fatal(bb.ctx.Err())
659+
}
660+
653661
b.ReportMetric(float64(*bytesWritten/b.N), "written/op")
654662
b.ReportMetric(float64(*bytesRead/b.N), "read/op")
655663

read.go

Lines changed: 31 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -217,51 +217,52 @@ func (c *Conn) readLoop(ctx context.Context) (header, error) {
217217
}
218218
}
219219

220-
// prepareRead sets the readTimeout context and returns a done function
221-
// to be called after the read is done. It also returns an error if the
222-
// connection is closed. The reference to the error is used to assign
223-
// an error depending on if the connection closed or the context timed
224-
// out during use. Typically, the referenced error is a named return
225-
// variable of the function calling this method.
226-
func (c *Conn) prepareRead(ctx context.Context, err *error) (func(), error) {
220+
// prepareRead sets the read timeout and checks whether the connection is closed.
221+
func (c *Conn) prepareRead(ctx context.Context) (bool, error) {
227222
select {
228223
case <-c.closed:
229-
return nil, net.ErrClosed
224+
return false, net.ErrClosed
230225
default:
231226
}
232-
c.setupReadTimeout(ctx)
233-
234-
done := func() {
235-
c.clearReadTimeout()
236-
select {
237-
case <-c.closed:
238-
if *err != nil {
239-
*err = net.ErrClosed
240-
}
241-
default:
242-
}
243-
if *err != nil && ctx.Err() != nil {
244-
*err = ctx.Err()
245-
}
246-
}
227+
timeoutSet := c.setupReadTimeout(ctx)
247228

248229
c.closeStateMu.Lock()
249230
closeReceivedErr := c.closeReceivedErr
250231
c.closeStateMu.Unlock()
251232
if closeReceivedErr != nil {
252-
defer done()
253-
return nil, closeReceivedErr
233+
if timeoutSet {
234+
c.clearReadTimeout()
235+
}
236+
return false, closeReceivedErr
254237
}
255238

256-
return done, nil
239+
return timeoutSet, nil
240+
}
241+
242+
// finishRead clears the read timeout and reports whether the connection or
243+
// operation context ended while the read was in progress.
244+
func (c *Conn) finishRead(ctx context.Context, err *error, timeoutSet bool) {
245+
if timeoutSet {
246+
c.clearReadTimeout()
247+
}
248+
select {
249+
case <-c.closed:
250+
if *err != nil {
251+
*err = net.ErrClosed
252+
}
253+
default:
254+
}
255+
if *err != nil && ctx.Err() != nil {
256+
*err = ctx.Err()
257+
}
257258
}
258259

259260
func (c *Conn) readFrameHeader(ctx context.Context) (_ header, err error) {
260-
readDone, err := c.prepareRead(ctx, &err)
261+
timeoutSet, err := c.prepareRead(ctx)
261262
if err != nil {
262263
return header{}, err
263264
}
264-
defer readDone()
265+
defer c.finishRead(ctx, &err, timeoutSet)
265266

266267
h, err := readFrameHeader(c.br, c.readHeaderBuf[:])
267268
if err != nil {
@@ -272,11 +273,11 @@ func (c *Conn) readFrameHeader(ctx context.Context) (_ header, err error) {
272273
}
273274

274275
func (c *Conn) readFramePayload(ctx context.Context, p []byte) (_ int, err error) {
275-
readDone, err := c.prepareRead(ctx, &err)
276+
timeoutSet, err := c.prepareRead(ctx)
276277
if err != nil {
277278
return 0, err
278279
}
279-
defer readDone()
280+
defer c.finishRead(ctx, &err, timeoutSet)
280281

281282
n, err := io.ReadFull(c.br, p)
282283
if err != nil {

write.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -318,8 +318,9 @@ func (c *Conn) writeFrame(ctx context.Context, fin bool, flate bool, opcode opco
318318
return 0, net.ErrClosed
319319
default:
320320
}
321-
c.setupWriteTimeout(ctx)
322-
defer c.clearWriteTimeout()
321+
if c.setupWriteTimeout(ctx) {
322+
defer c.clearWriteTimeout()
323+
}
323324

324325
c.writeHeader.fin = fin
325326
c.writeHeader.opcode = opcode

0 commit comments

Comments
 (0)