#!/usr/bin/env python3

# Copyright (c) 2009, Giampaolo Rodola'. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.

"""Benchmark all API calls and print them from fastest to slowest.

$ make print_api_speed
SYSTEM APIS                NUM CALLS      SECONDS
-------------------------------------------------
disk_usage                       300      0.00157
cpu_count                        300      0.00255
pid_exists                       300      0.00792
cpu_times                        300      0.01044
boot_time                        300      0.01136
cpu_percent                      300      0.01290
cpu_times_percent                300      0.01515
virtual_memory                   300      0.01594
users                            300      0.01964
net_io_counters                  300      0.02027
cpu_stats                        300      0.02034
net_if_addrs                     300      0.02962
swap_memory                      300      0.03209
sensors_battery                  300      0.05186
pids                             300      0.07954
net_if_stats                     300      0.09321
disk_io_counters                 300      0.09406
cpu_count (cores)                300      0.10293
disk_partitions                  300      0.10345
cpu_freq                         300      0.20817
sensors_fans                     300      0.63476
sensors_temperatures             231      2.00039
process_iter (all)               171      2.01300
net_connections                   97      2.00206

PROCESS APIS               NUM CALLS      SECONDS
-------------------------------------------------
create_time                      300      0.00009
exe                              300      0.00015
nice                             300      0.00057
ionice                           300      0.00091
cpu_affinity                     300      0.00091
cwd                              300      0.00151
num_fds                          300      0.00391
memory_info                      300      0.00597
memory_percent                   300      0.00648
io_counters                      300      0.00707
name                             300      0.00894
status                           300      0.00900
ppid                             300      0.00906
num_threads                      300      0.00932
cpu_num                          300      0.00933
num_ctx_switches                 300      0.00943
uids                             300      0.00979
gids                             300      0.01002
cpu_times                        300      0.01008
cmdline                          300      0.01009
terminal                         300      0.01059
is_running                       300      0.01063
threads                          300      0.01209
connections                      300      0.01276
cpu_percent                      300      0.01463
open_files                       300      0.01630
username                         300      0.01655
environ                          300      0.02250
memory_full_info                 300      0.07066
memory_maps                      300      0.74281
"""


import argparse
import inspect
import os
import sys
from timeit import default_timer as timer

import psutil
from psutil._common import print_color


TIMES = 300
timings = []
templ = "{:<25} {:>10}   {:>10}"


def print_header(what):
    s = templ.format(what, "NUM CALLS", "SECONDS")
    print_color(s, color=None, bold=True)
    print("-" * len(s))


def print_timings():
    timings.sort(key=lambda x: (x[1], -x[2]), reverse=True)
    i = 0
    while timings[:]:
        title, times, elapsed = timings.pop(0)
        s = templ.format(title, str(times), f"{elapsed:.5f}")
        if i > len(timings) - 5:
            print_color(s, color="red")
        else:
            print(s)


def timecall(title, fun, *args, **kw):
    print("{:<50}".format(title), end="")
    sys.stdout.flush()
    t = timer()
    for n in range(TIMES):
        fun(*args, **kw)
        elapsed = timer() - t
        if elapsed > 2:
            break
    print("\r", end="")
    sys.stdout.flush()
    timings.append((title, n + 1, elapsed))


def set_highest_priority():
    """Set highest CPU and I/O priority (requires root)."""
    p = psutil.Process()
    if psutil.WINDOWS:
        p.nice(psutil.HIGH_PRIORITY_CLASS)
    else:
        p.nice(-20)

    if psutil.LINUX:
        p.ionice(psutil.IOPRIO_CLASS_RT, value=7)
    elif psutil.WINDOWS:
        p.ionice(psutil.IOPRIO_HIGH)


def main():
    global TIMES

    parser = argparse.ArgumentParser(
        description=__doc__, formatter_class=argparse.RawTextHelpFormatter
    )
    parser.add_argument('-t', '--times', type=int, default=TIMES)
    args = parser.parse_args()
    TIMES = args.times
    assert TIMES > 1, TIMES

    try:
        set_highest_priority()
    except psutil.AccessDenied:
        prio_set = False
    else:
        prio_set = True

    # --- system

    public_apis = []
    ignore = [
        'wait_procs',
        'process_iter',
        'win_service_get',
        'win_service_iter',
    ]
    if psutil.MACOS:
        ignore.append('net_connections')  # raises AD
    for name in psutil.__all__:
        obj = getattr(psutil, name, None)
        if inspect.isfunction(obj):
            if name not in ignore:
                public_apis.append(name)

    print_header("SYSTEM APIS")
    for name in public_apis:
        fun = getattr(psutil, name)
        args = ()
        if name == 'pid_exists':
            args = (os.getpid(),)
        elif name == 'disk_usage':
            args = (os.getcwd(),)
        timecall(name, fun, *args)
    timecall('cpu_count (cores)', psutil.cpu_count, logical=False)
    timecall('process_iter (all)', lambda: list(psutil.process_iter()))
    print_timings()

    # --- process
    print()
    print_header("PROCESS APIS")
    ignore = [
        'send_signal',
        'suspend',
        'resume',
        'terminate',
        'kill',
        'wait',
        'as_dict',
        'parent',
        'parents',
        'oneshot',
        'pid',
        'rlimit',
        'children',
    ]
    if psutil.MACOS:
        ignore.append('memory_maps')  # XXX
    p = psutil.Process()
    for name in sorted(dir(p)):
        if not name.startswith('_') and name not in ignore:
            fun = getattr(p, name)
            timecall(name, fun)
    print_timings()

    if not prio_set:
        msg = "\nWARN: couldn't set highest process priority "
        msg += "(requires root)"
        print_color(msg, "red")


if __name__ == '__main__':
    main()
