#!/usr/bin/env python3 # vim: tabstop=8 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('gnuplot "%s" 2>/dev/null' % script) 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]]], generations: list[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( '' % css, file=file, ) for i in reports: print( '