# 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