#!/usr/bin/env python3
# vim: tabstop=4 expandtab shiftwidth=4 softtabstop=4

# Copyright (c) 2016, Arvid Norberg
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
#
#     * Redistributions of source code must retain the above copyright
#       notice, this list of conditions and the following disclaimer.
#     * Redistributions in binary form must reproduce the above copyright
#       notice, this list of conditions and the following disclaimer in
#       the documentation and/or other materials provided with the distribution.
#     * Neither the name of the author nor the names of its
#       contributors may be used to endorse or promote products derived
#       from this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.

# this script can parse and generate reports from the alert log from a
# libtorrent session

import os
import sys
import math
from multiprocessing.pool import ThreadPool
from pathlib import Path
from argparse import ArgumentParser
from typing import Optional


line_graph = 0
histogram = 1
stacked = 2
diff = 3


def graph_colors() -> list[str]:
    colors: list[str] = []

    pattern = [[0, 0, 1], [0, 1, 0], [1, 0, 0], [1, 0, 1], [0, 1, 1], [1, 1, 0]]
    brightness = [0xD8, 0xBB, 0x60]

    for op in range(3):
        for c in pattern:
            c = [v * brightness[op] for v in c]
            colors.append("#%02x%02x%02x" % (c[0], c[1], c[2]))
    return colors


def gradient_colors(num_colors: int) -> list[str]:
    colors = []
    for i in range(0, num_colors):
        f = i / float(num_colors)
        pi = 3.1415927
        r = max(int(255 * (math.sin(f * pi) + 0.2)), 0)
        g = max(int(255 * (math.sin((f - 0.5) * pi) + 0.2)), 0)
        b = max(int(255 * (math.sin((f + 0.5) * pi) + 0.2)), 0)
        c = "#%02x%02x%02x" % (min(r, 255), min(g, 255), min(b, 255))
        colors.append(c)
    return colors


def plot_fun(script: Path) -> None:
    try:
        ret = os.system(f"gnuplot \"{script}\" 2>/dev/null")
    except Exception as e:
        print("please install gnuplot: sudo apt install gnuplot")
        raise e
    if ret != 0 and ret != 256:
        print("gnuplot failed: %d\n" % ret)
        raise Exception("abort")

    sys.stdout.write(".")
    sys.stdout.flush()


def to_title(key: str) -> str:
    return key.replace("_", " ").replace(".", " - ")


def gen_report(
    output_dir: Path,
    keys: list[str],
    name: str,
    unit: str,
    lines: list[str],
    short_unit: str,
    generation: int,
    log_file: Path,
    options: dict[str, int],
) -> Optional[Path]:
    filename = output_dir / f"{name}_{generation:04d}.png"
    thumb = output_dir / f"{name}_{generation:04d}_thumb.png"

    # don't re-render a graph unless the logfile has changed
    try:
        dst1 = filename.stat()
        dst2 = thumb.stat()
        src = log_file.stat()

        if dst1.st_mtime > src.st_mtime and dst2.st_mtime > src.st_mtime:
            sys.stdout.write(".")
            return None

    except Exception:
        pass

    script = output_dir / f"{name}_{generation:04d}.gnuplot"
    with open(script, "w") as out:
        print("set term png size 1200,700", file=out)
        print('set output "%s"' % filename, file=out)
        if "allow-negative" not in options:
            print("set yrange [0:*]", file=out)
        print("set tics nomirror", file=out)
        print("set key box", file=out)
        print("set key left top", file=out)

        colors = graph_colors()

        try:
            if "gradient" in options:
                colors = gradient_colors(options["gradient"])
        except Exception:
            pass

        first = True
        color = 0
        if options["type"] == histogram:
            binwidth = options["binwidth"]
            numbins = int(options["numbins"])

            print("binwidth=%f" % binwidth, file=out)
            print("set boxwidth binwidth", file=out)
            print("bin(x,width)=width*floor(x/width) + binwidth/2", file=out)
            print("set xrange [0:%f]" % (binwidth * numbins), file=out)
            print('set xlabel "%s"' % unit, file=out)
            print('set ylabel "number"', file=out)

            k = lines[0]
            try:
                column = keys.index(k) + 2
            except Exception:
                print('"%s" not found' % k)
                return None
            print(
                'plot "%s" using (bin($%d,binwidth)):(1.0) smooth freq with boxes'
                % (log_file, column),
                file=out,
            )
            print("", file=out)
            print("", file=out)
            print("", file=out)

        elif options["type"] == stacked:
            print("set xrange [0:*]", file=out)
            print('set ylabel "%s"' % unit, file=out)
            print('set xlabel "time (s)"', file=out)
            print('set format y "%%.1s%%c%s";' % short_unit, file=out)
            print("set style fill solid 1.0 noborder", file=out)
            print("plot", end=" ", file=out)
            graph = ""
            plot_expression = ""
            for k in lines:
                try:
                    column = keys.index(k) + 2
                except Exception:
                    print('"%s" not found' % k)
                    continue
                if not first:
                    plot_expression = ", " + plot_expression
                    graph += "+"
                axis = "x1y1"
                graph += "$%d" % column
                plot_expression = (
                    ' "%s" using 1:(%s) title "%s" axes %s with filledcurves x1 lc rgb "%s"'
                    % (log_file, graph, to_title(k), axis, colors[color % len(colors)])
                    + plot_expression
                )
                first = False
                color += 1
            print(plot_expression, file=out)
        elif options["type"] == diff:
            print("set xrange [0:*]", file=out)
            print('set ylabel "%s"' % unit, file=out)
            print('set xlabel "time (s)"', file=out)
            print('set format y "%%.1s%%c%s";' % short_unit, file=out)
            graph = ""
            title = ""
            for k in lines:
                try:
                    column = keys.index(k) + 2
                except Exception:
                    print('"%s" not found' % k)
                    continue
                if not first:
                    graph += "-"
                    title += " - "
                graph += "$%d" % column
                title += to_title(k)
                first = False
            print(
                'plot "%s" using 1:(%s) title "%s" with step'
                % (log_file, graph, title),
                file=out,
            )
        else:
            print("set xrange [0:*]", file=out)
            print('set ylabel "%s"' % unit, file=out)
            print('set xlabel "time (s)"', file=out)
            print('set format y "%%.1s%%c%s";' % short_unit, file=out)
            print("plot", end=" ", file=out)
            for k in lines:
                try:
                    column = keys.index(k) + 2
                except Exception:
                    print('"%s" not found' % k)
                    continue
                if not first:
                    print(", ", end=" ", file=out)
                axis = "x1y1"
                print(
                    ' "%s" using 1:%d title "%s" axes %s with steps lc rgb "%s"'
                    % (
                        log_file,
                        column,
                        to_title(k),
                        axis,
                        colors[color % len(colors)],
                    ),
                    end=" ",
                    file=out,
                )
                first = False
                color += 1
            print("", file=out)

        print("set term png size 150,100", file=out)
        print('set output "%s"' % thumb, file=out)
        print("set key off", file=out)
        print("unset tics", file=out)
        print('set format x ""', file=out)
        print('set format y ""', file=out)
        print('set xlabel ""', file=out)
        print('set ylabel ""', file=out)
        print('set y2label ""', file=out)
        print("set rmargin 0", file=out)
        print("set lmargin 0", file=out)
        print("set tmargin 0", file=out)
        print("set bmargin 0", file=out)
        print("replot", file=out)
    return script


def gen_html(
    reports: list[tuple[str, str, str, str, list[str], dict[str, int]]],
    num_generations: int,
    output_dir: Path,
) -> None:
    with open(output_dir / "index.html", "w+") as file:

        css = """img { margin: 0}
#head { display: block }
#graphs { white-space:nowrap; }
h1 { line-height: 1; display: inline }
h2 { line-height: 1; display: inline; font-size: 1em; font-weight: normal};"""

        print(
            '<html><head><style type="text/css">%s</style></head><body>' % css,
            file=file,
        )

        for i in reports:
            print(
                '<div id="head"><h1>%s </h1><h2>%s</h2><div><div id="graphs">'
                % (i[0], i[3]),
                file=file,
            )
            for g in range(num_generations):
                print(
                    '<a href="%s_%04d.png"><img src="%s_%04d_thumb.png"></a>'
                    % (i[0], g, i[0], g),
                    file=file,
                )
            print("</div>", file=file)

        print("</body></html>", file=file)


def main(input_file: Path, num_threads: int, output_dir: Path) -> None:
    thread_pool = ThreadPool(num_threads)
    output_dir.mkdir(parents=True, exist_ok=True)
    g = 0
    with open(input_file) as stat, open(output_dir / f"counters.{g:04}.dat", "w+") as data_out:
        print("looking for stats header")
        while True:
            line = stat.readline()
            if not line:
                raise RuntimeError(
                    f"no 'session stats header:' line found in {input_file}"
                )
            if "session stats header:" in line:
                break

        print("found")
        keys = line.split("session stats header:")[1].strip().split(", ")

        idx = 0
        for line in stat:
            if "session stats (" not in line:
                continue
            data_out.write(
                f"{idx}\t" + line.split(" values): ")[1].strip().replace(", ", "\t")
                + "\n"
            )
            idx += 1
            if idx >= 3600:
                g += 1
                data_out.close()
                data_out = open(output_dir / f"counters.{g:04}.dat", "w+")
                idx = 0

    num_generations = g + 1

    reports: list[tuple[str, str, str, str, list[str], dict[str, int]]] = [
        (
            "torrents",
            "num",
            "",
            "number of torrents in different torrent states",
            [
                "ses.num_downloading_torrents",
                "ses.num_seeding_torrents",
                "ses.num_checking_torrents",
                "ses.num_stopped_torrents",
                "ses.num_upload_only_torrents",
                "ses.num_error_torrents",
                "ses.num_queued_seeding_torrents",
                "ses.num_queued_download_torrents",
            ],
            {"type": stacked},
        ),
        (
            "peers",
            "num",
            "",
            "num connected peers",
            ["peer.num_peers_connected", "peer.num_peers_half_open"],
            {"type": stacked},
        ),
        (
            "peers_max",
            "num",
            "",
            "num connected peers",
            ["peer.num_peers_connected", "peer.num_peers_half_open"],
            {},
        ),
        (
            "peer_churn",
            "num",
            "",
            "connecting and disconnecting peers",
            [
                "peer.num_peers_half_open",
                "peer.connection_attempts",
                "peer.boost_connection_attempts",
                "peer.missed_connection_attempts",
                "peer.no_peer_connection_attempts",
            ],
            {},
        ),
        (
            "new_peers",
            "num",
            "",
            "",
            ["peer.incoming_connections", "peer.connection_attempts"],
            {},
        ),
        (
            "connection_attempts",
            "num",
            "",
            "",
            ["peer.connection_attempt_loops", "peer.connection_attempts"],
            {},
        ),
        (
            "pieces",
            "num",
            "",
            "number completed pieces",
            [
                "ses.num_total_pieces_added",
                "ses.num_piece_passed",
                "ses.num_piece_failed",
            ],
            {},
        ),
        (
            "disk_write_queue",
            "Bytes",
            "B",
            "bytes queued up by peers, to be written to disk",
            ["disk.queued_write_bytes"],
            {},
        ),
        (
            "peers_requests",
            "num",
            "",
            "incoming piece request rate",
            [
                "peer.piece_requests",
                "peer.max_piece_requests",
                "peer.invalid_piece_requests",
                "peer.choked_piece_requests",
                "peer.cancelled_piece_requests",
            ],
            {},
        ),
        (
            "peers_upload",
            "num",
            "",
            "number of peers by state wrt. uploading",
            [
                "peer.num_peers_up_disk",
                "peer.num_peers_up_interested",
                "peer.num_peers_up_unchoked_all",
                "peer.num_peers_up_unchoked_optimistic",
                "peer.num_peers_up_unchoked",
                "peer.num_peers_up_requests",
            ],
            {},
        ),
        (
            "peers_download",
            "num",
            "",
            "number of peers by state wrt. downloading",
            [
                "peer.num_peers_down_interested",
                "peer.num_peers_down_unchoked",
                "peer.num_peers_down_requests",
                "peer.num_peers_down_disk",
            ],
            {},
        ),
        (
            "peer_errors",
            "num",
            "",
            "number of peers by error that disconnected them",
            [
                "peer.disconnected_peers",
                "peer.eof_peers",
                "peer.connreset_peers",
                "peer.connrefused_peers",
                "peer.connaborted_peers",
                "peer.perm_peers",
                "peer.buffer_peers",
                "peer.unreachable_peers",
                "peer.broken_pipe_peers",
                "peer.addrinuse_peers",
                "peer.no_access_peers",
                "peer.invalid_arg_peers",
                "peer.aborted_peers",
            ],
            {"type": stacked},
        ),
        (
            "peer_errors_incoming",
            "num",
            "",
            "number of peers by incoming or outgoing connection",
            ["peer.error_incoming_peers", "peer.error_outgoing_peers"],
            {},
        ),
        (
            "peer_errors_transport",
            "num",
            "",
            "number of peers by transport protocol",
            ["peer.error_tcp_peers", "peer.error_utp_peers"],
            {},
        ),
        (
            "peer_errors_encryption",
            "num",
            "",
            "number of peers by encryption level",
            [
                "peer.error_encrypted_peers",
                "peer.error_rc4_peers",
            ],
            {},
        ),
        (
            "incoming requests",
            "num",
            "",
            "incoming 16kiB block requests",
            ["ses.num_incoming_request"],
            {},
        ),
        (
            "waste",
            "downloaded bytes",
            "B",
            "proportion of all downloaded bytes that were wasted",
            [
                "net.recv_failed_bytes",
                "net.recv_redundant_bytes",
                "net.recv_ip_overhead_bytes",
            ],
            {"type": stacked},
        ),
        (
            "waste by source",
            "num wasted bytes",
            "B",
            "what is causing the waste",
            [
                "ses.waste_piece_timed_out",
                "ses.waste_piece_cancelled",
                "ses.waste_piece_unknown",
                "ses.waste_piece_seed",
                "ses.waste_piece_end_game",
                "ses.waste_piece_closing",
            ],
            {"type": stacked},
        ),
        (
            "disk_time",
            "% of total disk job time",
            "%%",
            "proportion of time spent by the disk thread",
            ["disk.disk_read_time", "disk.disk_write_time", "disk.disk_hash_time"],
            {"type": stacked},
        ),
        (
            "disk_queue",
            "blocks (16kiB)",
            "",
            "disk store-buffer size",
            [
                "disk.num_write_jobs",
                "disk.num_read_jobs",
                "disk.num_jobs",
                "disk.queued_disk_jobs",
                "disk.blocked_disk_jobs",
            ],
            {},
        ),
        (
            "disk fences",
            "num",
            "",
            "number of jobs currently blocked by a fence job",
            ["disk.blocked_disk_jobs"],
            {},
        ),
        (
            "fence jobs",
            "num",
            "",
            "active fence jobs per type",
            [
                "disk.num_fenced_move_storage",
                "disk.num_fenced_release_files",
                "disk.num_fenced_delete_files",
                "disk.num_fenced_check_fastresume",
                "disk.num_fenced_save_resume_data",
                "disk.num_fenced_rename_file",
                "disk.num_fenced_stop_torrent",
                "disk.num_fenced_file_priority",
                "disk.num_fenced_clear_piece",
            ],
            {"type": stacked},
        ),
        (
            "disk threads",
            "num",
            "",
            "number of disk threads currently writing",
            ["disk.num_writing_threads", "disk.num_running_threads"],
            {},
        ),
        (
            "connection_type",
            "num",
            "",
            "peers by transport protocol",
            [
                "peer.num_tcp_peers",
                "peer.num_socks5_peers",
                "peer.num_http_proxy_peers",
                "peer.num_utp_peers",
                "peer.num_i2p_peers",
                "peer.num_ssl_peers",
                "peer.num_ssl_socks5_peers",
                "peer.num_ssl_http_proxy_peers",
                "peer.num_ssl_utp_peers",
            ],
            {},
        ),
        # (
        #     "uTP delay",
        #     "buffering delay",
        #     "s",
        #     "network delays measured by uTP",
        #     [
        #         "uTP peak send delay",
        #         "uTP peak recv delay",
        #         "uTP avg send delay",
        #         "uTP avg recv delay",
        #     ],
        # ),
        # (
        #     "uTP send delay histogram",
        #     "buffering delay",
        #     "s",
        #     "send delays measured by uTP",
        #     ["uTP avg send delay"],
        #     {"type": histogram, "binwidth": 0.05, "numbins": 100},
        # ),
        # (
        #     "uTP recv delay histogram",
        #     "buffering delay",
        #     "s",
        #     "receive delays measured by uTP",
        #     ["uTP avg recv delay"],
        #     {"type": histogram, "binwidth": 0.05, "numbins": 100},
        # ),
        (
            "uTP stats",
            "num",
            "",
            "number of uTP events",
            [
                "utp.utp_packet_loss",
                "utp.utp_timeout",
                "utp.utp_packets_in",
                "utp.utp_packets_out",
                "utp.utp_fast_retransmit",
                "utp.utp_packet_resend",
                "utp.utp_samples_above_target",
                "utp.utp_samples_below_target",
                "utp.utp_payload_pkts_in",
                "utp.utp_payload_pkts_out",
                "utp.utp_invalid_pkts_in",
                "utp.utp_redundant_pkts_in",
            ],
            {"type": stacked},
        ),
        (
            "boost.asio messages",
            "num events",
            "",
            "number of messages posted",
            [
                "net.on_read_counter",
                "net.on_write_counter",
                "net.on_tick_counter",
                "net.on_lsd_counter",
                "net.on_lsd_peer_counter",
                "net.on_udp_counter",
                "net.on_accept_counter",
                "net.on_disk_counter",
            ],
            {"type": stacked},
        ),
        (
            "send_buffer_sizes",
            "num",
            "",
            "",
            [
                "sock_bufs.socket_send_size3",
                "sock_bufs.socket_send_size4",
                "sock_bufs.socket_send_size5",
                "sock_bufs.socket_send_size6",
                "sock_bufs.socket_send_size7",
                "sock_bufs.socket_send_size8",
                "sock_bufs.socket_send_size9",
                "sock_bufs.socket_send_size10",
                "sock_bufs.socket_send_size11",
                "sock_bufs.socket_send_size12",
                "sock_bufs.socket_send_size13",
                "sock_bufs.socket_send_size14",
                "sock_bufs.socket_send_size15",
                "sock_bufs.socket_send_size16",
                "sock_bufs.socket_send_size17",
                "sock_bufs.socket_send_size18",
                "sock_bufs.socket_send_size19",
                "sock_bufs.socket_send_size20",
            ],
            {"type": stacked, "gradient": 18},
        ),
        (
            "recv_buffer_sizes",
            "num",
            "",
            "",
            [
                "sock_bufs.socket_recv_size3",
                "sock_bufs.socket_recv_size4",
                "sock_bufs.socket_recv_size5",
                "sock_bufs.socket_recv_size6",
                "sock_bufs.socket_recv_size7",
                "sock_bufs.socket_recv_size8",
                "sock_bufs.socket_recv_size9",
                "sock_bufs.socket_recv_size10",
                "sock_bufs.socket_recv_size11",
                "sock_bufs.socket_recv_size12",
                "sock_bufs.socket_recv_size13",
                "sock_bufs.socket_recv_size14",
                "sock_bufs.socket_recv_size15",
                "sock_bufs.socket_recv_size16",
                "sock_bufs.socket_recv_size17",
                "sock_bufs.socket_recv_size18",
                "sock_bufs.socket_recv_size19",
                "sock_bufs.socket_recv_size20",
            ],
            {"type": stacked, "gradient": 18},
        ),
        (
            "request latency",
            "us",
            "",
            "latency from receiving requests to sending response",
            ["disk.request_latency"],
            {},
        ),
        (
            "disk_read_latency",
            "num",
            "",
            "histogram of disk read-job latency. cumulative counts; the slope of each band is that"
            " bucket's rate over time",
            [
                "disk.disk_read_latency1",
                "disk.disk_read_latency2",
                "disk.disk_read_latency3",
                "disk.disk_read_latency4",
                "disk.disk_read_latency5",
                "disk.disk_read_latency6",
                "disk.disk_read_latency7",
                "disk.disk_read_latency8",
                "disk.disk_read_latency9",
                "disk.disk_read_latency10",
                "disk.disk_read_latency11",
                "disk.disk_read_latency12",
                "disk.disk_read_latency13",
                "disk.disk_read_latency14",
                "disk.disk_read_latency15",
                "disk.disk_read_latency16",
                "disk.disk_read_latency17",
                "disk.disk_read_latency18",
                "disk.disk_read_latency19",
                "disk.disk_read_latency20",
            ],
            {"type": stacked, "gradient": 20},
        ),
        (
            "incoming messages",
            "num",
            "",
            "number of received bittorrent messages, by type",
            [
                "ses.num_incoming_choke",
                "ses.num_incoming_unchoke",
                "ses.num_incoming_interested",
                "ses.num_incoming_not_interested",
                "ses.num_incoming_have",
                "ses.num_incoming_bitfield",
                "ses.num_incoming_request",
                "ses.num_incoming_piece",
                "ses.num_incoming_cancel",
                "ses.num_incoming_dht_port",
                "ses.num_incoming_suggest",
                "ses.num_incoming_have_all",
                "ses.num_incoming_have_none",
                "ses.num_incoming_reject",
                "ses.num_incoming_allowed_fast",
                "ses.num_incoming_ext_handshake",
                "ses.num_incoming_pex",
                "ses.num_incoming_metadata",
                "ses.num_incoming_extended",
            ],
            {"type": stacked},
        ),
        (
            "outgoing messages",
            "num",
            "",
            "number of sent bittorrent messages, by type",
            [
                "ses.num_outgoing_choke",
                "ses.num_outgoing_unchoke",
                "ses.num_outgoing_interested",
                "ses.num_outgoing_not_interested",
                "ses.num_outgoing_have",
                "ses.num_outgoing_bitfield",
                "ses.num_outgoing_request",
                "ses.num_outgoing_piece",
                "ses.num_outgoing_cancel",
                "ses.num_outgoing_dht_port",
                "ses.num_outgoing_suggest",
                "ses.num_outgoing_have_all",
                "ses.num_outgoing_have_none",
                "ses.num_outgoing_reject",
                "ses.num_outgoing_allowed_fast",
                "ses.num_outgoing_ext_handshake",
                "ses.num_outgoing_pex",
                "ses.num_outgoing_metadata",
                "ses.num_outgoing_extended",
            ],
            {"type": stacked},
        ),
        (
            "request in balance",
            "num",
            "",
            "request and piece message balance",
            [
                "ses.num_incoming_request",
                "ses.num_outgoing_piece",
                "ses.num_outgoing_reject",
            ],
            {"type": diff},
        ),
        (
            "request out balance",
            "num",
            "",
            "request and piece message balance",
            [
                "ses.num_outgoing_request",
                "ses.num_incoming_piece",
                "ses.num_incoming_reject",
            ],
            {"type": diff},
        ),
        (
            "piece_picker_invocations",
            "invocations of piece picker",
            "",
            "",
            [
                "picker.reject_piece_picks",
                "picker.unchoke_piece_picks",
                "picker.incoming_redundant_piece_picks",
                "picker.incoming_piece_picks",
                "picker.end_game_piece_picks",
                "picker.snubbed_piece_picks",
                "picker.interesting_piece_picks",
                "picker.hash_fail_piece_picks",
            ],
            {"type": stacked},
        ),
        (
            "piece_picker_loops",
            "loops through piece picker",
            "",
            "",
            [
                "picker.piece_picker_partial_loops",
                "picker.piece_picker_suggest_loops",
                "picker.piece_picker_sequential_loops",
                "picker.piece_picker_reverse_rare_loops",
                "picker.piece_picker_rare_loops",
                "picker.piece_picker_rand_start_loops",
                "picker.piece_picker_rand_loops",
                "picker.piece_picker_busy_loops",
            ],
            {"type": stacked},
        ),
        (
            "async_accept",
            "number of outstanding accept calls",
            "",
            "",
            ["ses.num_outstanding_accept"],
            {},
        ),
        (
            "queued_trackers",
            "number of queued tracker announces",
            "",
            "",
            ["tracker.num_queued_tracker_announces"],
            {},
        ),
        (
            "file_pool_size",
            "file pool sze",
            "",
            "",
            ["disk.file_pool_size"],
            {},
        ),
        (
            "file_pool_misses",
            "file pool cache misses",
            "",
            "",
            ["disk.file_pool_misses", "disk.file_pool_thread_stall", "disk.file_pool_race"],
            {},
        ),
        # (
        #     "picker_full_partials_distribution",
        #     "full pieces",
        #     "",
        #     "",
        #     ["num full partial pieces"],
        #     {"type": histogram, "binwidth": 5, "numbins": 120},
        # ),
        # (
        #     "picker_partials_distribution",
        #     "partial pieces",
        #     "",
        #     "",
        #     ["num downloading partial pieces"],
        #     {"type": histogram, "binwidth": 5, "numbins": 120},
        # ),
        (
            "disk_cache",
            "disk cache size",
            "",
            "",
            [ "disk.cached_blocks", "disk.disk_blocks_in_use", "disk.num_unhashed" ]
        ),
        (
            "disk_io",
            "disk operations",
            "",
            "",
            [ "disk.num_write_ops", "disk.num_read_back", "disk.num_blocks_hashed"]
        ),
    ]

    print("generating graphs")

    scripts = []
    g = 0

    for g in range(num_generations):
        scripts: list[Path] = []
        print(f"[{' ' * len(reports)}] {g:04}\r[", end="", flush=True)
        for rep in reports:
            try:
                options = rep[5]
            except Exception:
                options = {}
            if "type" not in options:
                options["type"] = line_graph

            script = gen_report(
                output_dir,
                keys,
                rep[0],
                rep[1],
                rep[4],
                rep[2],
                g,
                output_dir / f"counters.{g:04}.dat",
                options,
            )
            if script is not None:
                scripts.append(script)

        # run gnuplot on all scripts, in parallel
        thread_pool.map(plot_fun, scripts)
        scripts = []
        print()

    print("\ngenerating html")
    gen_html(reports, num_generations, output_dir)


if __name__ == "__main__":
    p = ArgumentParser()
    p.add_argument(
        "input",
        type=Path,
        help="libtorrent log file to parse. It must include the session_stats log entries",
    )
    p.add_argument(
        "--threads", default=8, type=int, help="The number of threads to run gnuplot in"
    )
    p.add_argument(
        "--output-dir",
        default="./session_stats_report",
        type=Path,
        help="The directory to save the output files in",
    )

    args = p.parse_args()
    main(args.input, args.threads, args.output_dir)
