# 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