#!/usr/bin/env python3
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4
from __future__ import print_function

import sys
import os
import resource
import shutil
import shlex
import time
import subprocess
import random
import signal
import hashlib

# this is a disk I/O benchmark script. It runs benchmarks
# over different number of peers.

# to set up the test, build the example directory in release
# and stage client_test and connection_tester to the examples directory:
#
#   bjam -j8 link=static release debug-symbols=on stage_client_test \
#       stage_connection_tester
#
# make sure gnuplot is installed.

# the following lists define the space tests will be run in

peers = [50, 200, 500, 1000]
# builds = ['rtorrent', 'utorrent', 'libtorrent']
builds = ['libtorrent']

# the number of peers for the filesystem test. The
# idea is to stress test the filesystem by using a lot
# of peers, since each peer essentially is a separate
# read location on the platter
default_peers = peers[1]

# the amount of cache for the filesystem test
# 5.5 GiB of cache
default_cache = 400000

# the number of seconds to run each test. It's important that
# this is shorter than what it takes to finish downloading
# the test torrent, since then the average rate will not
# be representative of the peak anymore
# this has to be long enough to download a full copy
# of the test torrent. It's also important for the
# test to be long enough that the warming up of the
# disk cache is not a significant part of the test,
# since download rates will be extremely high while downloading
# into RAM
test_duration = 100

utorrent_version = 'utorrent-server-alpha-v3_3'

# make sure the environment is properly set up
try:
    if os.name == 'posix':
        resource.setrlimit(resource.RLIMIT_NOFILE, (4000, 5000))
except Exception:
    if resource.getrlimit(resource.RLIMIT_NOFILE)[0] < 4000:
        print('please set ulimit -n to at least 4000')
        sys.exit(1)


def build_stage_dirs():
    ret = []
    for i in builds[2:3]:
        ret.append('stage_%s' % i)
    return ret


# make sure we have all the binaries available
binaries = ['client_test', 'connection_tester']
for b in build_stage_dirs():
    for i in binaries:
        p = os.path.join(b, i)
        if not os.path.exists(p):
            print('make sure "%s" is available in ./%s' % (i, b))
            sys.exit(1)

# make sure we have a test torrent
if not os.path.exists('test.torrent'):
    print('generating test torrent')
    # generate a 100 GB torrent, to make sure it won't all fit in physical RAM
    os.system('./connection_tester gen-torrent -s 100000 -t test.torrent')

# use a new port for each test to make sure they keep working
# this port is incremented for each test run
port = 10000 + random.randint(0, 40000)

try:
    os.mkdir('benchmark-dir')
except Exception:
    pass


def clear_caches():
    if 'linux' in sys.platform:
        os.system('sync')
        try:
            open('/proc/sys/vm/drop_caches', 'w').write('3')
        except Exception:
            pass
    elif 'darwin' in sys.platform:
        os.system('purge')


def build_utorrent_commandline(config, port):
    num_peers = config['num-peers']
    torrent_path = config['torrent']
    target_folder = build_target_folder(config)

    try:
        os.mkdir('utorrent_session')
    except Exception:
        pass
    with open('utorrent_session/settings.dat', 'w+') as cfg:

        cfg.write('d')
        cfg.write('20:ul_slots_per_torrenti%de' % num_peers)
        cfg.write('17:conns_per_torrenti%de' % num_peers)
        cfg.write('14:conns_globallyi%de' % num_peers)
        cfg.write('9:bind_porti%de' % port)
        cfg.write('19:dir_active_download%d:%s' % (len(config['save-path']),
                                                   config['save-path']))
        cfg.write('19:diskio.sparse_filesi1e')
        cfg.write('14:cache.overridei1e')
        cfg.write('19:cache.override_sizei%de' % int(config['cache-size'] *
                                                     16 / 1024))
        cfg.write('17:dir_autoload_flagi1e')
        cfg.write('12:dir_autoload8:autoload')
        cfg.write('11:logger_maski4294967295e')
        cfg.write('1:vi0e')
        cfg.write('12:webui.enablei1e')
        cfg.write('19:webui.enable_listeni1e')
        cfg.write('14:webui.hashword20:' + hashlib.sha1(
            'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaadmin').digest())
        cfg.write('10:webui.porti8080e')
        cfg.write('10:webui.salt32:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa')
        cfg.write('14:webui.username5:admin')
        cfg.write('e')

    try:
        os.mkdir('utorrent_session/autoload')
    except Exception:
        pass
    try:
        shutil.copy(torrent_path, 'utorrent_session/autoload/')
    except Exception:
        pass
    return './%s/utserver -logfile %s/client.log -settingspath ' % \
        (utorrent_version, target_folder) + \
        'utorrent_session'


def build_rtorrent_commandline(config, port):
    num_peers = config['num-peers']
    torrent_path = config['torrent']
    target_folder = build_target_folder(config)

    if os.path.exists(target_folder):
        add_command = ''
    else:
        try:
            os.mkdir(target_folder)
        except Exception:
            pass
        # it seems rtorrent may delete the original torrent when it's being added
        try:
            shutil.copy(torrent_path, target_folder)
        except Exception:
            pass
        add_command = '-O load_start_verbose=%s/%s ' % (target_folder, torrent_path)

    return ('rtorrent -d %s -n -p %d-%d -O max_peers=%d -O max_uploads=%d %s -s '
            '%s -O max_memory_usage=128000000000') % (
                config['save-path'], port, port, num_peers, num_peers, add_command, target_folder)


def build_libtorrent_commandline(config, port):
    num_peers = config['num-peers']
    torrent_path = config['torrent']
    target_folder = build_target_folder(config)

    return ('./client_test -k -O -F 500 --enable_upnp=0 --enable_natpmp=0 '
            '--enable_dht=0 --mixed_mode_algorithm=0 --peer_timeout=%d '
            '--listen_queue_size=%d --unchoke_slots_limit=%d -T %d '
            '--connections_limit=%d --cache_size=%d -s "%s" '
            '--listen_interfaces="0.0.0.0:%d" --aio_threads=%d '
            '-f %s/client.log %s') % (
                test_duration, num_peers, num_peers, num_peers, num_peers, config['cache-size'],
                config['save-path'], port, config['disk-threads'], target_folder, torrent_path)


def build_commandline(config, port):

    if config['build'] == 'utorrent':
        return build_utorrent_commandline(config, port)

    if config['build'] == 'rtorrent':
        return build_rtorrent_commandline(config, port)

    if config['build'] == 'libtorrent':
        return build_libtorrent_commandline(config, port)


def delete_files(files):
    for i in files:
        print('deleting %s' % i)
        try:
            os.remove(i)
        except Exception:
            try:
                shutil.rmtree(i)
            except Exception:
                try:
                    if os.path.exists(i):
                        print('failed to delete %s' % i)
                except Exception:
                    pass


def build_test_config(num_peers=default_peers, cache_size=default_cache,
                      test='download', build='libtorrent', profile='', disk_threads=16,
                      torrent='test.torrent', disable_disk=False):
    config = {'test': test, 'save-path': os.path.join('.', 'benchmark-dir'), 'num-peers': num_peers,
              'cache-size': cache_size, 'build': build, 'profile': profile,
              'disk-threads': disk_threads, 'torrent': torrent, 'disable-disk': disable_disk}
    return config


def build_target_folder(config):

    no_disk = ''
    if config['disable-disk']:
        no_disk = '_no-disk'

    return 'results_%s_%s_%d_%d_%d%s' % (config['build'],
                                         config['test'],
                                         config['num-peers'],
                                         config['cache-size'],
                                         config['disk-threads'],
                                         no_disk)


def find_library(name):
    paths = ['/usr/lib64/', '/usr/local/lib64/', '/usr/lib/', '/usr/local/lib/']

    for p in paths:
        try:
            if os.path.exists(p + name):
                return p + name
        except Exception:
            pass
    return name


def find_binary(names):
    paths = ['/usr/bin/', '/usr/local/bin/']
    for n in names:
        for p in paths:
            try:
                if os.path.exists(p + n):
                    return p + n
            except Exception:
                pass
    return names[0]


def run_test(config):

    j = os.path.join

    target_folder = build_target_folder(config)
    if os.path.exists(target_folder):
        print('results already exists, skipping test (%s)' % target_folder)
        return

    print('\n\n*********************************')
    print('*          RUNNING TEST         *')
    print('*********************************\n\n')
    print('%s %s' % (config['build'], config['test']))

    # make sure any previous test file is removed
    # don't clean up unless we're running a download-test, so that we leave the test file
    # complete for a seed test.
    delete_files(['utorrent_session/settings.dat', 'utorrent_session/settings.dat.old', 'asserts.log'])
    if config['test'] == 'download' or config['test'] == 'dual':
        delete_files([j(config['save-path'], 'test'),
                      '.ses_state',
                      j(config['save-path'], '.resume'),
                      'utorrent_session',
                      '.dht_state',
                      'rtorrent_session'])

    try:
        os.mkdir(target_folder)
    except Exception:
        pass

    # save off the command line for reference
    global port
    cmdline = build_commandline(config, port)
    binary = cmdline.split(' ')[0]
    environment = None
    if config['profile'] == 'tcmalloc':
        environment = {'LD_PRELOAD': find_library('libprofiler.so.0'),
                       'CPUPROFILE': j(target_folder, 'cpu_profile.prof')}
    if config['profile'] == 'memory':
        environment = {'LD_PRELOAD': find_library('libprofiler.so.0'),
                       'HEAPPROFILE': j(target_folder, 'heap_profile.prof')}
    if config['profile'] == 'perf':
        cmdline = 'perf record -g --output=' + \
            j(target_folder, 'perf_profile.prof') + ' ' + cmdline
    with open(j(target_folder, 'cmdline.txt'), 'w+') as f:
        f.write(cmdline)

    with open(j(target_folder, 'config.txt'), 'w+') as f:
        print(config, file=f)

    print('clearing disk cache')
    clear_caches()
    print('OK')
    client_output = open(j(target_folder, 'client.output'), 'w+')
    client_error = open(j(target_folder, 'client.error'), 'w+')
    print('launching: %s' % cmdline)
    client = subprocess.Popen(
        shlex.split(cmdline),
        stdout=client_output,
        stdin=subprocess.PIPE,
        stderr=client_error,
        env=environment)
    print('OK')
    # enable disk stats printing
    if config['build'] == 'libtorrent':
        print('x', end=' ', file=client.stdin)
    time.sleep(4)
    test_dir = 'upload' if config['test'] == 'download' else 'download' if config['test'] == 'upload' else 'dual'
    cmdline = './connection_tester %s -c %d -d 127.0.0.1 -p %d -t %s' % (
        test_dir, config['num-peers'], port, config['torrent'])
    print('launching: %s' % cmdline)
    tester_output = open(j(target_folder, 'tester.output'), 'w+')
    tester = subprocess.Popen(shlex.split(cmdline), stdout=tester_output)
    print('OK')

    time.sleep(2)

    print('\n')
    i = 0
    while True:
        time.sleep(1)
        tester.poll()
        if tester.returncode is not None:
            print('tester terminated')
            break
        client.poll()
        if client.returncode is not None:
            print('client terminated')
            break
        print('\r%d / %d\x1b[K' % (i, test_duration), end=' ')
        sys.stdout.flush()
        i += 1
        # in download- and dual tests, connection_tester will exit once the
        # client is done downloading. In upload tests, we'll upload for
        # 'test_duration' number of seconds until we end the test
        if config['test'] != 'download' and config['test'] != 'dual' and i >= test_duration:
            break
    print('\n')

    if client.returncode is None:
        try:
            print('killing client')
            client.send_signal(signal.SIGINT)
        except Exception:
            pass

    time.sleep(10)
    client.wait()
    tester.wait()
    tester_output.close()
    client_output.close()
    terminate = False
    if tester.returncode != 0:
        print('tester returned %d' % tester.returncode)
        terminate = True
    if client.returncode != 0:
        print('client returned %d' % client.returncode)
        terminate = True

    try:
        shutil.copy('asserts.log', target_folder)
    except Exception:
        pass

    os.chdir(target_folder)

    if config['build'] == 'libtorrent':
        # parse session stats
        print('parsing session log')
        os.system('python ../../tools/parse_session_stats.py client.log')

    os.chdir('..')

    if config['profile'] == 'tcmalloc':
        print('analyzing CPU profile [%s]' % binary)
        os.system('%s --pdf %s %s/cpu_profile.prof >%s/cpu_profile.pdf' %
                  (find_binary(['google-pprof', 'pprof']), binary, target_folder, target_folder))
    if config['profile'] == 'memory':
        for i in range(1, 300):
            profile = j(target_folder, 'heap_profile.prof.%04d.heap' % i)
            try:
                os.stat(profile)
            except Exception:
                break
            print('analyzing heap profile [%s] %d' % (binary, i))
            os.system('%s --pdf %s %s >%s/heap_profile_%d.pdf' %
                      (find_binary(['google-pprof', 'pprof']), binary, profile, target_folder, i))
    if config['profile'] == 'perf':
        print('analyzing CPU profile [%s]' % binary)
        os.system(('perf report --input=%s/perf_profile.prof --threads --demangle --show-nr-samples '
                   '>%s/profile.txt' % (target_folder, target_folder)))

    port += 1

    if terminate:
        sys.exit(1)


for b in builds:
    for test in ['upload', 'download', 'dual']:
        config = build_test_config(build=b, test=test, profile='perf')
        run_test(config)

for p in peers:
    for test in ['upload', 'download', 'dual']:
        config = build_test_config(num_peers=p, test=test, profile='perf')
        run_test(config)
