├── requirements.txt ├── usage.gif ├── MANIFEST.in ├── do_latency ├── __init__.py ├── __main__.py ├── ping.py ├── download.py ├── pyping.py └── do_latency.py ├── tox.ini ├── .gitignore ├── README.rst ├── LICENSE └── setup.py /requirements.txt: -------------------------------------------------------------------------------- 1 | terminaltables==2.1.0 2 | tqdm==4.23.0 3 | six==1.10.0 4 | -------------------------------------------------------------------------------- /usage.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dizballanze/do-latency/HEAD/usage.gif -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include LICENSE 2 | include README.rst 3 | include requirements.txt 4 | -------------------------------------------------------------------------------- /do_latency/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | __version__ = "0.4.0" 4 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = py27,py34,py35 3 | [testenv] 4 | platform = darwin 5 | commands = do-latency --udp 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | venv/ 2 | venv3/ 3 | .tox/ 4 | .DS_Store 5 | *.swp 6 | *.egg-info 7 | *.png 8 | *.pyc 9 | build/ 10 | dist/ 11 | .vagrant/ 12 | Vagrantfile 13 | -------------------------------------------------------------------------------- /do_latency/__main__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """ executed when `do_latency` directory is called as a script """ 4 | 5 | from .do_latency import main 6 | 7 | 8 | main() 9 | -------------------------------------------------------------------------------- /do_latency/ping.py: -------------------------------------------------------------------------------- 1 | import time 2 | 3 | from .pyping import ping 4 | 5 | 6 | def do_ping(host, count=10, timeout=10, udp=False, hook=None): 7 | results = [] 8 | for i in range(0, count): 9 | results.append(ping(host, timeout, udp=udp)) 10 | if hook is not None: 11 | hook() 12 | return "{:.3f}".format(sum([result for result in results if result is not None]) / count) 13 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | Digital Ocean latency checker 2 | ============================= 3 | 4 | .. image:: https://img.shields.io/badge/Say%20Thanks-!-1EAEDB.svg 5 | :target: https://saythanks.io/to/dizballanze 6 | 7 | Digital Ocean latency checker helps to find fastest DO region from your location. 8 | 9 | INSTALLATION 10 | ------------ 11 | 12 | :: 13 | 14 | pip install do-latency 15 | 16 | USAGE 17 | ----- 18 | 19 | .. image:: https://raw.githubusercontent.com/dizballanze/do-latency/master/usage.gif 20 | 21 | - **-h, --help** - show help 22 | - **--ping-count** - count of ICMP requests for latency check (default: 10) 23 | - **--file-size {10mb, 100mb}** - size of downloaded file (default: 10mb). 24 | - **--udp** - use UDP not ICMP. 25 | 26 | **In some linux systems UDP testing does not work, so you should use true ICMP and run `do-latency` from root:** 27 | 28 | :: 29 | 30 | sudo do-latency 31 | 32 | 33 | TODO 34 | ---- 35 | 36 | [x] latency check with ICMP 37 | 38 | [x] download speed measurement 39 | 40 | [x] python 3 support 41 | 42 | LICENSE 43 | ------- 44 | 45 | MIT 46 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /do_latency/download.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | try: 3 | import urllib.request as urllib2 4 | from urllib.error import URLError 5 | except ImportError: 6 | import urllib2 7 | from urllib2 import URLError 8 | import time 9 | 10 | 11 | BLOCK_SIZE = 8192 12 | 13 | 14 | def do_download(url, hook=None): 15 | """ 16 | Downloads file and returns speed in mbps. 17 | """ 18 | returnFormat = "{:06.3f}" 19 | try: 20 | http_handler = urllib2.urlopen(url) 21 | except URLError as e: 22 | if hook is not None: 23 | hook(None, "'{}': {}".format(url, e.reason)) 24 | return returnFormat.format(0) 25 | file_size = float(http_handler.headers["Content-Length"]) 26 | start_time = time.time() 27 | status_downloaded = 0 28 | while True: 29 | buf = http_handler.read(BLOCK_SIZE) 30 | if not buf: 31 | break 32 | if hook is not None: 33 | status_downloaded += len(buf) 34 | progress = (float(status_downloaded) / file_size) * 100 35 | if progress >= 1: 36 | hook(int(progress)) 37 | status_downloaded = ((float(progress) - int(progress)) / 100) * file_size 38 | speed = ((file_size * 8) / (1024 * 1024)) / (time.time() - start_time) 39 | http_handler.close() 40 | return returnFormat.format(speed) 41 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from setuptools import setup 4 | import re 5 | import os 6 | import io 7 | 8 | def read(fname): 9 | return io.open(os.path.join(os.path.dirname(__file__), fname), encoding="UTF-8").read() 10 | 11 | version = re.search('^__version__\s*=\s*"(.*)"', 12 | open('do_latency/__init__.py').read(), re.M).group(1) 13 | 14 | with open('requirements.txt') as f: 15 | required = f.read().splitlines() 16 | 17 | setup( 18 | name='do-latency', 19 | version=version, 20 | author='Yuri Shikanov', 21 | author_email='dizballanze@gmail.com', 22 | packages=['do_latency'], 23 | # scripts=[], 24 | url='https://github.com/dizballanze/do-latency', 25 | license='MIT', 26 | description='Digital Ocean latency checker helps to find fastest DO region from your location.', 27 | long_description=read('README.rst'), 28 | install_requires=required, 29 | data_files=[('', ['LICENSE', 'README.rst'])], 30 | entry_points={ 31 | "console_scripts": ['do-latency = do_latency.do_latency:main'] 32 | }, 33 | classifiers=[ 34 | 'Development Status :: 4 - Beta', 35 | 'License :: OSI Approved :: MIT License', 36 | 'Programming Language :: Python :: 2.7', 37 | 'Programming Language :: Python :: 3', 38 | 'Programming Language :: Python :: 3.4', 39 | 'Programming Language :: Python :: 3.5', 40 | 'Programming Language :: Python :: Implementation :: CPython', 41 | ], 42 | keywords='digital ocean latency ping connection speed ICMP' 43 | ) 44 | -------------------------------------------------------------------------------- /do_latency/pyping.py: -------------------------------------------------------------------------------- 1 | """Ping utility module. 2 | """ 3 | 4 | __author__ = 'Volodymyr Burenin' 5 | 6 | import os 7 | import random 8 | import select 9 | import socket 10 | import struct 11 | import time 12 | 13 | 14 | def calc_crc16(data): 15 | """Calc byte string CRC16""" 16 | s = 0 17 | if len(data) % 2 == 1: 18 | data += chr(0) 19 | 20 | for i in range(0, len(data), 2): 21 | if type(data[i]) is str: 22 | s += ord(data[i]) + (ord(data[i + 1]) << 8) 23 | else: 24 | s += data[i] + (data[i + 1] << 8) 25 | s &= 0xffffffff 26 | 27 | s = (s >> 16) + (s & 0xffff) 28 | 29 | s = ~s & 0xffff 30 | return s >> 8 | (s << 8 & 0xff00) 31 | 32 | 33 | def receive_reply(raw_socket, wait_timeout): 34 | """ICMP echo reply. 35 | """ 36 | 37 | select_data = select.select([raw_socket], [], [], wait_timeout) 38 | recv_ts = time.time() 39 | 40 | if select_data[0]: 41 | packet, addr = raw_socket.recvfrom(1024) 42 | 43 | # Cut IP header. 44 | ip = packet[0] if type(packet[0]) is int else ord(packet[0]) 45 | ip_len = (ip & 0xf) * 4 46 | icmp_header = packet[ip_len:ip_len + 8] 47 | 48 | icmp_t, icmp_c, crc16, pkt_id, seq = struct.unpack( 49 | '>bbHHH', icmp_header) 50 | 51 | if icmp_t == 0 and icmp_c == 0: 52 | return pkt_id, seq, recv_ts 53 | 54 | return None, None, None 55 | 56 | 57 | def send_ping(raw_socket, dest_addr, pkt_id, seq_code, data_length=48): 58 | """Echo request. 59 | """ 60 | 61 | pkt_crc16 = 0 62 | # icmp_type(1B):icmp_code(1B):crc16(2B):id(2):seq(2b) 63 | header = struct.pack('>bbHHH', 8, 0, pkt_crc16, pkt_id, seq_code) 64 | 65 | data = b'p' * data_length 66 | 67 | pkt_crc16 = calc_crc16(header + data) 68 | header = struct.pack('>bbHHH', 8, 0, pkt_crc16, pkt_id, seq_code) 69 | 70 | packet = header + data 71 | raw_socket.sendto(packet, (dest_addr, socket.MSG_OOB)) 72 | 73 | 74 | def ping(host, timeout=2, ping_id=None, udp=False): 75 | """Ping remote host. 76 | 77 | :param str host: Host name/address. 78 | :param float timeout: timeout. 79 | :param int ping_id: 16 bit integer to identify packet. 80 | """ 81 | dest_addr = socket.gethostbyname(host) 82 | icmp = socket.getprotobyname('icmp') 83 | socket_type = socket.SOCK_DGRAM if udp else socket.SOCK_RAW 84 | raw_socket = socket.socket(socket.AF_INET, socket_type, icmp) 85 | 86 | ping_id = os.getpid() if ping_id is None else ping_id 87 | ping_id &= 0xffff 88 | 89 | seq_code = random.randint(1, 65535) 90 | 91 | latency = None 92 | 93 | start_ts = time.time() 94 | end_ts = start_ts + timeout 95 | 96 | send_ping(raw_socket, dest_addr, ping_id, seq_code) 97 | 98 | while time.time() < end_ts: 99 | r_ping_id, r_seq_code, r_recv_ts = receive_reply(raw_socket, timeout) 100 | if ping_id == r_ping_id and r_seq_code == seq_code: 101 | latency = r_recv_ts - start_ts 102 | break 103 | 104 | return latency 105 | 106 | -------------------------------------------------------------------------------- /do_latency/do_latency.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import argparse 4 | 5 | from tqdm import tqdm 6 | from terminaltables import AsciiTable 7 | import six 8 | 9 | from .ping import do_ping 10 | from .download import do_download 11 | 12 | 13 | REGIONS = { 14 | 'nyc1': 'speedtest-nyc1.digitalocean.com', 15 | 'nyc2': 'speedtest-nyc2.digitalocean.com', 16 | 'nyc3': 'speedtest-nyc3.digitalocean.com', 17 | 'tor1': 'speedtest-tor1.digitalocean.com', 18 | 'ams2': 'speedtest-ams2.digitalocean.com', 19 | 'ams3': 'speedtest-ams3.digitalocean.com', 20 | 'sfo1': 'speedtest-sfo1.digitalocean.com', 21 | 'sfo2': 'speedtest-sfo2.digitalocean.com', 22 | 'sgp1': 'speedtest-sgp1.digitalocean.com', 23 | 'lon1': 'speedtest-lon1.digitalocean.com', 24 | 'fra1': 'speedtest-fra1.digitalocean.com', 25 | 'blr1': 'speedtest-blr1.digitalocean.com' 26 | } 27 | BAR_FORMAT = "{desc}|{bar}|{percentage:3.2f}%" 28 | PADDING_FORMAT = "{:>30}" 29 | 30 | 31 | def start_test(ping_count=10, file_size="10mb", udp=False): 32 | results = {key: [] for key in REGIONS} 33 | # Latency testing 34 | pbar = tqdm(total=(len(REGIONS) * ping_count), desc=PADDING_FORMAT.format("Latency testing"), bar_format=BAR_FORMAT, leave=True) 35 | for region, host in six.iteritems(REGIONS): 36 | pbar.set_description(PADDING_FORMAT.format("Latency testing ({})".format(region))) 37 | results[region].append(do_ping(host, count=ping_count, udp=udp, hook=lambda: pbar.update(1))) 38 | pbar.close() 39 | # Download speed testing 40 | pbar = tqdm(total=(len(REGIONS) * 100), desc=PADDING_FORMAT.format("Download speed testing"), bar_format=BAR_FORMAT, leave=True, disable=False) 41 | for region, host in six.iteritems(REGIONS): 42 | pbar.set_description(PADDING_FORMAT.format("Download speed testing ({})".format(region))) 43 | url = "http://{}/{}.test".format(host, file_size) 44 | results[region].append(do_download(url, lambda progress, message=None: update_pbar(pbar, progress, message))) 45 | # Output sorted by latency results as table 46 | table_data = [[key] + value for key, value in six.iteritems(results)] 47 | table_data.sort(key=lambda row: float(row[1])) 48 | table_data.insert(0, ["Region", "Latency (ms)", "Download speed (mbps)"]) 49 | table = AsciiTable(table_data) 50 | print("\n\n{}\n".format( table.table)) 51 | 52 | 53 | def update_pbar(pbar, progress=None, message=None): 54 | if progress is not None: 55 | pbar.update(progress) 56 | if message is not None: 57 | tqdm.write(message) 58 | 59 | 60 | def main(): 61 | parser = argparse.ArgumentParser(description="Digital Ocean regions latency checking tool.") 62 | parser.add_argument("--ping-count", help='Count of ICMP requests for latency check (default: %(default)s)', type=int, default=10) 63 | parser.add_argument("--file-size", help='File size for download speed test (default: %(default)s)', type=str, default="10mb", choices=("10mb", "100mb")) 64 | parser.add_argument('--udp', dest='udp', action='store_true', help="Use UDP not ICMP") 65 | args = parser.parse_args() 66 | start_test(**args.__dict__) 67 | 68 | 69 | if __name__ == "__main__": 70 | main() 71 | --------------------------------------------------------------------------------