1# Lint as: python2, python3 2# Copyright (c) 2012 The Chromium OS Authors. All rights reserved. 3# Use of this source code is governed by a BSD-style license that can be 4# found in the LICENSE file. 5 6"""A tool to measure single-stream link bandwidth using HTTP connections.""" 7 8import logging, random, time 9from six.moves import urllib 10 11import numpy.random 12 13TIMEOUT = 90 14 15 16class Error(Exception): 17 pass 18 19 20def TimeTransfer(url, data): 21 """Transfers data to/from url. Returns (time, url contents).""" 22 start_time = time.time() 23 result = urllib.request.urlopen(url, data=data, timeout=TIMEOUT) 24 got = result.read() 25 transfer_time = time.time() - start_time 26 if transfer_time <= 0: 27 raise Error("Transfer of %s bytes took nonsensical time %s" 28 % (url, transfer_time)) 29 return (transfer_time, got) 30 31 32def TimeTransferDown(url_pattern, size): 33 url = url_pattern % {'size': size} 34 (transfer_time, got) = TimeTransfer(url, data=None) 35 if len(got) != size: 36 raise Error('Got %d bytes, expected %d' % (len(got), size)) 37 return transfer_time 38 39 40def TimeTransferUp(url, size): 41 """If size > 0, POST size bytes to URL, else GET url. Return time taken.""" 42 data = numpy.random.bytes(size) 43 (transfer_time, _) = TimeTransfer(url, data) 44 return transfer_time 45 46 47def BenchmarkOneDirection(latency, label, url, benchmark_function): 48 """Transfer a reasonable amount of data and record the speed. 49 50 Args: 51 latency: Time for a 1-byte transfer 52 label: Label to add to perf keyvals 53 url: URL (or pattern) to transfer at 54 benchmark_function: Function to perform actual transfer 55 Returns: 56 Key-value dictionary, suitable for reporting to write_perf_keyval. 57 """ 58 59 size = 1 << 15 # Start with a small download 60 maximum_size = 1 << 24 # Go large, if necessary 61 multiple = 1 62 63 remaining = 2 64 transfer_time = 0 65 66 # Long enough that startup latency shouldn't dominate. 67 target = max(20 * latency, 10) 68 logging.info('Target time: %s' % target) 69 70 while remaining > 0: 71 size = min(int(size * multiple), maximum_size) 72 transfer_time = benchmark_function(url, size) 73 logging.info('Transfer of %s took %s (%s b/s)' 74 % (size, transfer_time, 8 * size / transfer_time)) 75 if transfer_time >= target: 76 break 77 remaining -= 1 78 79 # Take the latency into account when guessing a size for a 80 # larger transfer. This is a pretty simple model, but it 81 # appears to work. 82 adjusted_transfer_time = max(transfer_time - latency, 0.01) 83 multiple = target / adjusted_transfer_time 84 85 if remaining == 0: 86 logging.warning( 87 'Max size transfer still took less than minimum desired time %s' 88 % target) 89 90 return {'seconds_%s_fetch_time' % label: transfer_time, 91 'bytes_%s_bytes_transferred' % label: size, 92 'bits_second_%s_speed' % label: 8 * size / transfer_time, 93 } 94 95 96def HttpSpeed(download_url_format_string, 97 upload_url): 98 """Measures upload and download performance to the supplied URLs. 99 100 Args: 101 download_url_format_string: URL pattern with %(size) for payload bytes 102 upload_url: URL that accepts large POSTs 103 Returns: 104 A dict of perf_keyval 105 """ 106 # We want the download to be substantially longer than the 107 # one-byte fetch time that we can isolate bandwidth instead of 108 # latency. 109 latency = TimeTransferDown(download_url_format_string, 1) 110 111 logging.info('Latency is %s' % latency) 112 113 down = BenchmarkOneDirection( 114 latency, 115 'downlink', 116 download_url_format_string, 117 TimeTransferDown) 118 119 up = BenchmarkOneDirection( 120 latency, 121 'uplink', 122 upload_url, 123 TimeTransferUp) 124 125 up.update(down) 126 return up 127