Point plots

This commit is contained in:
Pascal Engélibert 2026-04-30 15:31:56 +02:00
commit 0f52f2813d
2 changed files with 170 additions and 13 deletions

180
plots.py
View file

@ -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 <file1> <target1> [<file2> <target2>] [...]
# 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()