Sfoglia il codice sorgente

tweaks to NAMES implementation (#2058)

* tweaks to NAMES implementation

* tweak member caching

* add a benchmark for NAMES
tags/v2.12.0-rc1
Shivaram Lingamneni 1 anno fa
parent
commit
eeec481b8d
Nessun account collegato all'indirizzo email del committer
8 ha cambiato i file con 148 aggiunte e 56 eliminazioni
  1. 46
    50
      irc/channel.go
  2. 43
    0
      irc/client_test.go
  3. 0
    2
      irc/handlers.go
  4. 4
    0
      irc/modes/modes.go
  5. 3
    4
      irc/types.go
  6. 7
    0
      irc/utils/bitset.go
  7. 22
    0
      irc/utils/text.go
  8. 23
    0
      irc/utils/text_test.go

+ 46
- 50
irc/channel.go Vedi File

34
 	key               string
34
 	key               string
35
 	forward           string
35
 	forward           string
36
 	members           MemberSet
36
 	members           MemberSet
37
-	membersCache      []*Client // allow iteration over channel members without holding the lock
38
 	name              string
37
 	name              string
39
 	nameCasefolded    string
38
 	nameCasefolded    string
40
 	server            *Server
39
 	server            *Server
54
 	dirtyBits         uint
53
 	dirtyBits         uint
55
 	settings          ChannelSettings
54
 	settings          ChannelSettings
56
 	uuid              utils.UUID
55
 	uuid              utils.UUID
56
+	// these caches are paired to allow iteration over channel members without holding the lock
57
+	membersCache    []*Client
58
+	memberDataCache []*memberData
57
 }
59
 }
58
 
60
 
59
 // NewChannel creates a new channel from a `Server` and a `name`
61
 // NewChannel creates a new channel from a `Server` and a `name`
421
 
423
 
422
 func (channel *Channel) regenerateMembersCache() {
424
 func (channel *Channel) regenerateMembersCache() {
423
 	channel.stateMutex.RLock()
425
 	channel.stateMutex.RLock()
424
-	result := make([]*Client, len(channel.members))
426
+	membersCache := make([]*Client, len(channel.members))
427
+	dataCache := make([]*memberData, len(channel.members))
425
 	i := 0
428
 	i := 0
426
-	for client := range channel.members {
427
-		result[i] = client
429
+	for client, info := range channel.members {
430
+		membersCache[i] = client
431
+		dataCache[i] = info
428
 		i++
432
 		i++
429
 	}
433
 	}
430
 	channel.stateMutex.RUnlock()
434
 	channel.stateMutex.RUnlock()
431
 
435
 
432
 	channel.stateMutex.Lock()
436
 	channel.stateMutex.Lock()
433
-	channel.membersCache = result
437
+	channel.membersCache = membersCache
438
+	channel.memberDataCache = dataCache
434
 	channel.stateMutex.Unlock()
439
 	channel.stateMutex.Unlock()
435
 }
440
 }
436
 
441
 
438
 func (channel *Channel) Names(client *Client, rb *ResponseBuffer) {
443
 func (channel *Channel) Names(client *Client, rb *ResponseBuffer) {
439
 	channel.stateMutex.RLock()
444
 	channel.stateMutex.RLock()
440
 	clientData, isJoined := channel.members[client]
445
 	clientData, isJoined := channel.members[client]
446
+	chname := channel.name
447
+	membersCache, memberDataCache := channel.membersCache, channel.memberDataCache
441
 	channel.stateMutex.RUnlock()
448
 	channel.stateMutex.RUnlock()
442
 	isOper := client.HasRoleCapabs("sajoin")
449
 	isOper := client.HasRoleCapabs("sajoin")
443
 	respectAuditorium := channel.flags.HasMode(modes.Auditorium) && !isOper &&
450
 	respectAuditorium := channel.flags.HasMode(modes.Auditorium) && !isOper &&
445
 	isMultiPrefix := rb.session.capabilities.Has(caps.MultiPrefix)
452
 	isMultiPrefix := rb.session.capabilities.Has(caps.MultiPrefix)
446
 	isUserhostInNames := rb.session.capabilities.Has(caps.UserhostInNames)
453
 	isUserhostInNames := rb.session.capabilities.Has(caps.UserhostInNames)
447
 
454
 
448
-	maxNamLen := 480 - len(client.server.name) - len(client.Nick())
449
-	var namesLines []string
450
-	var buffer strings.Builder
455
+	maxNamLen := 480 - len(client.server.name) - len(client.Nick()) - len(chname)
456
+	var tl utils.TokenLineBuilder
457
+	tl.Initialize(maxNamLen, " ")
451
 	if isJoined || !channel.flags.HasMode(modes.Secret) || isOper {
458
 	if isJoined || !channel.flags.HasMode(modes.Secret) || isOper {
452
-		for _, target := range channel.Members() {
459
+		for i, target := range membersCache {
460
+			if !isJoined && target.HasMode(modes.Invisible) && !isOper {
461
+				continue
462
+			}
453
 			var nick string
463
 			var nick string
454
 			if isUserhostInNames {
464
 			if isUserhostInNames {
455
 				nick = target.NickMaskString()
465
 				nick = target.NickMaskString()
456
 			} else {
466
 			} else {
457
 				nick = target.Nick()
467
 				nick = target.Nick()
458
 			}
468
 			}
459
-			channel.stateMutex.RLock()
460
-			memberData, _ := channel.members[target]
461
-			channel.stateMutex.RUnlock()
462
-			modeSet := memberData.modes
463
-			if modeSet == nil {
464
-				continue
465
-			}
466
-			if !isJoined && target.HasMode(modes.Invisible) && !isOper {
467
-				continue
468
-			}
469
-			if respectAuditorium && modeSet.HighestChannelUserMode() == modes.Mode(0) {
469
+			memberData := memberDataCache[i]
470
+			if respectAuditorium && memberData.modes.HighestChannelUserMode() == modes.Mode(0) {
470
 				continue
471
 				continue
471
 			}
472
 			}
472
-			prefix := modeSet.Prefixes(isMultiPrefix)
473
-			if buffer.Len()+len(nick)+len(prefix)+1 > maxNamLen {
474
-				namesLines = append(namesLines, buffer.String())
475
-				buffer.Reset()
476
-			}
477
-			if buffer.Len() > 0 {
478
-				buffer.WriteString(" ")
479
-			}
480
-			buffer.WriteString(prefix)
481
-			buffer.WriteString(nick)
482
-		}
483
-		if buffer.Len() > 0 {
484
-			namesLines = append(namesLines, buffer.String())
473
+			tl.AddParts(memberData.modes.Prefixes(isMultiPrefix), nick)
485
 		}
474
 		}
486
 	}
475
 	}
487
 
476
 
488
-	for _, line := range namesLines {
489
-		if buffer.Len() > 0 {
490
-			rb.Add(nil, client.server.name, RPL_NAMREPLY, client.nick, "=", channel.name, line)
491
-		}
477
+	for _, line := range tl.Lines() {
478
+		rb.Add(nil, client.server.name, RPL_NAMREPLY, client.nick, "=", chname, line)
492
 	}
479
 	}
493
-	rb.Add(nil, client.server.name, RPL_ENDOFNAMES, client.nick, channel.name, client.t("End of NAMES list"))
480
+	rb.Add(nil, client.server.name, RPL_ENDOFNAMES, client.nick, chname, client.t("End of NAMES list"))
494
 }
481
 }
495
 
482
 
496
 // does `clientMode` give you privileges to grant/remove `targetMode` to/from people,
483
 // does `clientMode` give you privileges to grant/remove `targetMode` to/from people,
514
 // ClientIsAtLeast returns whether the client has at least the given channel privilege.
501
 // ClientIsAtLeast returns whether the client has at least the given channel privilege.
515
 func (channel *Channel) ClientIsAtLeast(client *Client, permission modes.Mode) bool {
502
 func (channel *Channel) ClientIsAtLeast(client *Client, permission modes.Mode) bool {
516
 	channel.stateMutex.RLock()
503
 	channel.stateMutex.RLock()
517
-	memberData := channel.members[client]
504
+	memberData, present := channel.members[client]
518
 	founder := channel.registeredFounder
505
 	founder := channel.registeredFounder
519
 	channel.stateMutex.RUnlock()
506
 	channel.stateMutex.RUnlock()
520
 
507
 
522
 		return true
509
 		return true
523
 	}
510
 	}
524
 
511
 
512
+	if !present {
513
+		return false
514
+	}
515
+
525
 	for _, mode := range modes.ChannelUserModes {
516
 	for _, mode := range modes.ChannelUserModes {
526
 		if memberData.modes.HasMode(mode) {
517
 		if memberData.modes.HasMode(mode) {
527
 			return true
518
 			return true
571
 	}
562
 	}
572
 	channel.stateMutex.Lock()
563
 	channel.stateMutex.Lock()
573
 	defer channel.stateMutex.Unlock()
564
 	defer channel.stateMutex.Unlock()
574
-	if _, ok := channel.members[client]; !ok {
575
-		return
565
+	if mData, ok := channel.members[client]; ok {
566
+		mData.modes.Clear()
567
+		for _, mode := range status.Modes {
568
+			mData.modes.SetMode(modes.Mode(mode), true)
569
+		}
570
+		mData.joinTime = status.JoinTime
576
 	}
571
 	}
577
-	memberData := channel.members[client]
578
-	memberData.modes = newModes
579
-	memberData.joinTime = status.JoinTime
580
-	channel.members[client] = memberData
581
 }
572
 }
582
 
573
 
583
 func (channel *Channel) ClientHasPrivsOver(client *Client, target *Client) bool {
574
 func (channel *Channel) ClientHasPrivsOver(client *Client, target *Client) bool {
584
 	channel.stateMutex.RLock()
575
 	channel.stateMutex.RLock()
585
 	founder := channel.registeredFounder
576
 	founder := channel.registeredFounder
586
-	clientModes := channel.members[client].modes
587
-	targetModes := channel.members[target].modes
577
+	clientData, clientOK := channel.members[client]
578
+	targetData, targetOK := channel.members[target]
588
 	channel.stateMutex.RUnlock()
579
 	channel.stateMutex.RUnlock()
589
 
580
 
590
 	if founder != "" {
581
 	if founder != "" {
595
 		}
586
 		}
596
 	}
587
 	}
597
 
588
 
598
-	return channelUserModeHasPrivsOver(clientModes.HighestChannelUserMode(), targetModes.HighestChannelUserMode())
589
+	return clientOK && targetOK &&
590
+		channelUserModeHasPrivsOver(
591
+			clientData.modes.HighestChannelUserMode(),
592
+			targetData.modes.HighestChannelUserMode(),
593
+		)
599
 }
594
 }
600
 
595
 
601
 func (channel *Channel) hasClient(client *Client) bool {
596
 func (channel *Channel) hasClient(client *Client) bool {
1319
 
1314
 
1320
 	if channel.flags.HasMode(modes.OpModerated) {
1315
 	if channel.flags.HasMode(modes.OpModerated) {
1321
 		channel.stateMutex.RLock()
1316
 		channel.stateMutex.RLock()
1322
-		cuData := channel.members[client]
1317
+		cuData, ok := channel.members[client]
1323
 		channel.stateMutex.RUnlock()
1318
 		channel.stateMutex.RUnlock()
1324
-		if cuData.modes.HighestChannelUserMode() == modes.Mode(0) {
1319
+		if !ok || cuData.modes.HighestChannelUserMode() == modes.Mode(0) {
1325
 			// max(statusmsg_minmode, halfop)
1320
 			// max(statusmsg_minmode, halfop)
1326
 			if minPrefixMode == modes.Mode(0) || minPrefixMode == modes.Voice {
1321
 			if minPrefixMode == modes.Mode(0) || minPrefixMode == modes.Voice {
1327
 				minPrefixMode = modes.Halfop
1322
 				minPrefixMode = modes.Halfop
1490
 	chname := channel.name
1485
 	chname := channel.name
1491
 	members := channel.membersCache
1486
 	members := channel.membersCache
1492
 	channel.membersCache = nil
1487
 	channel.membersCache = nil
1488
+	channel.memberDataCache = nil
1493
 	channel.members = make(MemberSet)
1489
 	channel.members = make(MemberSet)
1494
 	// TODO try to prevent Purge racing against (pending) Join?
1490
 	// TODO try to prevent Purge racing against (pending) Join?
1495
 	channel.stateMutex.Unlock()
1491
 	channel.stateMutex.Unlock()

+ 43
- 0
irc/client_test.go Vedi File

4
 package irc
4
 package irc
5
 
5
 
6
 import (
6
 import (
7
+	"fmt"
7
 	"testing"
8
 	"testing"
8
 
9
 
10
+	"github.com/ergochat/ergo/irc/languages"
9
 	"github.com/ergochat/ergo/irc/utils"
11
 	"github.com/ergochat/ergo/irc/utils"
10
 )
12
 )
11
 
13
 
30
 	}
32
 	}
31
 }
33
 }
32
 
34
 
35
+func BenchmarkNames(b *testing.B) {
36
+	channelSize := 1024
37
+	server := &Server{
38
+		name: "ergo.test",
39
+	}
40
+	lm, err := languages.NewManager(false, "", "")
41
+	if err != nil {
42
+		b.Fatal(err)
43
+	}
44
+	server.config.Store(&Config{
45
+		languageManager: lm,
46
+	})
47
+	for i := 0; i < b.N; i++ {
48
+		channel := &Channel{
49
+			name:           "#test",
50
+			nameCasefolded: "#test",
51
+			server:         server,
52
+			members:        make(MemberSet),
53
+		}
54
+		for j := 0; j < channelSize; j++ {
55
+			nick := fmt.Sprintf("client_%d", j)
56
+			client := &Client{
57
+				server:         server,
58
+				nick:           nick,
59
+				nickCasefolded: nick,
60
+			}
61
+			channel.members.Add(client)
62
+			channel.regenerateMembersCache()
63
+			session := &Session{
64
+				client: client,
65
+			}
66
+			rb := NewResponseBuffer(session)
67
+			channel.Names(client, rb)
68
+			if len(rb.messages) < 2 {
69
+				b.Fatalf("not enough messages: %d", len(rb.messages))
70
+			}
71
+			// to inspect the messages: line, _ := rb.messages[0].Line()
72
+		}
73
+	}
74
+}
75
+
33
 func TestUserMasks(t *testing.T) {
76
 func TestUserMasks(t *testing.T) {
34
 	var um UserMaskSet
77
 	var um UserMaskSet
35
 
78
 

+ 0
- 2
irc/handlers.go Vedi File

2060
 		channels = strings.Split(msg.Params[0], ",")
2060
 		channels = strings.Split(msg.Params[0], ",")
2061
 	}
2061
 	}
2062
 
2062
 
2063
-	// TODO: in a post-federation world, process `target` (server to forward request to)
2064
-
2065
 	// implement the modern behavior: https://modern.ircdocs.horse/#names-message
2063
 	// implement the modern behavior: https://modern.ircdocs.horse/#names-message
2066
 	// "Servers MAY only return information about the first <channel> and silently ignore the others."
2064
 	// "Servers MAY only return information about the first <channel> and silently ignore the others."
2067
 	// "If no parameter is given for this command, servers SHOULD return one RPL_ENDOFNAMES numeric
2065
 	// "If no parameter is given for this command, servers SHOULD return one RPL_ENDOFNAMES numeric

+ 4
- 0
irc/modes/modes.go Vedi File

345
 	return &set
345
 	return &set
346
 }
346
 }
347
 
347
 
348
+func (set *ModeSet) Clear() {
349
+	utils.BitsetClear(set[:])
350
+}
351
+
348
 // test whether `mode` is set
352
 // test whether `mode` is set
349
 func (set *ModeSet) HasMode(mode Mode) bool {
353
 func (set *ModeSet) HasMode(mode Mode) bool {
350
 	if set == nil {
354
 	if set == nil {

+ 3
- 4
irc/types.go Vedi File

16
 type ClientSet = utils.HashSet[*Client]
16
 type ClientSet = utils.HashSet[*Client]
17
 
17
 
18
 type memberData struct {
18
 type memberData struct {
19
-	modes    *modes.ModeSet
19
+	modes    modes.ModeSet
20
 	joinTime int64
20
 	joinTime int64
21
 }
21
 }
22
 
22
 
23
 // MemberSet is a set of members with modes.
23
 // MemberSet is a set of members with modes.
24
-type MemberSet map[*Client]memberData
24
+type MemberSet map[*Client]*memberData
25
 
25
 
26
 // Add adds the given client to this set.
26
 // Add adds the given client to this set.
27
 func (members MemberSet) Add(member *Client) {
27
 func (members MemberSet) Add(member *Client) {
28
-	members[member] = memberData{
29
-		modes:    modes.NewModeSet(),
28
+	members[member] = &memberData{
30
 		joinTime: time.Now().UnixNano(),
29
 		joinTime: time.Now().UnixNano(),
31
 	}
30
 	}
32
 }
31
 }

+ 7
- 0
irc/utils/bitset.go Vedi File

48
 	}
48
 	}
49
 }
49
 }
50
 
50
 
51
+// BitsetClear clears the bitset in-place.
52
+func BitsetClear(set []uint32) {
53
+	for i := 0; i < len(set); i++ {
54
+		atomic.StoreUint32(&set[i], 0)
55
+	}
56
+}
57
+
51
 // BitsetEmpty returns whether the bitset is empty.
58
 // BitsetEmpty returns whether the bitset is empty.
52
 // This has false positives under concurrent modification (i.e., it can return true
59
 // This has false positives under concurrent modification (i.e., it can return true
53
 // even though w.r.t. the sequence of atomic modifications, there was no point at
60
 // even though w.r.t. the sequence of atomic modifications, there was no point at

+ 22
- 0
irc/utils/text.go Vedi File

125
 	t.buf.WriteString(token)
125
 	t.buf.WriteString(token)
126
 }
126
 }
127
 
127
 
128
+// AddParts concatenates `parts` into a token and adds it to the line,
129
+// creating a new line if necessary.
130
+func (t *TokenLineBuilder) AddParts(parts ...string) {
131
+	var tokenLen int
132
+	for _, part := range parts {
133
+		tokenLen += len(part)
134
+	}
135
+	if t.buf.Len() != 0 {
136
+		tokenLen += len(t.delim)
137
+	}
138
+	if t.lineLen < t.buf.Len()+tokenLen {
139
+		t.result = append(t.result, t.buf.String())
140
+		t.buf.Reset()
141
+	}
142
+	if t.buf.Len() != 0 {
143
+		t.buf.WriteString(t.delim)
144
+	}
145
+	for _, part := range parts {
146
+		t.buf.WriteString(part)
147
+	}
148
+}
149
+
128
 // Lines terminates the line-building and returns all the lines.
150
 // Lines terminates the line-building and returns all the lines.
129
 func (t *TokenLineBuilder) Lines() (result []string) {
151
 func (t *TokenLineBuilder) Lines() (result []string) {
130
 	result = t.result
152
 	result = t.result

+ 23
- 0
irc/utils/text_test.go Vedi File

43
 	val = BuildTokenLines(10, []string{"abcd", "efgh", "ijkl"}, ",")
43
 	val = BuildTokenLines(10, []string{"abcd", "efgh", "ijkl"}, ",")
44
 	assertEqual(val, []string{"abcd,efgh", "ijkl"}, t)
44
 	assertEqual(val, []string{"abcd,efgh", "ijkl"}, t)
45
 }
45
 }
46
+
47
+func TestTLBuilderAddParts(t *testing.T) {
48
+	var tl TokenLineBuilder
49
+	tl.Initialize(20, " ")
50
+	tl.Add("bob")
51
+	tl.AddParts("@", "alice")
52
+	tl.AddParts("@", "ErgoBot__")
53
+	assertEqual(tl.Lines(), []string{"bob @alice", "@ErgoBot__"}, t)
54
+}
55
+
56
+func BenchmarkTokenLines(b *testing.B) {
57
+	tokens := strings.Fields(monteCristo)
58
+	b.ResetTimer()
59
+
60
+	for i := 0; i < b.N; i++ {
61
+		var tl TokenLineBuilder
62
+		tl.Initialize(400, " ")
63
+		for _, tok := range tokens {
64
+			tl.Add(tok)
65
+		}
66
+		tl.Lines()
67
+	}
68
+}

Loading…
Annulla
Salva