diff --git a/ping.go b/ping.go index 27d4547..3222e0c 100644 --- a/ping.go +++ b/ping.go @@ -164,6 +164,12 @@ type Pinger struct { stddevm2 float64 statsMu sync.RWMutex + // Jitter calculation fields + prevRtt time.Duration + jitterSum time.Duration + jitterCount int + Jitter time.Duration + // If true, keep a record of rtts of all received packets. // Set to false to avoid memory bloat for long running pings. RecordRtts bool @@ -315,6 +321,9 @@ type Statistics struct { // StdDevRtt is the standard deviation of the round-trip times sent via // this pinger. StdDevRtt time.Duration + + // Jitter calculation based on mean absolute differences between consecutive RTTs + Jitter time.Duration } func (p *Pinger) updateStatistics(pkt *Packet) { @@ -324,6 +333,20 @@ func (p *Pinger) updateStatistics(pkt *Packet) { p.PacketsRecv++ if p.RecordRtts { p.rtts = append(p.rtts, pkt.Rtt) + + // Jitter calculation (mean absolute difference between RTTs) + if p.jitterCount == 0 { + p.jitterCount = 1 + } else { + diff := pkt.Rtt - p.prevRtt + if diff < 0 { + diff = -diff + } + p.jitterSum += diff + p.Jitter = p.jitterSum / time.Duration(p.jitterCount) + p.jitterCount++ + } + p.prevRtt = pkt.Rtt } if p.RecordTTLs { @@ -705,6 +728,7 @@ func (p *Pinger) Statistics() *Statistics { MinRtt: p.minRtt, AvgRtt: p.avgRtt, StdDevRtt: p.stdDevRtt, + Jitter: p.Jitter, } return &s } diff --git a/ping_test.go b/ping_test.go index 7605e9e..f04851e 100644 --- a/ping_test.go +++ b/ping_test.go @@ -376,6 +376,98 @@ func TestEmptyIPAddr(t *testing.T) { AssertError(t, err, "empty pinger did not return an error") } +func TestStatistics1(t *testing.T) { + // Create a localhost ipv4 pinger + p := New("localhost") + err := p.Resolve() + AssertNoError(t, err) + AssertEqualStrings(t, "localhost", p.Addr()) + + p.PacketsSent = 10 + p.updateStatistics(&Packet{Rtt: time.Duration(2890)}) + p.updateStatistics(&Packet{Rtt: time.Duration(2330)}) + p.updateStatistics(&Packet{Rtt: time.Duration(2450)}) + p.updateStatistics(&Packet{Rtt: time.Duration(2990)}) + p.updateStatistics(&Packet{Rtt: time.Duration(3150)}) + p.updateStatistics(&Packet{Rtt: time.Duration(2700)}) + p.updateStatistics(&Packet{Rtt: time.Duration(3420)}) + p.updateStatistics(&Packet{Rtt: time.Duration(2400)}) + p.updateStatistics(&Packet{Rtt: time.Duration(3080)}) + p.updateStatistics(&Packet{Rtt: time.Duration(2850)}) + + stats := p.Statistics() + if stats.PacketsRecv != 10 { + t.Errorf("Expected %v, got %v", 10, stats.PacketsRecv) + } + if stats.PacketsSent != 10 { + t.Errorf("Expected %v, got %v", 10, stats.PacketsSent) + } + if stats.PacketLoss != 0 { + t.Errorf("Expected %v, got %v", 0, stats.PacketLoss) + } + if stats.MinRtt != time.Duration(2330) { + t.Errorf("Expected %v, got %v", time.Duration(2330), stats.MinRtt) + } + if stats.MaxRtt != time.Duration(3420) { + t.Errorf("Expected %v, got %v", time.Duration(3420), stats.MaxRtt) + } + if stats.AvgRtt != time.Duration(2826) { + t.Errorf("Expected %v, got %v", time.Duration(2826), stats.AvgRtt) + } + if stats.StdDevRtt != time.Duration(337) { + t.Errorf("Expected %v, got %v", time.Duration(337), stats.StdDevRtt) + } + if stats.Jitter != time.Duration(497) { + t.Errorf("Expected %v, got %v", time.Duration(497), stats.Jitter) + } +} + +func TestStatistics2(t *testing.T) { + // Create a localhost ipv4 pinger + p := New("localhost") + err := p.Resolve() + AssertNoError(t, err) + AssertEqualStrings(t, "localhost", p.Addr()) + + p.PacketsSent = 10 + p.updateStatistics(&Packet{Rtt: time.Duration(36700)}) + p.updateStatistics(&Packet{Rtt: time.Duration(36900)}) + p.updateStatistics(&Packet{Rtt: time.Duration(37600)}) + p.updateStatistics(&Packet{Rtt: time.Duration(37500)}) + p.updateStatistics(&Packet{Rtt: time.Duration(37400)}) + p.updateStatistics(&Packet{Rtt: time.Duration(37200)}) + p.updateStatistics(&Packet{Rtt: time.Duration(36700)}) + p.updateStatistics(&Packet{Rtt: time.Duration(37000)}) + p.updateStatistics(&Packet{Rtt: time.Duration(37200)}) + p.updateStatistics(&Packet{Rtt: time.Duration(38100)}) + + stats := p.Statistics() + if stats.PacketsRecv != 10 { + t.Errorf("Expected %v, got %v", 10, stats.PacketsRecv) + } + if stats.PacketsSent != 10 { + t.Errorf("Expected %v, got %v", 10, stats.PacketsSent) + } + if stats.PacketLoss != 0 { + t.Errorf("Expected %v, got %v", 0, stats.PacketLoss) + } + if stats.MinRtt != time.Duration(36700) { + t.Errorf("Expected %v, got %v", time.Duration(36700), stats.MinRtt) + } + if stats.MaxRtt != time.Duration(38100) { + t.Errorf("Expected %v, got %v", time.Duration(38100), stats.MaxRtt) + } + if stats.AvgRtt != time.Duration(37230) { + t.Errorf("Expected %v, got %v", time.Duration(37230), stats.AvgRtt) + } + if stats.StdDevRtt != time.Duration(414) { + t.Errorf("Expected %v, got %v", time.Duration(414), stats.StdDevRtt) + } + if stats.Jitter != time.Duration(355) { + t.Errorf("Expected %v, got %v", time.Duration(355), stats.Jitter) + } +} + func TestStatisticsSunny(t *testing.T) { // Create a localhost ipv4 pinger p := New("localhost") @@ -417,6 +509,9 @@ func TestStatisticsSunny(t *testing.T) { if stats.StdDevRtt != time.Duration(0) { t.Errorf("Expected %v, got %v", time.Duration(0), stats.StdDevRtt) } + if stats.Jitter != time.Duration(0) { + t.Errorf("Expected %v, got %v", time.Duration(0), stats.Jitter) + } } func TestStatisticsLossy(t *testing.T) { @@ -460,6 +555,9 @@ func TestStatisticsLossy(t *testing.T) { if stats.StdDevRtt != time.Duration(29603) { t.Errorf("Expected %v, got %v", time.Duration(29603), stats.StdDevRtt) } + if stats.Jitter != time.Duration(24367) { + t.Errorf("Expected %v, got %v", time.Duration(24367), stats.Jitter) + } } func TestStatisticsZeroDivision(t *testing.T) {