Browse Source

Merge pull request #591 from slingamn/history_autoresize.4

autoresizing of history buffers (#349)
tags/v1.2.0-rc1
Daniel Oaks 4 years ago
parent
commit
7a56c4e0ad
No account linked to committer's email address
7 changed files with 190 additions and 31 deletions
  1. 1
    1
      irc/channel.go
  2. 1
    1
      irc/client.go
  3. 5
    4
      irc/config.go
  4. 85
    9
      irc/history/history.go
  5. 83
    5
      irc/history/history_test.go
  6. 5
    9
      irc/server.go
  7. 10
    2
      oragono.yaml

+ 1
- 1
irc/channel.go View File

@@ -76,7 +76,7 @@ func NewChannel(s *Server, name string, registered bool) *Channel {
76 76
 	config := s.Config()
77 77
 
78 78
 	channel.writerSemaphore.Initialize(1)
79
-	channel.history.Initialize(config.History.ChannelLength)
79
+	channel.history.Initialize(config.History.ChannelLength, config.History.AutoresizeWindow)
80 80
 
81 81
 	if !registered {
82 82
 		for _, mode := range config.Channels.defaultModes {

+ 1
- 1
irc/client.go View File

@@ -232,7 +232,7 @@ func (server *Server) RunClient(conn clientConn) {
232 232
 		nickCasefolded: "*",
233 233
 		nickMaskString: "*", // * is used until actual nick is given
234 234
 	}
235
-	client.history.Initialize(config.History.ClientLength)
235
+	client.history.Initialize(config.History.ClientLength, config.History.AutoresizeWindow)
236 236
 	client.brbTimer.Initialize(client)
237 237
 	session := &Session{
238 238
 		client:     client,

+ 5
- 4
irc/config.go View File

@@ -353,10 +353,11 @@ type Config struct {
353 353
 
354 354
 	History struct {
355 355
 		Enabled          bool
356
-		ChannelLength    int `yaml:"channel-length"`
357
-		ClientLength     int `yaml:"client-length"`
358
-		AutoreplayOnJoin int `yaml:"autoreplay-on-join"`
359
-		ChathistoryMax   int `yaml:"chathistory-maxmessages"`
356
+		ChannelLength    int           `yaml:"channel-length"`
357
+		ClientLength     int           `yaml:"client-length"`
358
+		AutoresizeWindow time.Duration `yaml:"autoresize-window"`
359
+		AutoreplayOnJoin int           `yaml:"autoreplay-on-join"`
360
+		ChathistoryMax   int           `yaml:"chathistory-maxmessages"`
360 361
 	}
361 362
 
362 363
 	Filename string

+ 85
- 9
irc/history/history.go View File

@@ -25,6 +25,10 @@ const (
25 25
 	Nick
26 26
 )
27 27
 
28
+const (
29
+	initialAutoSize = 32
30
+)
31
+
28 32
 // a Tagmsg that consists entirely of transient tags is not stored
29 33
 var transientTags = map[string]bool{
30 34
 	"+draft/typing": true,
@@ -77,25 +81,39 @@ type Buffer struct {
77 81
 	sync.RWMutex
78 82
 
79 83
 	// ring buffer, see irc/whowas.go for conventions
80
-	buffer []Item
81
-	start  int
82
-	end    int
84
+	buffer      []Item
85
+	start       int
86
+	end         int
87
+	maximumSize int
88
+	window      time.Duration
83 89
 
84 90
 	lastDiscarded time.Time
85 91
 
86 92
 	enabled uint32
93
+
94
+	nowFunc func() time.Time
87 95
 }
88 96
 
89
-func NewHistoryBuffer(size int) (result *Buffer) {
97
+func NewHistoryBuffer(size int, window time.Duration) (result *Buffer) {
90 98
 	result = new(Buffer)
91
-	result.Initialize(size)
99
+	result.Initialize(size, window)
92 100
 	return
93 101
 }
94 102
 
95
-func (hist *Buffer) Initialize(size int) {
96
-	hist.buffer = make([]Item, size)
103
+func (hist *Buffer) Initialize(size int, window time.Duration) {
104
+	initialSize := size
105
+	if window != 0 {
106
+		initialSize = initialAutoSize
107
+		if size < initialSize {
108
+			initialSize = size // min(initialAutoSize, size)
109
+		}
110
+	}
111
+	hist.buffer = make([]Item, initialSize)
97 112
 	hist.start = -1
98 113
 	hist.end = -1
114
+	hist.window = window
115
+	hist.maximumSize = size
116
+	hist.nowFunc = time.Now
99 117
 
100 118
 	hist.setEnabled(size)
101 119
 }
@@ -132,6 +150,8 @@ func (list *Buffer) Add(item Item) {
132 150
 	list.Lock()
133 151
 	defer list.Unlock()
134 152
 
153
+	list.maybeExpand()
154
+
135 155
 	var pos int
136 156
 	if list.start == -1 { // empty
137 157
 		pos = 0
@@ -269,12 +289,68 @@ func (list *Buffer) next(index int) int {
269 289
 	}
270 290
 }
271 291
 
292
+// return n such that v <= n and n == 2**i for some i
293
+func roundUpToPowerOfTwo(v int) int {
294
+	// http://graphics.stanford.edu/~seander/bithacks.html
295
+	v -= 1
296
+	v |= v >> 1
297
+	v |= v >> 2
298
+	v |= v >> 4
299
+	v |= v >> 8
300
+	v |= v >> 16
301
+	return v + 1
302
+}
303
+
304
+func (list *Buffer) maybeExpand() {
305
+	if list.window == 0 {
306
+		return // autoresize is disabled
307
+	}
308
+
309
+	length := list.length()
310
+	if length < len(list.buffer) {
311
+		return // we have spare capacity already
312
+	}
313
+
314
+	if len(list.buffer) == list.maximumSize {
315
+		return // cannot expand any further
316
+	}
317
+
318
+	wouldDiscard := list.buffer[list.start].Message.Time
319
+	if list.window < list.nowFunc().Sub(wouldDiscard) {
320
+		return // oldest element is old enough to overwrite
321
+	}
322
+
323
+	newSize := roundUpToPowerOfTwo(length + 1)
324
+	if list.maximumSize < newSize {
325
+		newSize = list.maximumSize
326
+	}
327
+	list.resize(newSize)
328
+}
329
+
272 330
 // Resize shrinks or expands the buffer
273
-func (list *Buffer) Resize(size int) {
274
-	newbuffer := make([]Item, size)
331
+func (list *Buffer) Resize(maximumSize int, window time.Duration) {
275 332
 	list.Lock()
276 333
 	defer list.Unlock()
277 334
 
335
+	if list.maximumSize == maximumSize && list.window == window {
336
+		return // no-op
337
+	}
338
+
339
+	list.maximumSize = maximumSize
340
+	list.window = window
341
+
342
+	// if we're not autoresizing, we need to resize now;
343
+	// if we are autoresizing, we may need to shrink the buffer down to maximumSize,
344
+	// but we don't need to grow it now (we can just grow it on the next Add)
345
+	// TODO make it possible to shrink the buffer so that it only contains `window`
346
+	if window == 0 || maximumSize < len(list.buffer) {
347
+		list.resize(maximumSize)
348
+	}
349
+}
350
+
351
+func (list *Buffer) resize(size int) {
352
+	newbuffer := make([]Item, size)
353
+
278 354
 	list.setEnabled(size)
279 355
 
280 356
 	if list.start == -1 {

+ 83
- 5
irc/history/history_test.go View File

@@ -5,6 +5,7 @@ package history
5 5
 
6 6
 import (
7 7
 	"reflect"
8
+	"strconv"
8 9
 	"testing"
9 10
 	"time"
10 11
 )
@@ -16,7 +17,7 @@ const (
16 17
 func TestEmptyBuffer(t *testing.T) {
17 18
 	pastTime := easyParse(timeFormat)
18 19
 
19
-	buf := NewHistoryBuffer(0)
20
+	buf := NewHistoryBuffer(0, 0)
20 21
 	if buf.Enabled() {
21 22
 		t.Error("the buffer of size 0 must be considered disabled")
22 23
 	}
@@ -33,7 +34,7 @@ func TestEmptyBuffer(t *testing.T) {
33 34
 		t.Error("the empty/disabled buffer should report results as incomplete")
34 35
 	}
35 36
 
36
-	buf.Resize(1)
37
+	buf.Resize(1, 0)
37 38
 	if !buf.Enabled() {
38 39
 		t.Error("the buffer of size 1 must be considered enabled")
39 40
 	}
@@ -102,7 +103,7 @@ func assertEqual(supplied, expected interface{}, t *testing.T) {
102 103
 func TestBuffer(t *testing.T) {
103 104
 	start := easyParse("2006-01-01 00:00:00Z")
104 105
 
105
-	buf := NewHistoryBuffer(3)
106
+	buf := NewHistoryBuffer(3, 0)
106 107
 	buf.Add(easyItem("testnick0", "2006-01-01 15:04:05Z"))
107 108
 
108 109
 	buf.Add(easyItem("testnick1", "2006-01-02 15:04:05Z"))
@@ -128,12 +129,12 @@ func TestBuffer(t *testing.T) {
128 129
 	assertEqual(toNicks(since), []string{"testnick1"}, t)
129 130
 
130 131
 	// shrink the buffer, cutting off testnick1
131
-	buf.Resize(2)
132
+	buf.Resize(2, 0)
132 133
 	since, complete = buf.Between(easyParse("2006-01-02 00:00:00Z"), time.Now(), false, 0)
133 134
 	assertEqual(complete, false, t)
134 135
 	assertEqual(toNicks(since), []string{"testnick2", "testnick3"}, t)
135 136
 
136
-	buf.Resize(5)
137
+	buf.Resize(5, 0)
137 138
 	buf.Add(easyItem("testnick4", "2006-01-05 15:04:05Z"))
138 139
 	buf.Add(easyItem("testnick5", "2006-01-06 15:04:05Z"))
139 140
 	buf.Add(easyItem("testnick6", "2006-01-07 15:04:05Z"))
@@ -145,3 +146,80 @@ func TestBuffer(t *testing.T) {
145 146
 	since, _ = buf.Between(easyParse("2006-01-03 00:00:00Z"), time.Now(), true, 2)
146 147
 	assertEqual(toNicks(since), []string{"testnick2", "testnick3"}, t)
147 148
 }
149
+
150
+func autoItem(id int, t time.Time) (result Item) {
151
+	result.Message.Time = t
152
+	result.Nick = strconv.Itoa(id)
153
+	return
154
+}
155
+
156
+func atoi(s string) int {
157
+	result, err := strconv.Atoi(s)
158
+	if err != nil {
159
+		panic(err)
160
+	}
161
+	return result
162
+}
163
+
164
+func TestAutoresize(t *testing.T) {
165
+	now := easyParse("2006-01-01 00:00:00Z")
166
+	nowFunc := func() time.Time {
167
+		return now
168
+	}
169
+
170
+	buf := NewHistoryBuffer(128, time.Hour)
171
+	buf.nowFunc = nowFunc
172
+
173
+	// add items slowly (one every 10 minutes): the buffer should not expand
174
+	// beyond initialAutoSize
175
+	id := 0
176
+	for i := 0; i < 72; i += 1 {
177
+		buf.Add(autoItem(id, now))
178
+		if initialAutoSize < buf.length() {
179
+			t.Errorf("buffer incorrectly resized above %d to %d", initialAutoSize, buf.length())
180
+		}
181
+		now = now.Add(time.Minute * 10)
182
+		id += 1
183
+	}
184
+	items := buf.Latest(0)
185
+	assertEqual(len(items), initialAutoSize, t)
186
+	assertEqual(atoi(items[0].Nick), 40, t)
187
+	assertEqual(atoi(items[len(items)-1].Nick), 71, t)
188
+
189
+	// dump 100 items in very fast:
190
+	for i := 0; i < 100; i += 1 {
191
+		buf.Add(autoItem(id, now))
192
+		now = now.Add(time.Second)
193
+		id += 1
194
+	}
195
+	// ok, 5 items from the first batch are still in the 1-hour window;
196
+	// we should overwrite until only those 5 are left, then start expanding
197
+	// the buffer so that it retains those 5 and the 100 new items
198
+	items = buf.Latest(0)
199
+	assertEqual(len(items), 105, t)
200
+	assertEqual(atoi(items[0].Nick), 67, t)
201
+	assertEqual(atoi(items[len(items)-1].Nick), 171, t)
202
+
203
+	// another 100 items very fast:
204
+	for i := 0; i < 100; i += 1 {
205
+		buf.Add(autoItem(id, now))
206
+		now = now.Add(time.Second)
207
+		id += 1
208
+	}
209
+	// should fill up to the maximum size of 128 and start overwriting
210
+	items = buf.Latest(0)
211
+	assertEqual(len(items), 128, t)
212
+	assertEqual(atoi(items[0].Nick), 144, t)
213
+	assertEqual(atoi(items[len(items)-1].Nick), 271, t)
214
+}
215
+
216
+func TestRoundUp(t *testing.T) {
217
+	assertEqual(roundUpToPowerOfTwo(2), 2, t)
218
+	assertEqual(roundUpToPowerOfTwo(3), 4, t)
219
+	assertEqual(roundUpToPowerOfTwo(64), 64, t)
220
+	assertEqual(roundUpToPowerOfTwo(65), 128, t)
221
+	assertEqual(roundUpToPowerOfTwo(100), 128, t)
222
+	assertEqual(roundUpToPowerOfTwo(1000), 1024, t)
223
+	assertEqual(roundUpToPowerOfTwo(1025), 2048, t)
224
+	assertEqual(roundUpToPowerOfTwo(269435457), 536870912, t)
225
+}

+ 5
- 9
irc/server.go View File

@@ -697,16 +697,12 @@ func (server *Server) applyConfig(config *Config, initial bool) (err error) {
697 697
 	}
698 698
 
699 699
 	// resize history buffers as needed
700
-	if oldConfig != nil {
701
-		if oldConfig.History.ChannelLength != config.History.ChannelLength {
702
-			for _, channel := range server.channels.Channels() {
703
-				channel.history.Resize(config.History.ChannelLength)
704
-			}
700
+	if oldConfig != nil && oldConfig.History != config.History {
701
+		for _, channel := range server.channels.Channels() {
702
+			channel.history.Resize(config.History.ChannelLength, config.History.AutoresizeWindow)
705 703
 		}
706
-		if oldConfig.History.ClientLength != config.History.ClientLength {
707
-			for _, client := range server.clients.AllClients() {
708
-				client.history.Resize(config.History.ClientLength)
709
-			}
704
+		for _, client := range server.clients.AllClients() {
705
+			client.history.Resize(config.History.ClientLength, config.History.AutoresizeWindow)
710 706
 		}
711 707
 	}
712 708
 

+ 10
- 2
oragono.yaml View File

@@ -593,10 +593,18 @@ history:
593 593
     enabled: false
594 594
 
595 595
     # how many channel-specific events (messages, joins, parts) should be tracked per channel?
596
-    channel-length: 256
596
+    channel-length: 1024
597 597
 
598 598
     # how many direct messages and notices should be tracked per user?
599
-    client-length: 64
599
+    client-length: 256
600
+
601
+    # how long should we try to preserve messages?
602
+    # if `autoresize-window` is 0, the in-memory message buffers are preallocated to
603
+    # their maximum length. if it is nonzero, the buffers are initially small and
604
+    # are dynamically expanded up to the maximum length. if the buffer is full
605
+    # and the oldest message is older than `autoresize-window`, then it will overwrite
606
+    # the oldest message rather than resize; otherwise, it will expand if possible.
607
+    autoresize-window: 1h
600 608
 
601 609
     # number of messages to automatically play back on channel join (0 to disable):
602 610
     autoreplay-on-join: 0

Loading…
Cancel
Save