import os, sys import profile # Nice labels for algorithms of all kinds ALG_LABEL = { "AES_128_GCM_SHA256": "aes128", "AES_256_GCM_SHA384": "aes256", "CHACHA20_POLY1305_SHA256": "chacha20", "ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,ECDHE_RSA_WITH_AES_128_GCM_SHA256": "aes128", "ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,ECDHE_RSA_WITH_AES_256_GCM_SHA384": "aes256", "ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256,ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256": "chacha20", "prime256v1": "p256", "secp384r1": "p384", "rsa2048": "rsa2048", "rsa3072": "rsa3072", "rsa4096": "rsa4096", "X25519": "x25519", "SECP256R1": "p256", "SECP384R1": "p384", "X25519MLKEM768": "x25519mlkem", "SECP256R1MLKEM768": "p256mlkem", "MLKEM768": "mlkem", "0": "Off", "1": "On", } # Nice labels for TLS versions using ciphers VER_LABEL = { "AES_128_GCM_SHA256": "1.3", "AES_256_GCM_SHA384": "1.3", "CHACHA20_POLY1305_SHA256": "1.3", "ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,ECDHE_RSA_WITH_AES_128_GCM_SHA256": "1.2", "ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,ECDHE_RSA_WITH_AES_256_GCM_SHA384": "1.2", "ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256,ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256": "1.2" } # Titles for measured quantities OBJ_TITLE = { "cpu": "CPU time", "energy": "energy consumption", "profile": "time profile", } # Logfile column names COL = { "cpu": "cpu", "energy": "Wh", "cipher": "cipher", "cert": "alg", "kex": "kex", "ed": "ed", "side": "side", "record": "record", } # Physical units by object UNIT = { "cpu": "s", "energy": "J", "profile": "samples", } # Titles for criteria CRITERION_TITLE = { "cipher": "cipher", "cert": "signature algorithm", "kex": "key exchange", "ed": "0-RTT", } def impl_title(impl): # Gnuplot does not escape underscores when generating tex return impl.replace("_", "-") # Where gnuplot files, data files and images are output PLOTS_DIR = "/dev/shm/plots" # Energy per byte on the network, in W/s # Estimated at 0.06 kWh/GB by Aslan 2018 #WS_PER_BYTE = 0.06 * 1000 * 3600 / 1024**3 WS_PER_BYTE = 0.001875 * 1000 * 3600 / 1024**3 def gnuplot_histogram(**kwargs): if "machine" in kwargs and kwargs["machine"] != None: kwargs["machine"] = ", " + kwargs["machine"] else: kwargs["machine"] = "" titleline = "" if kwargs["maketitle"]: titleline = 'set title font "CMU Sans Serif,12" "{object_title} by {criterion_title} ({record}, {side}{machine}) ({unit})"'.format(**kwargs) cluster = "" for i in range(kwargs["nb_impls"]-1): cluster += """, "" using {}:xticlabels(1) title col""".format(i+4) f = open("{plots_dir}/{object}_by_{criterion}_{side}_{record}.gnuplot".format(plots_dir=PLOTS_DIR, **kwargs), "w") f.write("""\ set terminal pngcairo enhanced font "CMU Sans Serif,11" fontscale 1.0 size 800, 600 set output "{plots_dir}/{object}_by_{criterion}_{side}_{record}.png" set boxwidth 0.9 absolute set style fill solid 1.0 border lt -1 set style histogram clustered gap 1 title textcolor lt -1 set style data histograms {titleline} #set xtics border in scale 0,0 nomirror rotate by -45 autojustify set xtics border in scale 0,0 nomirror autojustify #set key fixed right top vertical Right noreverse noenhanced autotitle nobox set key fixed left top vertical Left reverse noenhanced autotitle nobox set colorbox vertical origin screen 0.9, 0.2 size screen 0.05, 0.6 front noinvert bdefault set xrange [ * : * ] noreverse writeback set yrange [ 0 : * ] set grid y lt 1 lw .75 lc "gray" plot \ newhistogram "", "{plots_dir}/{object}_by_{criterion}_{side}_{record}.dat" using 2:xticlabels(1) notitle col, \ newhistogram "", "{plots_dir}/{object}_by_{criterion}_{side}_{record}.dat" using 3:xticlabels(1) title col{cluster} set term cairolatex pdf set output "{plots_dir}/{object}_by_{criterion}_{side}_{record}.tex" replot """.format(plots_dir=PLOTS_DIR, cluster=cluster, titleline=titleline, **kwargs)) f.close() os.system("gnuplot {plots_dir}/{object}_by_{criterion}_{side}_{record}.gnuplot".format(plots_dir=PLOTS_DIR, **kwargs)) def gnuplot_stacked_histogram(**kwargs): if "machine" in kwargs and kwargs["machine"] != None: kwargs["machine"] = ", " + kwargs["machine"] else: kwargs["machine"] = "" titleline = "" if kwargs["maketitle"]: titleline = 'set title font "CMU Sans Serif,12" "{object_title} by {criterion_title} ({record}, {side}{machine}) ({unit})"'.format(**kwargs) f = open("{plots_dir}/{object}_by_{criterion}_{side}_{record}.gnuplot".format(plots_dir=PLOTS_DIR, **kwargs), "w") f.write("""\ set terminal pngcairo enhanced font "CMU Sans Serif,11" fontscale 1.0 size 800, 600 set output "{plots_dir}/{object}_by_{criterion}_{side}_{record}.png" set boxwidth 0.9 absolute set style fill solid 1.0 border lt -1 set style histogram rowstacked set style data histograms {titleline} set xtics border in scale 0,0 nomirror noenhanced rotate by 30 right set lmargin 9 set rmargin 1 set bmargin 5 set tmargin 2.5 set key fixed left top vertical Left noenhanced autotitle nobox invert reverse opaque set colorbox vertical origin screen 0.9, 0.2 size screen 0.05, 0.6 front noinvert bdefault set xrange [ * : * ] noreverse writeback set yrange [ 0 : * ] set grid y lt 1 lw .75 lc "gray" plot for [i=2:{nb_functions}] "{plots_dir}/{object}_by_{criterion}_{side}_{record}.dat" using i:xticlabels(1) title col #set term cairolatex pdf set term pict2e font ",10" set output "{plots_dir}/{object}_by_{criterion}_{side}_{record}.tex" set key font ",10" spacing 0.8 replot """.format(plots_dir=PLOTS_DIR, titleline=titleline, **kwargs).replace("aws_lc", "aws-lc")) f.close() os.system("gnuplot {plots_dir}/{object}_by_{criterion}_{side}_{record}.gnuplot".format(plots_dir=PLOTS_DIR, **kwargs)) def gnuplot_stacked_histogram_grouped(**kwargs): if "machine" in kwargs and kwargs["machine"] != None: kwargs["machine"] = ", " + kwargs["machine"] else: kwargs["machine"] = "" titleline = "" if kwargs["maketitle"]: titleline = 'set title font "CMU Sans Serif,12" "{object_title} by {criterion_title} ({record}, {side}{machine}) ({unit})"'.format(**kwargs) cluster = "" cluster_i = 0 for group in kwargs["groups"]: if cluster_i > 0: cluster += ",\\\n\t" cluster += f' newhistogram "{group}"' for i in range(3, kwargs["nb_functions"]+2): if i == 3: cluster += ',\\\n\t "{plots_dir}/{object}_by_{criterion}_{side}_{record}_{group}.dat"'.format(group=group, plots_dir=PLOTS_DIR, **kwargs) else: cluster += ',\\\n\t ""' if cluster_i == 0: cluster += f' using {i}:xticlabels(2) title col' else: cluster += f' using {i}:xticlabels(2) notitle' cluster_i += 1 f = open("{plots_dir}/{object}_by_{criterion}_{side}_{record}_{group_by}.gnuplot".format(plots_dir=PLOTS_DIR, **kwargs), "w") f.write("""\ set terminal pngcairo enhanced font "CMU Sans Serif,11" fontscale 1.0 size 800, 600 set output "{plots_dir}/{object}_by_{criterion}_{side}_{record}_{group_by}.png" set boxwidth 0.9 absolute set style fill solid 1.0 border lt -1 set style histogram rowstacked gap 0 set style data histograms {titleline} set xtics border in scale 0,0 nomirror noenhanced rotate by 30 right set lmargin 9 set rmargin 1 set bmargin 5 set tmargin 2.5 set key fixed left top vertical Left noenhanced nobox invert reverse opaque set colorbox vertical origin screen 0.9, 0.2 size screen 0.05, 0.6 front noinvert bdefault set xrange [ * : * ] noreverse writeback set yrange [ 0 : * ] set grid y lt 1 lw .75 lc "gray" plot{cluster} #set term cairolatex pdf set term pict2e font ",10" set output "{plots_dir}/{object}_by_{criterion}_{side}_{record}_{group_by}.tex" set key font ",10" spacing 0.8 replot """.format(plots_dir=PLOTS_DIR, cluster=cluster, titleline=titleline, **kwargs).replace("aws_lc", "aws-lc")) f.close() os.system("gnuplot {plots_dir}/{object}_by_{criterion}_{side}_{record}_{group_by}.gnuplot".format(plots_dir=PLOTS_DIR, **kwargs)) def make_log_plot(logs, exp, criterion, side, obj, record, machine=None, version=None, maketitle=True): f = open(f"/dev/shm/plots/{obj}_by_{criterion}_{side}_{record}.dat", "w") ciphers = {} impls = [] plain_line = None idle_val = None conv_factor = 1.0 if obj == "energy": # Convert Wh to Ws conv_factor = 3600.0 for log in logs: if log["exp"] == "idle": idle_val = float(log[COL[obj]]) / float(log["time"]) * conv_factor if log["exp"] != exp or log["record"] != record: continue if log["side"] == side and log["tls"] == "0": n = float(log.get("n", "1000")) plain_line = "plain {}".format((float(log[COL[obj]]) * conv_factor - idle_val * float(log["time"])) / n) if plain_line == None: return for log in logs: if log["exp"] == exp and log["record"] == record and log["side"] == side: if version != None and VER_LABEL[log["cipher"]] != version: continue n = float(log.get("n", "1000")) if version == None: ver = VER_LABEL[log["cipher"]] if log[COL[criterion]]+"/"+ver not in ciphers: ciphers[log[COL[criterion]]+"/"+ver] = {} ciphers[log[COL[criterion]]+"/"+ver][log["impl"]] = (float(log[COL[obj]]) * conv_factor - idle_val * float(log["time"])) / n else: if log[COL[criterion]] not in ciphers: ciphers[log[COL[criterion]]] = {} ciphers[log[COL[criterion]]][log["impl"]] = (float(log[COL[obj]]) * conv_factor - idle_val * float(log["time"])) / n if log["impl"] not in impls: impls.append(log["impl"]) impls.sort() f.write("{} none {}\n".format(criterion, " ".join([impl_title(impl) for impl in impls]))) f.write(plain_line+" -"*len(impls)+"\n") for cipher in ciphers: for impl in impls: if impl not in ciphers[cipher]: ciphers[cipher][impl] = 0 if version == None: cipher_parts = cipher.split("/") f.write("{}({}) - {}\n".format( ALG_LABEL[cipher_parts[0]], cipher_parts[1], " ".join([ str(ciphers[cipher][impl]) for impl in impls ]), )) else: f.write("{} - {}\n".format( ALG_LABEL[cipher], " ".join([ str(ciphers[cipher][impl]) for impl in impls ]), )) f.close() gnuplot_histogram( object=obj, criterion=criterion, side=side, object_title=OBJ_TITLE[obj], criterion_title=CRITERION_TITLE[criterion], unit=UNIT[obj], nb_impls=len(impls), record=record, machine=machine, maketitle=maketitle ) def make_log_plot_points(logs_by_target, exp, criterion, side, obj, record): f = open(f"/dev/shm/plots/{obj}_by_{criterion}_{side}_{record}.dat", "w") ciphers = {target: {} for target in logs_by_target} impls = [] plain = {target: None for target in logs_by_target} idle_val = {target: None for target in logs_by_target} conv_factor = 1.0 if obj == "energy": # Convert Wh to Ws conv_factor = 3600.0 for target in logs_by_target: logs = logs_by_target[target] for log in logs: if log["exp"] == "idle": idle_val[target] = float(log[COL[obj]]) / float(log["time"]) * conv_factor if log["exp"] != exp or log["record"] != record: continue if log["side"] == side and log["tls"] == "0": n = float(log.get("n", "1000")) plain[target] = (float(log[COL[obj]]) * conv_factor - idle_val[target] * float(log["time"])) / n for target in plain: if plain[target] == None: return for target in logs_by_target: logs = logs_by_target[target] for log in logs: if log["exp"] == exp and log["record"] == record and log["side"] == side: if version != None and VER_LABEL[log["cipher"]] != version: continue n = float(log.get("n", "1000")) if version == None: ver = VER_LABEL[log["cipher"]] if log[COL[criterion]]+"/"+ver not in ciphers[target]: ciphers[target][log[COL[criterion]]+"/"+ver] = {} ciphers[target][log[COL[criterion]]+"/"+ver][log["impl"]] = (float(log[COL[obj]]) * conv_factor - idle_val[target] * float(log["time"])) / n else: if log[COL[criterion]] not in ciphers[target]: ciphers[target][log[COL[criterion]]] = {} ciphers[target][log[COL[criterion]]][log["impl"]] = (float(log[COL[obj]]) * conv_factor - idle_val[target] * float(log["time"])) / n if log["impl"] not in impls: impls.append(log["impl"]) impls.sort() f.write("target {} none {}\n".format(criterion, " ".join([impl_title(impl) for impl in impls]))) for target in plain: for cipher in ciphers[target]: for impl in impls: if impl not in ciphers[target][cipher]: ciphers[target][cipher][impl] = 0 cipher_parts = cipher.split("/") f.write("{} {}({}) {} {}\n".format( target, ALG_LABEL[cipher_parts[0]], cipher_parts[1], plain[target], " ".join([ str(ciphers[target][cipher][impl]) for impl in impls ]), )) f.close() def make_profile_plot(logs, exp, criterion, side, record, no_flamegraph=False, machine=None, version=None, maketitle=False): f = open(f"/dev/shm/plots/profile_by_{criterion}_{side}_{record}.dat", "w") runs = [] functions = [] for log in logs: if version != None and VER_LABEL[log["cipher"]] != version: continue if log["exp"] == exp and log["record"] == record and log["side"] == side and log["tls"] == "1": svg_filename = log["prof"] + ".svg" if not no_flamegraph: os.system("flamegraph --perfdata {} -o {}".format(log["prof"], svg_filename)) try: profile_results = profile.extract_from_file(svg_filename) except FileNotFoundError: print(f"Cannot read {svg_filename}") continue print(profile_results) for function in profile_results: if function not in functions: functions.append(function) runs.append({ criterion: log[COL[criterion]], "impl": log["impl"], "functions": profile_results, }) f.write("{} {}\n".format(criterion, " ".join(functions))) for run in runs: """f.write("\"{} {}({})\" {}\n".format( run["impl"], ALG_LABEL[run[criterion]], VER_LABEL[log["cipher"]], " ".join([ str(run["functions"][function][0]) for function in functions ]), ))""" f.write("\"{} {}\" {}\n".format( impl_title(run["impl"]), ALG_LABEL[run[criterion]], " ".join([ str(run["functions"][function][0]) for function in functions ]), )) f.close() gnuplot_stacked_histogram( object="profile", criterion=criterion, side=side, object_title=OBJ_TITLE["profile"], criterion_title=CRITERION_TITLE[criterion], unit=UNIT["profile"], record=record, nb_functions=len(functions)+1, machine=machine, maketitle=maketitle ) def make_profile_plot_grouped(logs, exp, criterion, side, record, group_by, no_flamegraph=False, machine=None, version=None, maketitle=False): runs = [] functions = [] files = {} for log in logs: if version != None and VER_LABEL[log["cipher"]] != version: continue if log["exp"] == exp and log["record"] == record and log["side"] == side and log["tls"] == "1": svg_filename = log["prof"] + ".svg" if not no_flamegraph: os.system("flamegraph --perfdata {} -o {}".format(log["prof"], svg_filename)) try: profile_results = profile.extract_from_file(svg_filename) except FileNotFoundError: print(f"Cannot read {svg_filename}") continue print(profile_results) for function in profile_results: if function not in functions: functions.append(function) log_group = log[group_by] runs.append({ criterion: log[COL[criterion]], "impl": log["impl"], "group": log_group, "functions": profile_results, }) if log_group not in files: files[log_group] = open(f"/dev/shm/plots/profile_by_{criterion}_{side}_{record}_{log_group}.dat", "w") for group in files: files[group].write("{} {} {}\n".format(group_by, criterion, " ".join(functions))) for run in runs: files[run["group"]].write("{} {} {}\n".format( run["group"], ALG_LABEL[run[criterion]], " ".join([ str(run["functions"][function][0]) for function in functions ]), )) for group in files: files[group].close() gnuplot_stacked_histogram_grouped( object="profile", criterion=criterion, side=side, object_title=OBJ_TITLE["profile"], criterion_title=CRITERION_TITLE[criterion], unit=UNIT["profile"], record=record, nb_functions=len(functions)+1, machine=machine, maketitle=maketitle, group_by=group_by, groups=[group for group in files] ) # Are CPU and energy proportional def make_linear_regression(logs, cat="impl"): idle_cpu = None idle_energy = None for log in logs: if log["exp"] == "idle": idle_cpu = float(log["cpu"]) / float(log["time"]) idle_energy = float(log["Wh"]) / float(log["time"]) break samples_cpu = {"global":[]} samples_energy = {"global":[]} for log in logs: if log["impl"] == "-": continue sample_cpu = float(log["cpu"]) - idle_cpu * float(log["time"]) sample_energy = float(log["Wh"]) - idle_energy * float(log["time"]) samples_cpu["global"].append(sample_cpu) samples_energy["global"].append(sample_energy) if log[cat] not in samples_cpu: samples_cpu[log[cat]] = [] samples_energy[log[cat]] = [] samples_cpu[log[cat]].append(sample_cpu) samples_energy[log[cat]].append(sample_energy) print("Pearson correlation coefficients (energy/cpu)") results = {} for impl in samples_cpu: res = stats.linregress(samples_cpu[impl], samples_energy[impl]) print(impl, "\t", res.rvalue) results[impl] = res if impl != "global": plt.plot(samples_cpu[impl], samples_energy[impl], 'o', label=impl) #plt.plot(samples_cpu["global"], samples_energy["global"], 'o', label='samples') plt.plot(samples_cpu["global"], res.intercept + res.slope*np.array(samples_cpu["global"]), 'r', label='fitted line') plt.xlabel("CPU (s)") plt.ylabel("Energy (Wh)") plt.legend() #plt.show() plt.savefig(f"{PLOTS_DIR}/correlation_energy_cpu.png") # Measure relative difference between TLS versions def cmp_versions(logs, exps, criteria, objs): ciphers = {} idle_val = None for log in logs: if log["exp"] == "idle": idle_val = {obj:float(log[COL[obj]]) / float(log["time"]) for obj in objs} for log in logs: if log["exp"] not in exps or log["tls"] == "0": continue ver = VER_LABEL[log["cipher"]] if ver not in ciphers: ciphers[ver] = {} key = [] for criterion in criteria: key.append(ALG_LABEL.get(log[COL[criterion]], log[COL[criterion]])) key = "/".join(key) if key not in ciphers[ver]: ciphers[ver][key] = log diff_rel_max = {obj:0.0 for obj in objs} diff_rel_sum = {obj:0.0 for obj in objs} diff_rel_num = {obj:0 for obj in objs} for key in ciphers["1.2"]: if key not in ciphers["1.3"]: continue log12 = ciphers["1.2"][key] log13 = ciphers["1.3"][key] for obj in objs: val12 = float(log12[COL[obj]]) - idle_val[obj] * float(log12["time"]) val13 = float(log13[COL[obj]]) - idle_val[obj] * float(log13["time"]) # Difference relative to the mean of the two values try: diff_rel = abs(val12 - val13) / ((val12 + val13) / 2) except ZeroDivisionError: continue diff_rel_max[obj] = max(diff_rel_max[obj], diff_rel) diff_rel_sum[obj] += diff_rel diff_rel_num[obj] += 1 print("Diff rel max: ", diff_rel_max) try: diff_rel_avg = {obj:diff_rel_sum[obj]/diff_rel_num[obj] for obj in objs} print("Diff rel avg: ", diff_rel_avg) except ZeroDivisionError: pass def tabulate(lines, separator=" ", endline="", ruler=True): widths = [0] * len(lines[0]) for line in lines: for col in range(len(line)): if len(line[col]) > widths[col]: widths[col] = len(line[col]) table = "" ruler_state = False for line in lines: for col in range(len(line)): if col > 0: table += separator table += line[col] rem = widths[col] - len(line[col]) if ruler_state: table += " " * (rem % 4) + " ยท " * (rem // 4) else: table += " " * rem table += endline+"\n" ruler_state ^= ruler return table def make_summary(logs): plain_result_key = lambda log: (log["exp"], log["side"], log["record"]) result_key = lambda log: (log["exp"], log["side"], log["record"], log["impl"], log["alg"], log["kex"], log["cipher"], log["ed"]) plain_results = {} results = {} idle_val = None for log in logs: if log["exp"] == "idle": idle_val = { "cpu": float(log["cpu"]) / float(log["time"]), "energy": float(log["Wh"]) / float(log["time"]) * 3600, "in": float(log["bytes_in"]) / float(log["time"]), "out": float(log["bytes_out"]) / float(log["time"]), } if log["tls"] == "0": n = float(log.get("n", "1000")) plain_results[plain_result_key(log)] = { "cpu": (float(log["cpu"]) - idle_val["cpu"] * float(log["time"])) / n, "total_energy": float(log["Wh"]) * 3600 / n, "energy": (float(log["Wh"]) * 3600 - idle_val["energy"] * float(log["time"])) / n, "in": (float(log["bytes_in"]) - idle_val["in"] * float(log["time"])) / n, "out": (float(log["bytes_out"]) - idle_val["out"] * float(log["time"])) / n, } if log["exp"] != "idle" and log["tls"] == "1": n = float(log.get("n", "1000")) results[result_key(log)] = { "cpu": (float(log["cpu"]) - idle_val["cpu"] * float(log["time"])) / n, "total_energy": float(log["Wh"]) * 3600 / n, "energy": (float(log["Wh"]) * 3600 - idle_val["energy"] * float(log["time"])) / n, "in": (float(log["bytes_in"]) - idle_val["in"] * float(log["time"])) / n, "out": (float(log["bytes_out"]) - idle_val["out"] * float(log["time"])) / n, } lines = [[ "key", "idle (W)", "no_tls (Ws/S)", "tls (Ws/S)", "tls_min (Ws/S)", #"tls_max (Ws/S)", "tls_in (MB/S)", "tls_out (MB/S)", "tls_io (MB/S)", "tls_net (Ws/S)", ]] for k in results: r = results[k] p = plain_results[k[:3]] lines.append([ "/".join([str(i) for i in k]), str(round(idle_val["energy"], 1)), str(round(p["energy"], 1)), str(round(r["energy"], 1)), str(round(r["energy"] - p["energy"], 1)) + " (" + str(round((r["energy"] - p["energy"])/r["energy"]*100, 1)) + "%)", #str(round(r["total_energy"] - p["total_energy"], 1)), str(round((r["in"] - p["in"])/1024**2, 2)) + " (" + str(round((r["in"] - p["in"])/r["in"]*100, 1)) + "%)", str(round((r["out"] - p["out"])/1024**2, 2)) + " (" + str(round((r["out"] - p["out"])/r["out"]*100, 1)) + "%)", str(round((r["in"] + r["out"] - p["in"] - p["out"])/1024**2, 2)) + " (" + str(round((r["in"] + r["out"] - p["in"] - p["out"])/(r["in"] + r["out"])*100, 1)) + "%)", str(round(r["energy"] - p["energy"] + (r["out"] + r["in"] - p["out"] - p["in"]) * WS_PER_BYTE, 2)), ]) print(tabulate(lines)) def analyze_logs(logs): plain_result_key = lambda log: (log["exp"], log["side"], log["record"]) result_key = lambda log: (log["exp"], log["side"], log["record"], log["impl"], log["alg"], log["kex"], log["cipher"], log["ed"]) plain_results = {} results = {} idle_val = None for log in logs: if log["exp"] == "idle": idle_val = { "cpu": float(log["cpu"]) / float(log["time"]), "energy": float(log["Wh"]) / float(log["time"]) * 3600, "in": float(log["bytes_in"]) / float(log["time"]), "out": float(log["bytes_out"]) / float(log["time"]), } if log["tls"] == "0": n = float(log.get("n", "1000")) plain_results[plain_result_key(log)] = { "cpu": (float(log["cpu"]) - idle_val["cpu"] * float(log["time"])) / n, "total_energy": float(log["Wh"]) * 3600 / n, "energy": (float(log["Wh"]) * 3600 - idle_val["energy"] * float(log["time"])) / n, "in": (float(log["bytes_in"]) - idle_val["in"] * float(log["time"])) / n, "out": (float(log["bytes_out"]) - idle_val["out"] * float(log["time"])) / n, } if log["exp"] != "idle" and log["tls"] == "1": n = float(log.get("n", "1000")) results[result_key(log)] = { "exp": log["exp"], "side": log["side"], "record": log["record"], "impl": log["impl"], "alg": log["alg"], "kex": log["kex"], "cipher": log["cipher"], "ed": log["ed"], "cpu": (float(log["cpu"]) - idle_val["cpu"] * float(log["time"])) / n, "total_energy": float(log["Wh"]) * 3600 / n, "energy": (float(log["Wh"]) * 3600 - idle_val["energy"] * float(log["time"])) / n, "in": (float(log["bytes_in"]) - idle_val["in"] * float(log["time"])) / n, "out": (float(log["bytes_out"]) - idle_val["out"] * float(log["time"])) / n, } lines = [] for k in results: r = results[k] p = plain_results[k[:3]] lines.append({ "exp": r["exp"], "side": r["side"], "record": r["record"], "impl": r["impl"], "alg": r["alg"], "kex": r["kex"], "cipher": r["cipher"], "ed": r["ed"], "tls": r, "plain": p, }) return lines def aggregate(items, unique, constraints=[]): results = {} for item in items: satisfied = True for constraint in constraints: if type(constraints[constraint]) == list: if item[constraint] not in constraints[constraint]: satisfied = False break else: if item[constraint] != constraints[constraint]: satisfied = False break if not satisfied: continue unique_key = tuple(item[u] for u in unique) if unique_key not in results: results[unique_key] = {u: item[u] for u in unique} results[unique_key]["items"] = [item] else: results[unique_key]["items"].append(item) return results def fzfill(x, n): if type(x) != str: x = str(round(x, n)) return x + "0" * max(0, n - len(x) + 1 + x.find(".")) def make_tx_summary(client_logs, server_logs): client_results = analyze_logs(client_logs) server_results = analyze_logs(server_logs) txs = [] lines = [["exp", "record", "alg", "kex", "cipher", "ed", "client", "server", "plain (J/S)", "TLS (J/S)", "TLS only (J/S)", "TLS rel"]] for client in client_results: #print("client", client) if client["side"] != "client": continue for server in server_results: #print("server", server) if server["side"] != "server": continue #print(client, server) if client["exp"] == server["exp"] \ and client["record"] == server["record"] \ and client["alg"] == server["alg"] \ and client["kex"] == server["kex"] \ and client["cipher"] == server["cipher"] \ and client["ed"] == server["ed"]: plain_io_avg = (client["plain"]["in"] + client["plain"]["out"] + server["plain"]["in"] + server["plain"]["out"]) / 2 tls_io_avg = (client["tls"]["in"] + client["tls"]["out"] + server["tls"]["in"] + server["tls"]["out"]) / 2 tls_only_io_avg = tls_io_avg - plain_io_avg plain_energy = client["plain"]["energy"] + server["plain"]["energy"] + plain_io_avg * WS_PER_BYTE tls_energy = client["tls"]["energy"] + server["tls"]["energy"] + tls_io_avg * WS_PER_BYTE tls_only_energy = tls_energy - plain_energy tls_rel_energy = tls_only_energy / plain_energy txs.append({ "exp": client["exp"], "record": client["record"], "alg": client["alg"], "kex": client["kex"], "cipher": client["cipher"], "ed": client["ed"], "version": VER_LABEL[client["cipher"]], "client_impl": client["impl"], "server_impl": server["impl"], "plain_energy": plain_energy, "tls_energy": tls_energy, "tls_only_energy": tls_only_energy, "tls_rel_energy": tls_rel_energy, }) lines.append([ client["exp"], client["record"], client["alg"], client["kex"], client["cipher"], client["ed"], client["impl"], server["impl"], str(round(plain_energy, 2)), str(round(tls_energy, 2)), str(round(tls_only_energy, 2)), str(round(tls_rel_energy*100, 2))+"%", ]) print(tabulate(lines)) aggregated = aggregate(txs, ["exp", "record", "alg"], {"server_impl": ["aws-lc"], "client_impl": ["aws-lc"]}) latex = [] for cluster_k in aggregated: cluster = aggregated[cluster_k] items_to_find = {"kex": ["X25519", "X25519MLKEM768"], "cipher": []} items_to_find = { "1.2": {"kex": "X25519", "cipher": "ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,ECDHE_RSA_WITH_AES_256_GCM_SHA384"}, "ec": {"kex": "X25519", "cipher": "AES_256_GCM_SHA384", "ed": "0"}, "pq": {"kex": "X25519MLKEM768", "ed": "0"}, "ec+ed": {"kex": "X25519", "cipher": "AES_256_GCM_SHA384", "ed": "1"}, "pq+ed": {"kex": "X25519MLKEM768", "ed": "1"}, } items = {} for k in items_to_find: for item in cluster["items"]: ok = True for kk in items_to_find[k]: if item[kk] != items_to_find[k][kk]: ok = False break # named breaks please! if ok: items[k] = item latex.append([ " ", cluster["record"], "$"+str(round(items["ec"]["plain_energy"], 2))+"$", ("$" + str(fzfill(items["1.2"]["tls_only_energy"], 2)) + "$ ($" + str(fzfill(items["1.2"]["tls_rel_energy"]*100, 2)) + "\\%$)") if "1.2" in items else "", "$" + str(fzfill(items["ec"]["tls_only_energy"], 2)) + "$ ($" + str(fzfill(items["ec"]["tls_rel_energy"]*100, 2)) + "\\%$)", ("$" + str(fzfill(items["pq"]["tls_only_energy"], 2)) + "$ ($" + str(fzfill(items["pq"]["tls_rel_energy"]*100, 2)) + "\\%$)") if "pq" in items else "", ("$" + str(fzfill(items["ec+ed"]["tls_only_energy"], 2)) + "$ ($" + str(fzfill(items["ec+ed"]["tls_rel_energy"]*100, 2)) + "\\%$)") if "ec+ed" in items else "", ("$" + str(fzfill(items["pq+ed"]["tls_only_energy"], 2)) + "$ ($" + str(fzfill(items["pq+ed"]["tls_rel_energy"]*100, 2)) + "\\%$)") if "pq+ed" in items else "", ]) print(tabulate(latex, separator=" & ", endline="\\\\ \\cline{2-8}", ruler=False)) def getargv(arg:str, default="", n:int=1, args:list=sys.argv): if arg in args and len(args) > args.index(arg)+n: return args[args.index(arg)+n] else: return default def parse_logs(logfile_name): logfile = open(logfile_name, "r") lines = logfile.readlines() logfile.close() colnames = lines[0].removesuffix("\n").split(" ") logs = [] records = {} for line in lines[1:]: cols = line.removesuffix("\n").split(" ") log = {} for col in range(len(cols)): log[colnames[col]] = cols[col] if log["record"] != "-": records[log["record"]] = () logs.append(log) return (logs, records) if __name__ == "__main__": cmd = sys.argv[1] logfile_name = sys.argv[2] logs, records = parse_logs(logfile_name) os.makedirs("/dev/shm/plots", exist_ok=True) no_flamegraph = "-f" in sys.argv machine = getargv("-m", None) maketitle = "-t" not in sys.argv version = getargv("-v", None) if cmd == "log": for side in ["client", "server"]: for record in records: make_log_plot(logs, "impl-cipher-ver", "cipher", side, "cpu", record, machine=machine, maketitle=maketitle, version=version) make_log_plot(logs, "impl-cipher-ver", "cipher", side, "energy", record, machine=machine, maketitle=maketitle, version=version) make_log_plot(logs, "impl-cert-ver", "cert", side, "cpu", record, machine=machine, maketitle=maketitle, version=version) make_log_plot(logs, "impl-cert-ver", "cert", side, "energy", record, machine=machine, maketitle=maketitle, version=version) make_log_plot(logs, "impl-kex-ver", "kex", side, "cpu", record, machine=machine, maketitle=maketitle, version=version) make_log_plot(logs, "impl-kex-ver", "kex", side, "energy", record, machine=machine, maketitle=maketitle, version=version) make_log_plot(logs, "zrtt", "ed", side, "cpu", record, machine=machine, maketitle=maketitle, version="1.3") make_log_plot(logs, "zrtt", "ed", side, "energy", record, machine=machine, maketitle=maketitle, version="1.3") cmp_versions(logs, ["impl-cipher-ver", "impl-cert-ver", "impl-kex-ver"], ["side", "cipher", "cert", "kex", "record"], ["cpu", "energy"]) elif cmd == "log2": # Args: log2 [ ] [...] # Only use the intersection of the records logs_by_target = {sys.argv[3]: logs} records_to_remove = {} for i in range(4, len(sys.argv), 2): logs_i, records_i = parse_logs(sys.argv[i]) for record in records: if record not in records_i: records_to_remove[record] = () target_i = sys.argv[i+1] logs_by_target[target_i] = logs_i for record in records_to_remove: records.pop(record) for side in ["client", "server"]: for record in records: make_log_plot_points(logs_by_target, "impl-cipher-ver", "cipher", side, "energy", record) make_log_plot_points(logs_by_target, "impl-cert-ver", "cert", side, "energy", record) make_log_plot_points(logs_by_target, "impl-kex-ver", "kex", side, "energy", record) cmp_versions(logs, ["impl-cipher-ver", "impl-cert-ver", "impl-kex-ver"], ["side", "cipher", "cert", "kex", "record"], ["cpu", "energy"]) elif cmd == "prof": for side in ["client", "server"]: for record in records: #make_profile_plot(logs, "impl-cipher-ver", "cipher", side, record, no_flamegraph=no_flamegraph, machine=machine, maketitle=maketitle, version="1.3") #make_profile_plot(logs, "impl-cert-ver", "cert", side, record, no_flamegraph=no_flamegraph, machine=machine, maketitle=maketitle, version="1.3") #make_profile_plot(logs, "impl-kex-ver", "kex", side, record, no_flamegraph=no_flamegraph, machine=machine, maketitle=maketitle, version="1.3") make_profile_plot_grouped(logs, "impl-cipher-ver", "cipher", side, record, "impl", no_flamegraph=no_flamegraph, machine=machine, maketitle=maketitle, version="1.3") make_profile_plot_grouped(logs, "impl-cert-ver", "cert", side, record, "impl", no_flamegraph=no_flamegraph, machine=machine, maketitle=maketitle, version="1.3") make_profile_plot_grouped(logs, "impl-kex-ver", "kex", side, record, "impl", no_flamegraph=no_flamegraph, machine=machine, maketitle=maketitle, version="1.3") elif cmd == "summary": make_summary(logs) elif cmd == "tx": logfile_name = sys.argv[3] logfile = open(logfile_name, "r") lines = logfile.readlines() logfile.close() colnames = lines[0].removesuffix("\n").split(" ") logs2 = [] records = {} for line in lines[1:]: cols = line.removesuffix("\n").split(" ") log = {} for col in range(len(cols)): log[colnames[col]] = cols[col] if log["record"] != "-": records[log["record"]] = () logs2.append(log) make_tx_summary(logs, logs2) elif cmd == "correl": from scipy import stats import matplotlib.pyplot as plt import numpy as np make_linear_regression(logs, "alg")