Skip to content

Instantly share code, notes, and snippets.

@bacher09
Last active May 16, 2026 20:17
Show Gist options
  • Select an option

  • Save bacher09/255749cca2e1b92fc89021cc54fbbafd to your computer and use it in GitHub Desktop.

Select an option

Save bacher09/255749cca2e1b92fc89021cc54fbbafd to your computer and use it in GitHub Desktop.
Tool to access quality of connection for VoIP and Gaming protocols

Some use examples:

$ sudo latency_histogram.py -i eth0 -t src "dst port 26000"
$ sudo latency_histogram.py -i eth0 -t dst "src port 26000"
$ sudo latency_histogram.py -t dst --iface eth0 "src host 168.119.137.110"
$ sudo latency_histogram.py -t src --iface eth0 "dst host 168.119.137.110"

Example output:

Host: Slava (spectator)                                     
[ 0,  2) [count=   150] #                                                                                               
[ 2,  4) [count=    84] #                                   
[ 4,  6) [count=    71] #
[ 6,  8) [count=   172] #     
[ 8, 10) [count=  7535] #######################################################                                         
[10, 12) [count=  3544] ##########################          
[12, 14) [count=  1049] ########                            
[14, 16) [count=     5]                                                                                                 
[16, 18) [count=     0]                                                                                                 
[18, 20) [count=     0]                                     
[20, 22) [count=     0]                                     
[22, 24) [count=     0]                                     
[24, 26) [count=     0]                                                                                                 
[26, 28) [count=     0]                                     
[28, 30) [count=     1]     
[30, 32) [count=    18]   
[32, 34) [count=   800] ######                                                                                          
[34, 36) [count=   141] #                                   
[36, 38) [count=     1]  
[38, 40) [count=     0]                                                                                                 
[40, 42) [count=     0]                                     
[42, 44) [count=     0] 
[44, 46) [count=     0] 
[46, 48) [count=     0]                                                                                                 
[48, 50) [count=     0]                                                                                                 
[50, 52) [count=     0]       
[52, 54) [count=     0]  
[54, 56) [count=     0]  
[56, 58) [count=     1]  
[58, 60) [count=     0]  
[60, 62) [count=     0]                                                                                                 
[62, 64) [count=     0]                                     
[64, 66) [count=     1]                                     
Samples: 13573, Avg: 11.459 ms, Max: 65.595 ms, Mdev: 6.283 ms

Host: Shadow (spectator)                                                                                                
[ 0,  2) [count=   117] ###                                                                                             
[ 2,  4) [count=    22] #
[ 4,  6) [count=    35] #   
[ 6,  8) [count=   139] ####
[ 8, 10) [count=    20] #
[10, 12) [count=    43] #                                                                                               
[12, 14) [count=  1891] #######################################################
[14, 16) [count=   442] #############
[16, 18) [count=    27] #                                                                                               
[18, 20) [count=  1325] #######################################
[20, 22) [count=   907] ##########################
[22, 24) [count=    13] 
[24, 26) [count=     4] 
[26, 28) [count=     5] 
[28, 30) [count=     6] 
[30, 32) [count=     7] 
[32, 34) [count=     5] 
[34, 36) [count=     7] 
[36, 38) [count=     6] 
[38, 40) [count=    14] 
[40, 42) [count=    14]  
[42, 44) [count=    20] #
[44, 46) [count=    30] #                                                                                               
[46, 48) [count=    73] ##                                  
[48, 50) [count=   599] #################        
[50, 52) [count=   570] #################
[52, 54) [count=    43] #                                   
[54, 56) [count=    28] #
[56, 58) [count=    15] 
[58, 60) [count=    13] 
[60, 62) [count=     3] 
[62, 64) [count=     6] 
[64, 66) [count=     4] 
[66, 68) [count=     4] 
[68, 70) [count=     9] 
[70, 72) [count=     1] 
[72, 74) [count=     0] 
[74, 76) [count=     0] 
[76, 78) [count=     1] 
[78, 80) [count=     0] 
[80, 82) [count=     3] 
[82, 84) [count=     1] 
[84, 86) [count=     0] 
[86, 88) [count=     4] 
[88, 90) [count=     0] 
[90, 92) [count=     0] 
[92, 94) [count=     1] 
[94, 96) [count=     3]  
[96, +∞] [count=    18] #                                                                                               
Samples:  6498, Avg: 23.940 ms, Max: 163.040 ms, Mdev: 16.043 ms

Host: Ubuntu (spectator)                                                                                                
[ 0,  2) [count=    50] #                                                                                               
[ 2,  4) [count=     4]                                     
[ 4,  6) [count=    69] #                                                                                               
[ 6,  8) [count=   183] ###                                                                                             
[ 8, 10) [count=   118] ##                                  
[10, 12) [count=   239] ####  
[12, 14) [count=  1372] ########################
[14, 16) [count=  3104] #######################################################
[16, 18) [count=  1469] ##########################
[18, 20) [count=  1269] ######################
[20, 22) [count=  1224] ######################
[22, 24) [count=   355] ######
[24, 26) [count=    34] #
[26, 28) [count=     8] 
[28, 30) [count=     7]  
[30, 32) [count=     2]  
[32, 34) [count=     2]                                                                                                 
[34, 36) [count=     2]                                                                                                 
[36, 38) [count=     3]                                     
[38, 40) [count=     1]                                     
[40, 42) [count=     2]                                     
[42, 44) [count=     2]  
[44, 46) [count=     3]                                                                                                 
[46, 48) [count=     2]       
[48, 50) [count=     0]                                                                                                 
[50, 52) [count=     0]                                     
[52, 54) [count=     0]                                     
[54, 56) [count=     2]  
[56, 58) [count=     2]     
[58, 60) [count=     0]  
[60, 62) [count=     1]  
[62, 64) [count=     1]                                                                                                 
[64, 66) [count=     1]                                     
[66, 68) [count=     0]  
[68, 70) [count=     0]                                                                                                 
[70, 72) [count=     0]                                     
[72, 74) [count=     0]       
[74, 76) [count=     1]  
[76, 78) [count=     1] 
[78, 80) [count=     0] 
[80, 82) [count=     0] 
[82, 84) [count=     0]  
[84, 86) [count=     0]                                                                                                 
[86, 88) [count=     0]                                                                                                 
[88, 90) [count=     0] 
[90, 92) [count=     1]                                     
[92, 94) [count=     0]  
[94, 96) [count=     1]                                     
Samples:  9535, Avg: 16.315 ms, Max: 95.239 ms, Mdev: 4.184 ms  
#!/usr/bin/env python3
import os
import argparse
import subprocess
import shutil
from collections import Counter, defaultdict
import time
import math
import socket
def parse_addr(addr_input):
text = addr_input.decode("utf8").strip(' :')
return tuple(text.rsplit('.', 1))
def trace_packets(query, iface='any'):
devnull = open(os.devnull, "w")
try:
proc = subprocess.Popen(["tcpdump", "-p", "-i", iface, "-n", "-tt",
query], stdout=subprocess.PIPE, stderr=devnull)
for row in iter(proc.stdout.readline, b''):
items = row.split()
if items[1].strip() != b'IP':
continue
try:
yield float(items[0]), parse_addr(items[2]), parse_addr(items[4])
except (ValueError, IndexError):
pass
finally:
proc.kill()
devnull.close()
def validate_pcap(pcap_expression):
if isinstance(pcap_expression, (list, tuple)):
expression = " ".join(pcap_expression)
else:
expression = pcap_expression
with open(os.devnull, "w") as devnull:
result = subprocess.call(['tcpdump', '-i', 'any', '--no-optimize',
'-ddd', expression], stdout=devnull,
stderr=devnull)
if result != 0:
raise ValueError("Bad pcap expression")
else:
return expression
def build_parser():
parser = argparse.ArgumentParser(description='Build latency histogram')
parser.add_argument('-t', '--type', type=str, choices=('src', 'dst'),
default='src')
parser.add_argument('--min-samples', type=int, default=100)
parser.add_argument('-i', '--iface', type=str, default='any')
parser.add_argument('expression', nargs='+', metavar='pcap-expression')
return parser
def trace_latencies(expression, iface, type='src'):
# TODO: Implement gc
if type not in ('src', 'dst'):
raise ValueError("type should be src or dst")
oldts = {}
for ts, src, dst in trace_packets(expression, iface=iface):
if type == 'src':
key = src
else:
key = dst
if key not in oldts:
oldts[key] = ts
continue
diff = ts - oldts[key]
oldts[key] = ts
if diff < 0:
# something wrong, skip this
continue
yield ts, diff, key
class BaseHistogram:
def __init__(self, base_multiplier=1000):
self.base_multiplier = base_multiplier
self.samples = 0
self.max_value = 0
self._sum = 0
self._sum2 = 0
def add_value(self, value):
self.samples += 1
self._sum += value
self._sum2 += value ** 2
if value > self.max_value:
self.max_value = value
def print_header(self):
print("Host: {0}".format(self.format_addr(self.addr)))
def print_footer(self):
if self.samples == 0:
return
avg = self.mean * self.base_multiplier
max_val = self.max_value * self.base_multiplier
mdev = self.mdev * self.base_multiplier
print("Samples: {0:5d}, Avg: {1:.3f} ms, Max: {2:.3f} ms, Mdev: {3:.3f} ms\n"
.format(self.samples, avg, max_val, mdev))
def print_histogram(self):
raise NotImplementedError
@property
def mean(self):
if self.samples == 0:
return None
else:
return float(self._sum) / self.samples
@property
def mdev(self):
if self.samples == 0:
return None
a = self._sum2 / self.samples
b = self.mean ** 2
if a > b:
return math.sqrt(a - b)
else:
# seems we have float point rounding error
# most likely for this case stddev is near zero
return 0
@staticmethod
def format_addr(addr):
try:
socket.inet_pton(socket.AF_INET6, addr[0])
except OSError:
return ":".join(addr)
else:
return "[{0:s}]:{1:d}".format(addr[0], addr[1])
class LinearHistogram(BaseHistogram):
def __init__(self, addr, max_value, step, start=0, base_multiplier=1000):
super(LinearHistogram, self).__init__(base_multiplier=base_multiplier)
self.start = start
self.end = max_value
self.step = step
self.addr = addr
self._multiplier = float(base_multiplier) / step
self._inf_values = 0
self._dct = defaultdict(lambda: 0)
self._max_count = 0
self._m_start = self.start / base_multiplier
self._m_end = self.end / base_multiplier
self._last_line = 0
def add_value(self, value):
super(LinearHistogram, self).add_value(value)
if value < self._m_start:
# ignore values lower than start
return
if value >= self._m_end:
self._inf_values += 1
count = self._inf_values
else:
item = math.floor(self._multiplier * value - self.start)
self._dct[item] += 1
count = self._dct[item]
if item > self._last_line:
self._last_line = item
if count > self._max_count:
self._max_count = count
def print_histogram(self):
self.print_header()
terminal_size = shutil.get_terminal_size((80, 20))
info_fmt = "[{0:2d},{1:3d}) [count={2:6d}]"
info_len = len(info_fmt.format(0, 0, 0))
max_line = terminal_size.columns - info_len - 2
line_multiplier = float(max_line) / self._max_count
start, end = self.start, self.start + self.step
for i in range(0, self._last_line + 1):
count = self._dct[i]
part = info_fmt.format(start, end, count)
bar = '#' * round(count * line_multiplier)
print("{0} {1}".format(part, bar))
start = end
end += self.step
if self._inf_values > 0:
part = "[{0:2d}, +∞] [count={1:6d}]".format(start, self._inf_values)
bar = '#' * round(self._inf_values * line_multiplier)
print("{0} {1}".format(part, bar))
self.print_footer()
def main(args=None):
parser = build_parser()
vals = parser.parse_args(args=args)
try:
expression = validate_pcap(vals.expression)
except ValueError:
parser.error("Invalid pcap expression")
histograms_dct = {}
total = 0
start_time = time.time()
# TODO: change this
hist_constructor = lambda x: LinearHistogram(x, 98, 2)
try:
print("Tracing latency")
for ts, diff, key in trace_latencies(expression, iface=vals.iface,
type=vals.type):
hist = histograms_dct.get(key)
if hist is None:
hist = hist_constructor(key)
histograms_dct[key] = hist
hist.add_value(diff)
total += 1
except KeyboardInterrupt:
pass
end_time = time.time()
diff_time = end_time - start_time
print()
for key, hist in histograms_dct.items():
if hist.samples < vals.min_samples:
continue
hist.print_histogram()
print("total packets: {0:d}, trace time: {1:.3f} seconds\n"
.format(total, diff_time))
if __name__ == '__main__':
main()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment