Browse Source

improve performance by using less goroutines

- get rid of command channel in Client
- get rid of loginTimer; use other timers instead
- move debugging code to debug.go
tags/v0.1.0
Jeremy Latt 10 years ago
parent
commit
6c10add399
6 changed files with 153 additions and 108 deletions
  1. 40
    18
      irc/client.go
  2. 0
    2
      irc/commands.go
  3. 68
    0
      irc/debug.go
  4. 2
    2
      irc/reply.go
  5. 24
    54
      irc/server.go
  6. 19
    32
      irc/socket.go

+ 40
- 18
irc/client.go View File

@@ -7,9 +7,8 @@ import (
7 7
 )
8 8
 
9 9
 const (
10
-	LOGIN_TIMEOUT = time.Minute / 2 // how long the client has to login
11
-	IDLE_TIMEOUT  = time.Minute     // how long before a client is considered idle
12
-	QUIT_TIMEOUT  = time.Minute     // how long after idle before a client is kicked
10
+	IDLE_TIMEOUT = time.Minute // how long before a client is considered idle
11
+	QUIT_TIMEOUT = time.Minute // how long after idle before a client is kicked
13 12
 )
14 13
 
15 14
 type Client struct {
@@ -19,14 +18,12 @@ type Client struct {
19 18
 	capabilities CapabilitySet
20 19
 	capState     CapState
21 20
 	channels     ChannelSet
22
-	commands     chan Command
23 21
 	ctime        time.Time
24 22
 	flags        map[UserMode]bool
25 23
 	hasQuit      bool
26 24
 	hops         uint
27 25
 	hostname     Name
28 26
 	idleTimer    *time.Timer
29
-	loginTimer   *time.Timer
30 27
 	nick         Name
31 28
 	quitTimer    *time.Timer
32 29
 	realname     Text
@@ -44,13 +41,12 @@ func NewClient(server *Server, conn net.Conn) *Client {
44 41
 		capState:     CapNone,
45 42
 		capabilities: make(CapabilitySet),
46 43
 		channels:     make(ChannelSet),
47
-		commands:     make(chan Command),
48 44
 		ctime:        now,
49 45
 		flags:        make(map[UserMode]bool),
50 46
 		server:       server,
47
+		socket:       NewSocket(conn),
51 48
 	}
52
-	client.socket = NewSocket(conn, client.commands)
53
-	client.loginTimer = time.AfterFunc(LOGIN_TIMEOUT, client.connectionTimeout)
49
+	client.Touch()
54 50
 	go client.run()
55 51
 
56 52
 	return client
@@ -61,8 +57,31 @@ func NewClient(server *Server, conn net.Conn) *Client {
61 57
 //
62 58
 
63 59
 func (client *Client) run() {
64
-	for command := range client.commands {
65
-		if checkPass, ok := command.(checkPasswordCommand); ok {
60
+	var command Command
61
+	var err error
62
+	var line string
63
+
64
+	// Set the hostname for this client. The client may later send a PROXY
65
+	// command from stunnel that sets the hostname to something more accurate.
66
+	client.send(NewProxyCommand(AddrLookupHostname(
67
+		client.socket.conn.RemoteAddr())))
68
+
69
+	for err == nil {
70
+		if line, err = client.socket.Read(); err != nil {
71
+			command = NewQuitCommand("connection closed")
72
+
73
+		} else if command, err = ParseCommand(line); err != nil {
74
+			switch err {
75
+			case ErrParseCommand:
76
+				client.Reply(RplNotice(client.server, client,
77
+					NewText("failed to parse command")))
78
+
79
+			case NotEnoughArgsError:
80
+				// TODO
81
+			}
82
+			continue
83
+
84
+		} else if checkPass, ok := command.(checkPasswordCommand); ok {
66 85
 			checkPass.LoadPassword(client.server)
67 86
 			// Block the client thread while handling a potentially expensive
68 87
 			// password bcrypt operation. Since the server is single-threaded
@@ -71,13 +90,20 @@ func (client *Client) run() {
71 90
 			// completes. This could be a form of DoS if handled naively.
72 91
 			checkPass.CheckPassword()
73 92
 		}
74
-		command.SetClient(client)
75
-		client.server.commands <- command
93
+
94
+		client.send(command)
76 95
 	}
77 96
 }
78 97
 
98
+func (client *Client) send(command Command) {
99
+	command.SetClient(client)
100
+	client.server.commands <- command
101
+}
102
+
103
+// quit timer goroutine
104
+
79 105
 func (client *Client) connectionTimeout() {
80
-	client.commands <- NewQuitCommand("connection timeout")
106
+	client.send(NewQuitCommand("connection timeout"))
81 107
 }
82 108
 
83 109
 //
@@ -109,7 +135,7 @@ func (client *Client) Touch() {
109 135
 }
110 136
 
111 137
 func (client *Client) Idle() {
112
-	client.Reply(RplPing(client))
138
+	client.Reply(RplPing(client.server))
113 139
 
114 140
 	if client.quitTimer == nil {
115 141
 		client.quitTimer = time.AfterFunc(QUIT_TIMEOUT, client.connectionTimeout)
@@ -123,7 +149,6 @@ func (client *Client) Register() {
123 149
 		return
124 150
 	}
125 151
 	client.registered = true
126
-	client.loginTimer.Stop()
127 152
 	client.Touch()
128 153
 }
129 154
 
@@ -140,9 +165,6 @@ func (client *Client) destroy() {
140 165
 
141 166
 	// clean up self
142 167
 
143
-	if client.loginTimer != nil {
144
-		client.loginTimer.Stop()
145
-	}
146 168
 	if client.idleTimer != nil {
147 169
 		client.idleTimer.Stop()
148 170
 	}

+ 0
- 2
irc/commands.go View File

@@ -702,7 +702,6 @@ func ParseProxyCommand(args []string) (Command, error) {
702 702
 type AwayCommand struct {
703 703
 	BaseCommand
704 704
 	text Text
705
-	away bool
706 705
 }
707 706
 
708 707
 func ParseAwayCommand(args []string) (Command, error) {
@@ -710,7 +709,6 @@ func ParseAwayCommand(args []string) (Command, error) {
710 709
 
711 710
 	if len(args) > 0 {
712 711
 		cmd.text = NewText(args[0])
713
-		cmd.away = true
714 712
 	}
715 713
 
716 714
 	return cmd, nil

+ 68
- 0
irc/debug.go View File

@@ -0,0 +1,68 @@
1
+package irc
2
+
3
+import (
4
+	"os"
5
+	"runtime"
6
+	"runtime/debug"
7
+	"runtime/pprof"
8
+	"time"
9
+)
10
+
11
+func (msg *DebugCommand) HandleServer(server *Server) {
12
+	client := msg.Client()
13
+	if !client.flags[Operator] {
14
+		return
15
+	}
16
+
17
+	switch msg.subCommand {
18
+	case "GCSTATS":
19
+		stats := debug.GCStats{
20
+			Pause:          make([]time.Duration, 10),
21
+			PauseQuantiles: make([]time.Duration, 5),
22
+		}
23
+		debug.ReadGCStats(&stats)
24
+
25
+		server.Replyf(client, "last GC:     %s", stats.LastGC.Format(time.RFC1123))
26
+		server.Replyf(client, "num GC:      %d", stats.NumGC)
27
+		server.Replyf(client, "pause total: %s", stats.PauseTotal)
28
+		server.Replyf(client, "pause quantiles min%%: %s", stats.PauseQuantiles[0])
29
+		server.Replyf(client, "pause quantiles 25%%:  %s", stats.PauseQuantiles[1])
30
+		server.Replyf(client, "pause quantiles 50%%:  %s", stats.PauseQuantiles[2])
31
+		server.Replyf(client, "pause quantiles 75%%:  %s", stats.PauseQuantiles[3])
32
+		server.Replyf(client, "pause quantiles max%%: %s", stats.PauseQuantiles[4])
33
+
34
+	case "NUMGOROUTINE":
35
+		count := runtime.NumGoroutine()
36
+		server.Replyf(client, "num goroutines: %d", count)
37
+
38
+	case "PROFILEHEAP":
39
+		profFile := "ergonomadic.mprof"
40
+		file, err := os.Create(profFile)
41
+		if err != nil {
42
+			server.Replyf(client, "error: %s", err)
43
+			break
44
+		}
45
+		defer file.Close()
46
+		pprof.Lookup("heap").WriteTo(file, 0)
47
+		server.Replyf(client, "written to %s", profFile)
48
+
49
+	case "STARTCPUPROFILE":
50
+		profFile := "ergonomadic.prof"
51
+		file, err := os.Create(profFile)
52
+		if err != nil {
53
+			server.Replyf(client, "error: %s", err)
54
+			break
55
+		}
56
+		if err := pprof.StartCPUProfile(file); err != nil {
57
+			defer file.Close()
58
+			server.Replyf(client, "error: %s", err)
59
+			break
60
+		}
61
+
62
+		server.Replyf(client, "CPU profile writing to %s", profFile)
63
+
64
+	case "STOPCPUPROFILE":
65
+		pprof.StopCPUProfile()
66
+		server.Reply(client, "CPU profiling stopped")
67
+	}
68
+}

+ 2
- 2
irc/reply.go View File

@@ -151,8 +151,8 @@ func RplPing(target Identifiable) string {
151 151
 	return NewStringReply(nil, PING, ":%s", target.Nick())
152 152
 }
153 153
 
154
-func RplPong(client *Client) string {
155
-	return NewStringReply(nil, PONG, client.Nick().String())
154
+func RplPong(client *Client, msg Text) string {
155
+	return NewStringReply(nil, PONG, "%s :%s", client.server, msg.String())
156 156
 }
157 157
 
158 158
 func RplQuit(client *Client, message Text) string {

+ 24
- 54
irc/server.go View File

@@ -8,9 +8,6 @@ import (
8 8
 	"net"
9 9
 	"os"
10 10
 	"os/signal"
11
-	"runtime"
12
-	"runtime/debug"
13
-	"runtime/pprof"
14 11
 	"strings"
15 12
 	"syscall"
16 13
 	"time"
@@ -121,7 +118,6 @@ func (server *Server) loadChannels() {
121 118
 
122 119
 func (server *Server) processCommand(cmd Command) {
123 120
 	client := cmd.Client()
124
-	Log.debug.Printf("%s → %+v", client, cmd)
125 121
 
126 122
 	if !client.registered {
127 123
 		regCmd, ok := cmd.(RegServerCommand)
@@ -138,6 +134,7 @@ func (server *Server) processCommand(cmd Command) {
138 134
 		client.ErrUnknownCommand(cmd.Code())
139 135
 		return
140 136
 	}
137
+
141 138
 	switch srvCmd.(type) {
142 139
 	case *PingCommand, *PongCommand:
143 140
 		client.Touch()
@@ -149,6 +146,7 @@ func (server *Server) processCommand(cmd Command) {
149 146
 		client.Active()
150 147
 		client.Touch()
151 148
 	}
149
+
152 150
 	srvCmd.HandleServer(server)
153 151
 }
154 152
 
@@ -272,6 +270,14 @@ func (s *Server) Nick() Name {
272 270
 	return s.Id()
273 271
 }
274 272
 
273
+func (server *Server) Reply(target *Client, message string) {
274
+	target.Reply(RplPrivMsg(server, target, NewText(message)))
275
+}
276
+
277
+func (server *Server) Replyf(target *Client, format string, args ...interface{}) {
278
+	server.Reply(target, fmt.Sprintf(format, args...))
279
+}
280
+
275 281
 //
276 282
 // registration commands
277 283
 //
@@ -344,7 +350,8 @@ func (m *PassCommand) HandleServer(s *Server) {
344 350
 }
345 351
 
346 352
 func (m *PingCommand) HandleServer(s *Server) {
347
-	m.Client().Reply(RplPong(m.Client()))
353
+	client := m.Client()
354
+	client.Reply(RplPong(client, m.server.Text()))
348 355
 }
349 356
 
350 357
 func (m *PongCommand) HandleServer(s *Server) {
@@ -514,23 +521,33 @@ func (msg *OperCommand) HandleServer(server *Server) {
514 521
 
515 522
 	client.flags[Operator] = true
516 523
 	client.RplYoureOper()
517
-	client.RplUModeIs(client)
524
+	client.Reply(RplModeChanges(client, client, ModeChanges{&ModeChange{
525
+		mode: Operator,
526
+		op:   Add,
527
+	}}))
518 528
 }
519 529
 
520 530
 func (msg *AwayCommand) HandleServer(server *Server) {
521 531
 	client := msg.Client()
522
-	if msg.away {
532
+	if len(msg.text) > 0 {
523 533
 		client.flags[Away] = true
524 534
 	} else {
525 535
 		delete(client.flags, Away)
526 536
 	}
527 537
 	client.awayMessage = msg.text
528 538
 
539
+	var op ModeOp
529 540
 	if client.flags[Away] {
541
+		op = Add
530 542
 		client.RplNowAway()
531 543
 	} else {
544
+		op = Remove
532 545
 		client.RplUnAway()
533 546
 	}
547
+	client.Reply(RplModeChanges(client, client, ModeChanges{&ModeChange{
548
+		mode: Away,
549
+		op:   op,
550
+	}}))
534 551
 }
535 552
 
536 553
 func (msg *IsOnCommand) HandleServer(server *Server) {
@@ -638,53 +655,6 @@ func (msg *NamesCommand) HandleServer(server *Server) {
638 655
 	}
639 656
 }
640 657
 
641
-func (server *Server) Reply(target *Client, format string, args ...interface{}) {
642
-	target.Reply(RplPrivMsg(server, target, NewText(fmt.Sprintf(format, args...))))
643
-}
644
-
645
-func (msg *DebugCommand) HandleServer(server *Server) {
646
-	client := msg.Client()
647
-	if !client.flags[Operator] {
648
-		return
649
-	}
650
-
651
-	switch msg.subCommand {
652
-	case "GC":
653
-		runtime.GC()
654
-		server.Reply(client, "OK")
655
-
656
-	case "GCSTATS":
657
-		stats := debug.GCStats{
658
-			Pause:          make([]time.Duration, 10),
659
-			PauseQuantiles: make([]time.Duration, 5),
660
-		}
661
-		debug.ReadGCStats(&stats)
662
-		server.Reply(client, "last GC:     %s", stats.LastGC.Format(time.RFC1123))
663
-		server.Reply(client, "num GC:      %d", stats.NumGC)
664
-		server.Reply(client, "pause total: %s", stats.PauseTotal)
665
-		server.Reply(client, "pause quantiles min%%: %s", stats.PauseQuantiles[0])
666
-		server.Reply(client, "pause quantiles 25%%:  %s", stats.PauseQuantiles[1])
667
-		server.Reply(client, "pause quantiles 50%%:  %s", stats.PauseQuantiles[2])
668
-		server.Reply(client, "pause quantiles 75%%:  %s", stats.PauseQuantiles[3])
669
-		server.Reply(client, "pause quantiles max%%: %s", stats.PauseQuantiles[4])
670
-
671
-	case "NUMGOROUTINE":
672
-		count := runtime.NumGoroutine()
673
-		server.Reply(client, "num goroutines: %d", count)
674
-
675
-	case "PROFILEHEAP":
676
-		profFile := "ergonomadic-heap.prof"
677
-		file, err := os.Create(profFile)
678
-		if err != nil {
679
-			log.Printf("error: %s", err)
680
-			break
681
-		}
682
-		defer file.Close()
683
-		pprof.Lookup("heap").WriteTo(file, 0)
684
-		server.Reply(client, "written to %s", profFile)
685
-	}
686
-}
687
-
688 658
 func (msg *VersionCommand) HandleServer(server *Server) {
689 659
 	client := msg.Client()
690 660
 	if (msg.target != "") && (msg.target != server.name) {

+ 19
- 32
irc/socket.go View File

@@ -7,25 +7,22 @@ import (
7 7
 )
8 8
 
9 9
 const (
10
-	R   = '→'
11
-	W   = '←'
12
-	EOF = ""
10
+	R = '→'
11
+	W = '←'
13 12
 )
14 13
 
15 14
 type Socket struct {
16
-	conn   net.Conn
17
-	writer *bufio.Writer
15
+	conn    net.Conn
16
+	scanner *bufio.Scanner
17
+	writer  *bufio.Writer
18 18
 }
19 19
 
20
-func NewSocket(conn net.Conn, commands chan<- Command) *Socket {
21
-	socket := &Socket{
22
-		conn:   conn,
23
-		writer: bufio.NewWriter(conn),
20
+func NewSocket(conn net.Conn) *Socket {
21
+	return &Socket{
22
+		conn:    conn,
23
+		scanner: bufio.NewScanner(conn),
24
+		writer:  bufio.NewWriter(conn),
24 25
 	}
25
-
26
-	go socket.readLines(commands)
27
-
28
-	return socket
29 26
 }
30 27
 
31 28
 func (socket *Socket) String() string {
@@ -37,32 +34,22 @@ func (socket *Socket) Close() {
37 34
 	Log.debug.Printf("%s closed", socket)
38 35
 }
39 36
 
40
-func (socket *Socket) readLines(commands chan<- Command) {
41
-	commands <- NewProxyCommand(AddrLookupHostname(socket.conn.RemoteAddr()))
42
-
43
-	scanner := bufio.NewScanner(socket.conn)
44
-	for scanner.Scan() {
45
-		line := scanner.Text()
37
+func (socket *Socket) Read() (line string, err error) {
38
+	for socket.scanner.Scan() {
39
+		line = socket.scanner.Text()
46 40
 		if len(line) == 0 {
47 41
 			continue
48 42
 		}
49 43
 		Log.debug.Printf("%s → %s", socket, line)
50
-
51
-		msg, err := ParseCommand(line)
52
-		if err != nil {
53
-			// TODO error messaging to client
54
-			continue
55
-		}
56
-		commands <- msg
44
+		return
57 45
 	}
58 46
 
59
-	if err := scanner.Err(); err != nil {
60
-		Log.debug.Printf("%s error: %s", socket, err)
47
+	err = socket.scanner.Err()
48
+	socket.isError(err, R)
49
+	if err == nil {
50
+		err = io.EOF
61 51
 	}
62
-
63
-	commands <- NewQuitCommand("connection closed")
64
-
65
-	close(commands)
52
+	return
66 53
 }
67 54
 
68 55
 func (socket *Socket) Write(line string) (err error) {

Loading…
Cancel
Save