Browse Source

Merge pull request #1231 from slingamn/buffer.2

more memory-efficient implementation of line reading
tags/v2.5.0-rc1
Shivaram Lingamneni 3 years ago
parent
commit
db100f1f91
No account linked to committer's email address
7 changed files with 231 additions and 45 deletions
  1. 1
    13
      irc/history/history.go
  2. 0
    11
      irc/history/history_test.go
  3. 60
    17
      irc/ircconn.go
  4. 135
    0
      irc/ircconn_test.go
  5. 0
    4
      irc/socket.go
  6. 16
    0
      irc/utils/math.go
  7. 19
    0
      irc/utils/math_test.go

+ 1
- 13
irc/history/history.go View File

@@ -322,18 +322,6 @@ func (list *Buffer) next(index int) int {
322 322
 	}
323 323
 }
324 324
 
325
-// return n such that v <= n and n == 2**i for some i
326
-func roundUpToPowerOfTwo(v int) int {
327
-	// http://graphics.stanford.edu/~seander/bithacks.html
328
-	v -= 1
329
-	v |= v >> 1
330
-	v |= v >> 2
331
-	v |= v >> 4
332
-	v |= v >> 8
333
-	v |= v >> 16
334
-	return v + 1
335
-}
336
-
337 325
 func (list *Buffer) maybeExpand() {
338 326
 	if list.window == 0 {
339 327
 		return // autoresize is disabled
@@ -353,7 +341,7 @@ func (list *Buffer) maybeExpand() {
353 341
 		return // oldest element is old enough to overwrite
354 342
 	}
355 343
 
356
-	newSize := roundUpToPowerOfTwo(length + 1)
344
+	newSize := utils.RoundUpToPowerOfTwo(length + 1)
357 345
 	if list.maximumSize < newSize {
358 346
 		newSize = list.maximumSize
359 347
 	}

+ 0
- 11
irc/history/history_test.go View File

@@ -241,17 +241,6 @@ func TestDisabledByResize(t *testing.T) {
241 241
 	assertEqual(len(items), 0, t)
242 242
 }
243 243
 
244
-func TestRoundUp(t *testing.T) {
245
-	assertEqual(roundUpToPowerOfTwo(2), 2, t)
246
-	assertEqual(roundUpToPowerOfTwo(3), 4, t)
247
-	assertEqual(roundUpToPowerOfTwo(64), 64, t)
248
-	assertEqual(roundUpToPowerOfTwo(65), 128, t)
249
-	assertEqual(roundUpToPowerOfTwo(100), 128, t)
250
-	assertEqual(roundUpToPowerOfTwo(1000), 1024, t)
251
-	assertEqual(roundUpToPowerOfTwo(1025), 2048, t)
252
-	assertEqual(roundUpToPowerOfTwo(269435457), 536870912, t)
253
-}
254
-
255 244
 func BenchmarkInsert(b *testing.B) {
256 245
 	buf := NewHistoryBuffer(1024, 0)
257 246
 	b.ResetTimer()

+ 60
- 17
irc/ircconn.go View File

@@ -1,9 +1,9 @@
1 1
 package irc
2 2
 
3 3
 import (
4
-	"bufio"
5 4
 	"bytes"
6 5
 	"errors"
6
+	"io"
7 7
 	"net"
8 8
 	"unicode/utf8"
9 9
 
@@ -14,7 +14,8 @@ import (
14 14
 )
15 15
 
16 16
 const (
17
-	maxReadQBytes = ircmsg.MaxlenTagsFromClient + MaxLineLen + 1024
17
+	maxReadQBytes     = ircmsg.MaxlenTagsFromClient + MaxLineLen + 1024
18
+	initialBufferSize = 1024
18 19
 )
19 20
 
20 21
 var (
@@ -41,8 +42,13 @@ type IRCConn interface {
41 42
 
42 43
 // IRCStreamConn is an IRCConn over a regular stream connection.
43 44
 type IRCStreamConn struct {
44
-	conn   *utils.WrappedConn
45
-	reader *bufio.Reader
45
+	conn *utils.WrappedConn
46
+
47
+	buf        []byte
48
+	start      int // start of valid (i.e., read but not yet consumed) data in the buffer
49
+	end        int // end of valid data in the buffer
50
+	searchFrom int // start of valid data in the buffer not yet searched for \n
51
+	eof        bool
46 52
 }
47 53
 
48 54
 func NewIRCStreamConn(conn *utils.WrappedConn) *IRCStreamConn {
@@ -67,21 +73,58 @@ func (cc *IRCStreamConn) WriteLines(buffers [][]byte) (err error) {
67 73
 	return
68 74
 }
69 75
 
70
-func (cc *IRCStreamConn) ReadLine() (line []byte, err error) {
71
-	// lazy initialize the reader in case the IP is banned
72
-	if cc.reader == nil {
73
-		cc.reader = bufio.NewReaderSize(cc.conn, maxReadQBytes)
74
-	}
76
+func (cc *IRCStreamConn) ReadLine() ([]byte, error) {
77
+	for {
78
+		// try to find a terminated line in the buffered data already read
79
+		nlidx := bytes.IndexByte(cc.buf[cc.searchFrom:cc.end], '\n')
80
+		if nlidx != -1 {
81
+			// got a complete line
82
+			line := cc.buf[cc.start : cc.searchFrom+nlidx]
83
+			cc.start = cc.searchFrom + nlidx + 1
84
+			cc.searchFrom = cc.start
85
+			if globalUtf8EnforcementSetting && !utf8.Valid(line) {
86
+				return line, errInvalidUtf8
87
+			} else {
88
+				return line, nil
89
+			}
90
+		}
75 91
 
76
-	var isPrefix bool
77
-	line, isPrefix, err = cc.reader.ReadLine()
78
-	if isPrefix {
79
-		return nil, errReadQ
80
-	}
81
-	if globalUtf8EnforcementSetting && !utf8.Valid(line) {
82
-		err = errInvalidUtf8
92
+		if cc.start == 0 && len(cc.buf) == maxReadQBytes {
93
+			return nil, errReadQ // out of space, can't expand or slide
94
+		}
95
+
96
+		if cc.eof {
97
+			return nil, io.EOF
98
+		}
99
+
100
+		if len(cc.buf) < maxReadQBytes && (len(cc.buf)-(cc.end-cc.start) < initialBufferSize/2) {
101
+			// allocate a new buffer, copy any remaining data
102
+			newLen := utils.RoundUpToPowerOfTwo(len(cc.buf) + 1)
103
+			if newLen > maxReadQBytes {
104
+				newLen = maxReadQBytes
105
+			} else if newLen < initialBufferSize {
106
+				newLen = initialBufferSize
107
+			}
108
+			newBuf := make([]byte, newLen)
109
+			copy(newBuf, cc.buf[cc.start:cc.end])
110
+			cc.buf = newBuf
111
+		} else if cc.start != 0 {
112
+			// slide remaining data back to the front of the buffer
113
+			copy(cc.buf, cc.buf[cc.start:cc.end])
114
+		}
115
+		cc.end = cc.end - cc.start
116
+		cc.start = 0
117
+
118
+		cc.searchFrom = cc.end
119
+		n, err := cc.conn.Read(cc.buf[cc.end:])
120
+		cc.end += n
121
+		if n != 0 && err == io.EOF {
122
+			// we may have received new \n-terminated lines, try to parse them
123
+			cc.eof = true
124
+		} else if err != nil {
125
+			return nil, err
126
+		}
83 127
 	}
84
-	return
85 128
 }
86 129
 
87 130
 func (cc *IRCStreamConn) Close() (err error) {

+ 135
- 0
irc/ircconn_test.go View File

@@ -0,0 +1,135 @@
1
+// Copyright (c) 2020 Shivaram Lingamneni <slingamn@cs.stanford.edu>
2
+// released under the MIT license
3
+
4
+package irc
5
+
6
+import (
7
+	"io"
8
+	"math/rand"
9
+	"net"
10
+	"reflect"
11
+	"testing"
12
+	"time"
13
+
14
+	"github.com/oragono/oragono/irc/utils"
15
+)
16
+
17
+// mockConn is a fake net.Conn / io.Reader that yields len(counts) lines,
18
+// each consisting of counts[i] 'a' characters and a terminating '\n'
19
+type mockConn struct {
20
+	counts []int
21
+}
22
+
23
+func min(i, j int) (m int) {
24
+	if i < j {
25
+		return i
26
+	} else {
27
+		return j
28
+	}
29
+}
30
+
31
+func (c *mockConn) Read(b []byte) (n int, err error) {
32
+	for len(b) > 0 {
33
+		if len(c.counts) == 0 {
34
+			return n, io.EOF
35
+		}
36
+		if c.counts[0] == 0 {
37
+			b[0] = '\n'
38
+			c.counts = c.counts[1:]
39
+			b = b[1:]
40
+			n += 1
41
+			continue
42
+		}
43
+		size := min(c.counts[0], len(b))
44
+		for i := 0; i < size; i++ {
45
+			b[i] = 'a'
46
+		}
47
+		c.counts[0] -= size
48
+		b = b[size:]
49
+		n += size
50
+	}
51
+	return n, nil
52
+}
53
+
54
+func (c *mockConn) Write(b []byte) (n int, err error) {
55
+	return
56
+}
57
+
58
+func (c *mockConn) Close() error {
59
+	c.counts = nil
60
+	return nil
61
+}
62
+
63
+func (c *mockConn) LocalAddr() net.Addr {
64
+	return nil
65
+}
66
+
67
+func (c *mockConn) RemoteAddr() net.Addr {
68
+	return nil
69
+}
70
+
71
+func (c *mockConn) SetDeadline(t time.Time) error {
72
+	return nil
73
+}
74
+
75
+func (c *mockConn) SetReadDeadline(t time.Time) error {
76
+	return nil
77
+}
78
+
79
+func (c *mockConn) SetWriteDeadline(t time.Time) error {
80
+	return nil
81
+}
82
+
83
+func newMockConn(counts []int) *utils.WrappedConn {
84
+	cpCounts := make([]int, len(counts))
85
+	copy(cpCounts, counts)
86
+	c := &mockConn{
87
+		counts: cpCounts,
88
+	}
89
+	return &utils.WrappedConn{
90
+		Conn: c,
91
+	}
92
+}
93
+
94
+// construct a mock reader with some number of \n-terminated lines,
95
+// verify that IRCStreamConn can read and split them as expected
96
+func doLineReaderTest(counts []int, t *testing.T) {
97
+	c := newMockConn(counts)
98
+	r := NewIRCStreamConn(c)
99
+	var readCounts []int
100
+	for {
101
+		line, err := r.ReadLine()
102
+		if err == nil {
103
+			readCounts = append(readCounts, len(line))
104
+		} else if err == io.EOF {
105
+			break
106
+		} else {
107
+			panic(err)
108
+		}
109
+	}
110
+
111
+	if !reflect.DeepEqual(counts, readCounts) {
112
+		t.Errorf("expected %#v, got %#v", counts, readCounts)
113
+	}
114
+}
115
+
116
+const (
117
+	maxMockReaderLen     = 100
118
+	maxMockReaderLineLen = 4096 + 511
119
+)
120
+
121
+func TestLineReader(t *testing.T) {
122
+	counts := []int{44, 428, 3, 0, 200, 2000, 0, 4044, 33, 3, 2, 1, 0, 1, 2, 3, 48, 555}
123
+	doLineReaderTest(counts, t)
124
+
125
+	// fuzz
126
+	r := rand.New(rand.NewSource(time.Now().UnixNano()))
127
+	for i := 0; i < 1000; i++ {
128
+		countsLen := r.Intn(maxMockReaderLen) + 1
129
+		counts := make([]int, countsLen)
130
+		for i := 0; i < countsLen; i++ {
131
+			counts[i] = r.Intn(maxMockReaderLineLen)
132
+		}
133
+		doLineReaderTest(counts, t)
134
+	}
135
+}

+ 0
- 4
irc/socket.go View File

@@ -69,10 +69,6 @@ func (socket *Socket) Read() (string, error) {
69 69
 
70 70
 	if err == io.EOF {
71 71
 		socket.Close()
72
-		// process last message properly (such as ERROR/QUIT/etc), just fail next reads/writes
73
-		if line != "" {
74
-			err = nil
75
-		}
76 72
 	}
77 73
 
78 74
 	return line, err

+ 16
- 0
irc/utils/math.go View File

@@ -0,0 +1,16 @@
1
+// Copyright (c) 2020 Shivaram Lingamneni <slingamn@cs.stanford.edu>
2
+// released under the MIT license
3
+
4
+package utils
5
+
6
+// return n such that v <= n and n == 2**i for some i
7
+func RoundUpToPowerOfTwo(v int) int {
8
+	// http://graphics.stanford.edu/~seander/bithacks.html
9
+	v -= 1
10
+	v |= v >> 1
11
+	v |= v >> 2
12
+	v |= v >> 4
13
+	v |= v >> 8
14
+	v |= v >> 16
15
+	return v + 1
16
+}

+ 19
- 0
irc/utils/math_test.go View File

@@ -0,0 +1,19 @@
1
+// Copyright (c) 2020 Shivaram Lingamneni <slingamn@cs.stanford.edu>
2
+// released under the MIT license
3
+
4
+package utils
5
+
6
+import (
7
+	"testing"
8
+)
9
+
10
+func TestRoundUp(t *testing.T) {
11
+	assertEqual(RoundUpToPowerOfTwo(2), 2, t)
12
+	assertEqual(RoundUpToPowerOfTwo(3), 4, t)
13
+	assertEqual(RoundUpToPowerOfTwo(64), 64, t)
14
+	assertEqual(RoundUpToPowerOfTwo(65), 128, t)
15
+	assertEqual(RoundUpToPowerOfTwo(100), 128, t)
16
+	assertEqual(RoundUpToPowerOfTwo(1000), 1024, t)
17
+	assertEqual(RoundUpToPowerOfTwo(1025), 2048, t)
18
+	assertEqual(RoundUpToPowerOfTwo(269435457), 536870912, t)
19
+}

Loading…
Cancel
Save