Browse Source

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 year ago
parent
commit
eeec481b8d
No account linked to committer's email address
8 changed files with 148 additions and 56 deletions
  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 View File

@@ -34,7 +34,6 @@ type Channel struct {
34 34
 	key               string
35 35
 	forward           string
36 36
 	members           MemberSet
37
-	membersCache      []*Client // allow iteration over channel members without holding the lock
38 37
 	name              string
39 38
 	nameCasefolded    string
40 39
 	server            *Server
@@ -54,6 +53,9 @@ type Channel struct {
54 53
 	dirtyBits         uint
55 54
 	settings          ChannelSettings
56 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 61
 // NewChannel creates a new channel from a `Server` and a `name`
@@ -421,16 +423,19 @@ func (channel *Channel) AcceptTransfer(client *Client) (err error) {
421 423
 
422 424
 func (channel *Channel) regenerateMembersCache() {
423 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 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 432
 		i++
429 433
 	}
430 434
 	channel.stateMutex.RUnlock()
431 435
 
432 436
 	channel.stateMutex.Lock()
433
-	channel.membersCache = result
437
+	channel.membersCache = membersCache
438
+	channel.memberDataCache = dataCache
434 439
 	channel.stateMutex.Unlock()
435 440
 }
436 441
 
@@ -438,6 +443,8 @@ func (channel *Channel) regenerateMembersCache() {
438 443
 func (channel *Channel) Names(client *Client, rb *ResponseBuffer) {
439 444
 	channel.stateMutex.RLock()
440 445
 	clientData, isJoined := channel.members[client]
446
+	chname := channel.name
447
+	membersCache, memberDataCache := channel.membersCache, channel.memberDataCache
441 448
 	channel.stateMutex.RUnlock()
442 449
 	isOper := client.HasRoleCapabs("sajoin")
443 450
 	respectAuditorium := channel.flags.HasMode(modes.Auditorium) && !isOper &&
@@ -445,52 +452,32 @@ func (channel *Channel) Names(client *Client, rb *ResponseBuffer) {
445 452
 	isMultiPrefix := rb.session.capabilities.Has(caps.MultiPrefix)
446 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 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 463
 			var nick string
454 464
 			if isUserhostInNames {
455 465
 				nick = target.NickMaskString()
456 466
 			} else {
457 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 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 483
 // does `clientMode` give you privileges to grant/remove `targetMode` to/from people,
@@ -514,7 +501,7 @@ func channelUserModeHasPrivsOver(clientMode modes.Mode, targetMode modes.Mode) b
514 501
 // ClientIsAtLeast returns whether the client has at least the given channel privilege.
515 502
 func (channel *Channel) ClientIsAtLeast(client *Client, permission modes.Mode) bool {
516 503
 	channel.stateMutex.RLock()
517
-	memberData := channel.members[client]
504
+	memberData, present := channel.members[client]
518 505
 	founder := channel.registeredFounder
519 506
 	channel.stateMutex.RUnlock()
520 507
 
@@ -522,6 +509,10 @@ func (channel *Channel) ClientIsAtLeast(client *Client, permission modes.Mode) b
522 509
 		return true
523 510
 	}
524 511
 
512
+	if !present {
513
+		return false
514
+	}
515
+
525 516
 	for _, mode := range modes.ChannelUserModes {
526 517
 		if memberData.modes.HasMode(mode) {
527 518
 			return true
@@ -571,20 +562,20 @@ func (channel *Channel) setMemberStatus(client *Client, status alwaysOnChannelSt
571 562
 	}
572 563
 	channel.stateMutex.Lock()
573 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 574
 func (channel *Channel) ClientHasPrivsOver(client *Client, target *Client) bool {
584 575
 	channel.stateMutex.RLock()
585 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 579
 	channel.stateMutex.RUnlock()
589 580
 
590 581
 	if founder != "" {
@@ -595,7 +586,11 @@ func (channel *Channel) ClientHasPrivsOver(client *Client, target *Client) bool
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 596
 func (channel *Channel) hasClient(client *Client) bool {
@@ -1319,9 +1314,9 @@ func (channel *Channel) SendSplitMessage(command string, minPrefixMode modes.Mod
1319 1314
 
1320 1315
 	if channel.flags.HasMode(modes.OpModerated) {
1321 1316
 		channel.stateMutex.RLock()
1322
-		cuData := channel.members[client]
1317
+		cuData, ok := channel.members[client]
1323 1318
 		channel.stateMutex.RUnlock()
1324
-		if cuData.modes.HighestChannelUserMode() == modes.Mode(0) {
1319
+		if !ok || cuData.modes.HighestChannelUserMode() == modes.Mode(0) {
1325 1320
 			// max(statusmsg_minmode, halfop)
1326 1321
 			if minPrefixMode == modes.Mode(0) || minPrefixMode == modes.Voice {
1327 1322
 				minPrefixMode = modes.Halfop
@@ -1490,6 +1485,7 @@ func (channel *Channel) Purge(source string) {
1490 1485
 	chname := channel.name
1491 1486
 	members := channel.membersCache
1492 1487
 	channel.membersCache = nil
1488
+	channel.memberDataCache = nil
1493 1489
 	channel.members = make(MemberSet)
1494 1490
 	// TODO try to prevent Purge racing against (pending) Join?
1495 1491
 	channel.stateMutex.Unlock()

+ 43
- 0
irc/client_test.go View File

@@ -4,8 +4,10 @@
4 4
 package irc
5 5
 
6 6
 import (
7
+	"fmt"
7 8
 	"testing"
8 9
 
10
+	"github.com/ergochat/ergo/irc/languages"
9 11
 	"github.com/ergochat/ergo/irc/utils"
10 12
 )
11 13
 
@@ -30,6 +32,47 @@ func BenchmarkGenerateBatchID(b *testing.B) {
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 76
 func TestUserMasks(t *testing.T) {
34 77
 	var um UserMaskSet
35 78
 

+ 0
- 2
irc/handlers.go View File

@@ -2060,8 +2060,6 @@ func namesHandler(server *Server, client *Client, msg ircmsg.Message, rb *Respon
2060 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 2063
 	// implement the modern behavior: https://modern.ircdocs.horse/#names-message
2066 2064
 	// "Servers MAY only return information about the first <channel> and silently ignore the others."
2067 2065
 	// "If no parameter is given for this command, servers SHOULD return one RPL_ENDOFNAMES numeric

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

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

+ 3
- 4
irc/types.go View File

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

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

@@ -48,6 +48,13 @@ func BitsetSet(set []uint32, position uint, on bool) (changed bool) {
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 58
 // BitsetEmpty returns whether the bitset is empty.
52 59
 // This has false positives under concurrent modification (i.e., it can return true
53 60
 // even though w.r.t. the sequence of atomic modifications, there was no point at

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

@@ -125,6 +125,28 @@ func (t *TokenLineBuilder) Add(token string) {
125 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 150
 // Lines terminates the line-building and returns all the lines.
129 151
 func (t *TokenLineBuilder) Lines() (result []string) {
130 152
 	result = t.result

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

@@ -43,3 +43,26 @@ func TestBuildTokenLines(t *testing.T) {
43 43
 	val = BuildTokenLines(10, []string{"abcd", "efgh", "ijkl"}, ",")
44 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…
Cancel
Save