Parcourir la source

Merge pull request #591 from slingamn/history_autoresize.4

autoresizing of history buffers (#349)
tags/v1.2.0-rc1
Daniel Oaks il y a 4 ans
Parent
révision
7a56c4e0ad
Aucun compte lié à l'adresse e-mail de l'auteur
7 fichiers modifiés avec 190 ajouts et 31 suppressions
  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 Voir le fichier

76
 	config := s.Config()
76
 	config := s.Config()
77
 
77
 
78
 	channel.writerSemaphore.Initialize(1)
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
 	if !registered {
81
 	if !registered {
82
 		for _, mode := range config.Channels.defaultModes {
82
 		for _, mode := range config.Channels.defaultModes {

+ 1
- 1
irc/client.go Voir le fichier

232
 		nickCasefolded: "*",
232
 		nickCasefolded: "*",
233
 		nickMaskString: "*", // * is used until actual nick is given
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
 	client.brbTimer.Initialize(client)
236
 	client.brbTimer.Initialize(client)
237
 	session := &Session{
237
 	session := &Session{
238
 		client:     client,
238
 		client:     client,

+ 5
- 4
irc/config.go Voir le fichier

353
 
353
 
354
 	History struct {
354
 	History struct {
355
 		Enabled          bool
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
 	Filename string
363
 	Filename string

+ 85
- 9
irc/history/history.go Voir le fichier

25
 	Nick
25
 	Nick
26
 )
26
 )
27
 
27
 
28
+const (
29
+	initialAutoSize = 32
30
+)
31
+
28
 // a Tagmsg that consists entirely of transient tags is not stored
32
 // a Tagmsg that consists entirely of transient tags is not stored
29
 var transientTags = map[string]bool{
33
 var transientTags = map[string]bool{
30
 	"+draft/typing": true,
34
 	"+draft/typing": true,
77
 	sync.RWMutex
81
 	sync.RWMutex
78
 
82
 
79
 	// ring buffer, see irc/whowas.go for conventions
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
 	lastDiscarded time.Time
90
 	lastDiscarded time.Time
85
 
91
 
86
 	enabled uint32
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
 	result = new(Buffer)
98
 	result = new(Buffer)
91
-	result.Initialize(size)
99
+	result.Initialize(size, window)
92
 	return
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
 	hist.start = -1
112
 	hist.start = -1
98
 	hist.end = -1
113
 	hist.end = -1
114
+	hist.window = window
115
+	hist.maximumSize = size
116
+	hist.nowFunc = time.Now
99
 
117
 
100
 	hist.setEnabled(size)
118
 	hist.setEnabled(size)
101
 }
119
 }
132
 	list.Lock()
150
 	list.Lock()
133
 	defer list.Unlock()
151
 	defer list.Unlock()
134
 
152
 
153
+	list.maybeExpand()
154
+
135
 	var pos int
155
 	var pos int
136
 	if list.start == -1 { // empty
156
 	if list.start == -1 { // empty
137
 		pos = 0
157
 		pos = 0
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
 // Resize shrinks or expands the buffer
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
 	list.Lock()
332
 	list.Lock()
276
 	defer list.Unlock()
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
 	list.setEnabled(size)
354
 	list.setEnabled(size)
279
 
355
 
280
 	if list.start == -1 {
356
 	if list.start == -1 {

+ 83
- 5
irc/history/history_test.go Voir le fichier

5
 
5
 
6
 import (
6
 import (
7
 	"reflect"
7
 	"reflect"
8
+	"strconv"
8
 	"testing"
9
 	"testing"
9
 	"time"
10
 	"time"
10
 )
11
 )
16
 func TestEmptyBuffer(t *testing.T) {
17
 func TestEmptyBuffer(t *testing.T) {
17
 	pastTime := easyParse(timeFormat)
18
 	pastTime := easyParse(timeFormat)
18
 
19
 
19
-	buf := NewHistoryBuffer(0)
20
+	buf := NewHistoryBuffer(0, 0)
20
 	if buf.Enabled() {
21
 	if buf.Enabled() {
21
 		t.Error("the buffer of size 0 must be considered disabled")
22
 		t.Error("the buffer of size 0 must be considered disabled")
22
 	}
23
 	}
33
 		t.Error("the empty/disabled buffer should report results as incomplete")
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
 	if !buf.Enabled() {
38
 	if !buf.Enabled() {
38
 		t.Error("the buffer of size 1 must be considered enabled")
39
 		t.Error("the buffer of size 1 must be considered enabled")
39
 	}
40
 	}
102
 func TestBuffer(t *testing.T) {
103
 func TestBuffer(t *testing.T) {
103
 	start := easyParse("2006-01-01 00:00:00Z")
104
 	start := easyParse("2006-01-01 00:00:00Z")
104
 
105
 
105
-	buf := NewHistoryBuffer(3)
106
+	buf := NewHistoryBuffer(3, 0)
106
 	buf.Add(easyItem("testnick0", "2006-01-01 15:04:05Z"))
107
 	buf.Add(easyItem("testnick0", "2006-01-01 15:04:05Z"))
107
 
108
 
108
 	buf.Add(easyItem("testnick1", "2006-01-02 15:04:05Z"))
109
 	buf.Add(easyItem("testnick1", "2006-01-02 15:04:05Z"))
128
 	assertEqual(toNicks(since), []string{"testnick1"}, t)
129
 	assertEqual(toNicks(since), []string{"testnick1"}, t)
129
 
130
 
130
 	// shrink the buffer, cutting off testnick1
131
 	// shrink the buffer, cutting off testnick1
131
-	buf.Resize(2)
132
+	buf.Resize(2, 0)
132
 	since, complete = buf.Between(easyParse("2006-01-02 00:00:00Z"), time.Now(), false, 0)
133
 	since, complete = buf.Between(easyParse("2006-01-02 00:00:00Z"), time.Now(), false, 0)
133
 	assertEqual(complete, false, t)
134
 	assertEqual(complete, false, t)
134
 	assertEqual(toNicks(since), []string{"testnick2", "testnick3"}, t)
135
 	assertEqual(toNicks(since), []string{"testnick2", "testnick3"}, t)
135
 
136
 
136
-	buf.Resize(5)
137
+	buf.Resize(5, 0)
137
 	buf.Add(easyItem("testnick4", "2006-01-05 15:04:05Z"))
138
 	buf.Add(easyItem("testnick4", "2006-01-05 15:04:05Z"))
138
 	buf.Add(easyItem("testnick5", "2006-01-06 15:04:05Z"))
139
 	buf.Add(easyItem("testnick5", "2006-01-06 15:04:05Z"))
139
 	buf.Add(easyItem("testnick6", "2006-01-07 15:04:05Z"))
140
 	buf.Add(easyItem("testnick6", "2006-01-07 15:04:05Z"))
145
 	since, _ = buf.Between(easyParse("2006-01-03 00:00:00Z"), time.Now(), true, 2)
146
 	since, _ = buf.Between(easyParse("2006-01-03 00:00:00Z"), time.Now(), true, 2)
146
 	assertEqual(toNicks(since), []string{"testnick2", "testnick3"}, t)
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 Voir le fichier

697
 	}
697
 	}
698
 
698
 
699
 	// resize history buffers as needed
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 Voir le fichier

593
     enabled: false
593
     enabled: false
594
 
594
 
595
     # how many channel-specific events (messages, joins, parts) should be tracked per channel?
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
     # how many direct messages and notices should be tracked per user?
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
     # number of messages to automatically play back on channel join (0 to disable):
609
     # number of messages to automatically play back on channel join (0 to disable):
602
     autoreplay-on-join: 0
610
     autoreplay-on-join: 0

Chargement…
Annuler
Enregistrer