From 5f9858749467f846c7eac7e587f93ca4a7aa06da Mon Sep 17 00:00:00 2001 From: douglascdev Date: Mon, 23 Sep 2024 10:13:25 -0300 Subject: [PATCH 1/6] Add client function to return ping latency --- client.go | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/client.go b/client.go index 11c48a4..9e0b784 100644 --- a/client.go +++ b/client.go @@ -378,6 +378,7 @@ type Client struct { channelUserlistMutex *sync.RWMutex channelUserlist map[string]map[string]bool channelsMtx *sync.RWMutex + latencyMutex *sync.RWMutex onConnect func() onWhisperMessage func(message WhisperMessage) onPrivateMessage func(message PrivateMessage) @@ -430,6 +431,13 @@ type Client struct { // The variable may only be modified before calling Connect PongTimeout time.Duration + // LastSentPing is the time the last ping was sent. Used to measure latency. + lastSentPing time.Time + + // Latency is the latency to the irc server measured as the duration + // between when the last ping was sent and when the last pong was received + latency time.Duration + // SetupCmd is the command that is ran on successful connection to Twitch. Useful if you are proxying or something to run a custom command on connect. // The variable must be modified before calling Connect or the command will not run. SetupCmd string @@ -452,6 +460,7 @@ func NewClient(username, oauth string) *Client { channels: map[string]bool{}, channelUserlist: map[string]map[string]bool{}, channelsMtx: &sync.RWMutex{}, + latencyMutex: &sync.RWMutex{}, messageReceived: make(chan bool), read: make(chan string, ReadBufferSize), @@ -616,6 +625,23 @@ func (c *Client) Join(channels ...string) { c.channelsMtx.Unlock() } +// Latency returns the latency to the irc server measured as the duration +// between when the last ping was sent and when the last pong was received. +// Returns zero duration if no ping has been sent yet. +// Returns an error if SendPings is false. +func (c *Client) Latency() (latency time.Duration, err error) { + if !c.SendPings { + err = errors.New("measuring latency requires SendPings to be true") + return + } + + c.latencyMutex.RLock() + defer c.latencyMutex.RUnlock() + + latency = c.latency + return +} + // Creates an irc join message to join the given channels. // // Returns the join message, any channels included in the join message, @@ -862,6 +888,14 @@ func (c *Client) startPinger(closer io.Closer, wg *sync.WaitGroup) { } c.send(pingMessage) + // update lastSentPing without blocking this goroutine waiting for the lock + go func() { + timeSent := time.Now() + c.latencyMutex.Lock() + c.lastSentPing = timeSent + c.latencyMutex.Unlock() + }() + select { case <-c.pongReceived: // Received pong message within the time limit, we're good @@ -1157,6 +1191,9 @@ func (c *Client) handlePongMessage(msg PongMessage) { // Received a pong that was sent by us select { case c.pongReceived <- true: + c.latencyMutex.Lock() + c.latency = time.Since(c.lastSentPing) + c.latencyMutex.Unlock() default: } } From f4e584f1fb312cbdd87adab60eb6a72c92e3aac7 Mon Sep 17 00:00:00 2001 From: douglascdev Date: Mon, 23 Sep 2024 10:31:27 -0300 Subject: [PATCH 2/6] Add latency method to the readme --- README.MD | 1 + 1 file changed, 1 insertion(+) diff --git a/README.MD b/README.MD index 8a5f972..90ad939 100644 --- a/README.MD +++ b/README.MD @@ -208,6 +208,7 @@ func (c *Client) Depart(channel string) func (c *Client) Userlist(channel string) ([]string, error) func (c *Client) Connect() error func (c *Client) Disconnect() error +func (c *Client) Latency() (latency time.Duration, err error) ``` ### Options From 704202d99b0fa3f9dcd80ac71b850271b2881b3a Mon Sep 17 00:00:00 2001 From: douglascdev Date: Wed, 25 Sep 2024 15:53:33 -0300 Subject: [PATCH 3/6] Add latency tests --- client_test.go | 79 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 79 insertions(+) diff --git a/client_test.go b/client_test.go index 8208483..f605ae5 100644 --- a/client_test.go +++ b/client_test.go @@ -1846,6 +1846,85 @@ func TestPinger(t *testing.T) { client.Disconnect() } +func TestLatencySendPingsFalse(t *testing.T) { + t.Parallel() + client := newTestClient("") + client.SendPings = false + if _, err := client.Latency(); err == nil { + t.Fatal("Should not be able to measure latency when SendPings is false") + } +} + +func TestLatencyBeforePings(t *testing.T) { + t.Parallel() + var ( + client *Client + latency time.Duration + err error + ) + client = newTestClient("") + if latency, err = client.Latency(); err != nil { + t.Fatal(fmt.Errorf("Failed to measure latency: %w", err)) + } + + if latency != 0 { + t.Fatal("Latency should be zero before a ping is sent") + } +} + +func TestLatency(t *testing.T) { + t.Parallel() + const idlePingInterval = 10 * time.Millisecond + const expectedLatency = 50 * time.Millisecond + + wait := make(chan bool) + + var conn net.Conn + + host := startServer(t, func(c net.Conn) { + conn = c + }, func(message string) { + if message == pingMessage { + // Send an emulated pong + <-time.After(expectedLatency) + wait <- true + fmt.Fprintf(conn, formatPong(strings.Split(message, " :")[1])+"\r\n") + } + }) + client := newTestClient(host) + client.IdlePingInterval = idlePingInterval + + go client.Connect() + + select { + case <-wait: + case <-time.After(time.Second * 3): + t.Fatal("Did not establish a connection") + } + + var ( + returnedLatency time.Duration + err error + ) + for i := 0; i < 5; i++ { + // Wait for the client to send a ping + <-time.After(idlePingInterval + time.Millisecond*10) + + if returnedLatency, err = client.Latency(); err != nil { + t.Fatal(fmt.Errorf("Failed to measure latency: %w", err)) + } + + returnedLatency = returnedLatency.Round(time.Millisecond) + + if returnedLatency != expectedLatency { + t.Fatalf("Latency %s should be equal to %s", returnedLatency, expectedLatency) + } + + } + + client.Disconnect() +} + func TestCanAttachToPongMessageCallback(t *testing.T) { t.Parallel() From 140d9849a240796b51356caaa99841c1903ff8ba Mon Sep 17 00:00:00 2001 From: douglascdev Date: Wed, 25 Sep 2024 17:00:36 -0300 Subject: [PATCH 4/6] Allow a 3ms difference in latency to account for variance in CPU performance --- client_test.go | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/client_test.go b/client_test.go index f605ae5..d736a99 100644 --- a/client_test.go +++ b/client_test.go @@ -17,8 +17,10 @@ import ( "time" ) -var startPortMutex sync.Mutex -var startPort = 10000 +var ( + startPortMutex sync.Mutex + startPort = 10000 +) func newPort() (r int) { startPortMutex.Lock() @@ -1916,7 +1918,15 @@ func TestLatency(t *testing.T) { returnedLatency = returnedLatency.Round(time.Millisecond) - if returnedLatency != expectedLatency { + latencyDiff := func() time.Duration { + diff := returnedLatency - expectedLatency + if diff < 0 { + return -diff + } + return diff + }() + + if latencyDiff > time.Millisecond*3 { t.Fatalf("Latency %s should be equal to %s", returnedLatency, expectedLatency) } @@ -2143,7 +2153,7 @@ func TestCapabilities(t *testing.T) { in []string expected string } - var tests = []testTable{ + tests := []testTable{ { "Default Capabilities (not modifying)", nil, @@ -2218,7 +2228,7 @@ func TestEmptyCapabilities(t *testing.T) { name string in []string } - var tests = []testTable{ + tests := []testTable{ {"nil", nil}, {"Empty list", []string{}}, } From f2c7e5744ffdfa391efcdc6a786fce7ed666b0ee Mon Sep 17 00:00:00 2001 From: douglascdev Date: Wed, 25 Sep 2024 17:07:05 -0300 Subject: [PATCH 5/6] Fix error message --- client_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client_test.go b/client_test.go index d736a99..9e9e264 100644 --- a/client_test.go +++ b/client_test.go @@ -1927,7 +1927,7 @@ func TestLatency(t *testing.T) { }() if latencyDiff > time.Millisecond*3 { - t.Fatalf("Latency %s should be equal to %s", returnedLatency, expectedLatency) + t.Fatalf("Latency %s should be within 3ms of %s", returnedLatency, expectedLatency) } } From 2519e4794717a514d6b8da8708be58f0598bff68 Mon Sep 17 00:00:00 2001 From: gempir Date: Fri, 27 Sep 2024 21:38:50 +0200 Subject: [PATCH 6/6] add variable for tolerance and increase it to 5ms --- client_test.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/client_test.go b/client_test.go index 9e9e264..3d24385 100644 --- a/client_test.go +++ b/client_test.go @@ -1878,6 +1878,7 @@ func TestLatency(t *testing.T) { t.Parallel() const idlePingInterval = 10 * time.Millisecond const expectedLatency = 50 * time.Millisecond + const toleranceLatency = 5 * time.Millisecond wait := make(chan bool) @@ -1926,7 +1927,7 @@ func TestLatency(t *testing.T) { return diff }() - if latencyDiff > time.Millisecond*3 { + if latencyDiff > toleranceLatency { t.Fatalf("Latency %s should be within 3ms of %s", returnedLatency, expectedLatency) }