# Copyright (c) 2012 The Chromium OS Authors. All rights reserved. # Use of this source code is governed by a BSD-style license that can be # found in the LICENSE file. """A tool to measure single-stream link bandwidth using HTTP connections.""" import logging, random, time, urllib2 import numpy.random TIMEOUT = 90 class Error(Exception): pass def TimeTransfer(url, data): """Transfers data to/from url. Returns (time, url contents).""" start_time = time.time() result = urllib2.urlopen(url, data=data, timeout=TIMEOUT) got = result.read() transfer_time = time.time() - start_time if transfer_time <= 0: raise Error("Transfer of %s bytes took nonsensical time %s" % (url, transfer_time)) return (transfer_time, got) def TimeTransferDown(url_pattern, size): url = url_pattern % {'size': size} (transfer_time, got) = TimeTransfer(url, data=None) if len(got) != size: raise Error('Got %d bytes, expected %d' % (len(got), size)) return transfer_time def TimeTransferUp(url, size): """If size > 0, POST size bytes to URL, else GET url. Return time taken.""" data = numpy.random.bytes(size) (transfer_time, _) = TimeTransfer(url, data) return transfer_time def BenchmarkOneDirection(latency, label, url, benchmark_function): """Transfer a reasonable amount of data and record the speed. Args: latency: Time for a 1-byte transfer label: Label to add to perf keyvals url: URL (or pattern) to transfer at benchmark_function: Function to perform actual transfer Returns: Key-value dictionary, suitable for reporting to write_perf_keyval. """ size = 1 << 15 # Start with a small download maximum_size = 1 << 24 # Go large, if necessary multiple = 1 remaining = 2 transfer_time = 0 # Long enough that startup latency shouldn't dominate. target = max(20 * latency, 10) logging.info('Target time: %s' % target) while remaining > 0: size = min(int(size * multiple), maximum_size) transfer_time = benchmark_function(url, size) logging.info('Transfer of %s took %s (%s b/s)' % (size, transfer_time, 8 * size / transfer_time)) if transfer_time >= target: break remaining -= 1 # Take the latency into account when guessing a size for a # larger transfer. This is a pretty simple model, but it # appears to work. adjusted_transfer_time = max(transfer_time - latency, 0.01) multiple = target / adjusted_transfer_time if remaining == 0: logging.warning( 'Max size transfer still took less than minimum desired time %s' % target) return {'seconds_%s_fetch_time' % label: transfer_time, 'bytes_%s_bytes_transferred' % label: size, 'bits_second_%s_speed' % label: 8 * size / transfer_time, } def HttpSpeed(download_url_format_string, upload_url): """Measures upload and download performance to the supplied URLs. Args: download_url_format_string: URL pattern with %(size) for payload bytes upload_url: URL that accepts large POSTs Returns: A dict of perf_keyval """ # We want the download to be substantially longer than the # one-byte fetch time that we can isolate bandwidth instead of # latency. latency = TimeTransferDown(download_url_format_string, 1) logging.info('Latency is %s' % latency) down = BenchmarkOneDirection( latency, 'downlink', download_url_format_string, TimeTransferDown) up = BenchmarkOneDirection( latency, 'uplink', upload_url, TimeTransferUp) up.update(down) return up