From 0f52f2813dcbaa0e1ddf42894e77e87d5cc080bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pascal=20Eng=C3=A9libert?= Date: Thu, 30 Apr 2026 15:31:56 +0200 Subject: [PATCH] Point plots --- README.md | 3 +- plots.py | 180 ++++++++++++++++++++++++++++++++++++++++++++++++++---- 2 files changed, 170 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index 4e940eb..bd83da9 100644 --- a/README.md +++ b/README.md @@ -135,8 +135,9 @@ sudo chmod u+s powercap ### Grid5000 ```bash -ssh nancy.g5k +ssh lyon.g5k oarsub -q default -l host=2,walltime=2 -p "wattmeter=YES" -I +oarsub -q default -l host=2,walltime=2 -p "nova" -I # Check the name of the other node in https://intranet.grid5000.fr/oar/Lyon/drawgantt-svg/ # Let's call them p1 and p2 ping p2 diff --git a/plots.py b/plots.py index de9a083..41b31ec 100644 --- a/plots.py +++ b/plots.py @@ -74,7 +74,8 @@ 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.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: @@ -282,6 +283,70 @@ def make_log_plot(logs, exp, criterion, side, obj, record, machine=None, version 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 = [] @@ -487,26 +552,26 @@ def cmp_versions(logs, exps, criteria, objs): except ZeroDivisionError: pass -def tabulate(lines): +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 = False + ruler_state = False for line in lines: for col in range(len(line)): if col > 0: - table += " " + table += separator table += line[col] rem = widths[col] - len(line[col]) - if ruler: + if ruler_state: table += " " * (rem % 4) + " ยท " * (rem // 4) else: table += " " * rem - table += "\n" - ruler = not ruler + table += endline+"\n" + ruler_state ^= ruler return table def make_summary(logs): @@ -634,6 +699,36 @@ def analyze_logs(logs): }) 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) @@ -668,6 +763,7 @@ def make_tx_summary(client_logs, server_logs): "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, @@ -689,18 +785,50 @@ def make_tx_summary(client_logs, server_logs): 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 -if __name__ == "__main__": - cmd = sys.argv[1] - logfile_name = sys.argv[2] +def parse_logs(logfile_name): logfile = open(logfile_name, "r") lines = logfile.readlines() logfile.close() @@ -718,6 +846,14 @@ if __name__ == "__main__": 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 @@ -737,6 +873,26 @@ if __name__ == "__main__": 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: @@ -749,7 +905,7 @@ if __name__ == "__main__": elif cmd == "summary": make_summary(logs) elif cmd == "tx": - logfile_name = sys.argv[2] + logfile_name = sys.argv[3] logfile = open(logfile_name, "r") lines = logfile.readlines() logfile.close()