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