pandemonium_engine/modules/network_synchronizer/debugger_ui/debugger.py

390 lines
14 KiB
Python

#!/usr/bin/env python3
# PySimpleGUI call reference manual: https://www.pysimplegui.org/en/latest/call%20reference/
import PySimpleGUI as sg
import json
from os import listdir
from os.path import isfile, join, isdir, exists
# ------------------------------------------------------------------------------------------- Config
font_size = 9
# ---------------------------------------------------------------------------------------- Utilities
def load_json(path):
if exists(path):
with open(path) as f:
return json.load(f)
return {}
def compare_arrays(arr_a, arr_b):
if len(arr_a) != len(arr_b):
return False
for i in range(len(arr_a)):
if arr_a[i] != arr_b[i]:
return False
return True
def repeat_characters(char, n):
s = ""
for i in range(0, n):
s += char
return s
# Fetches the directories containing the frame info.
directories = [f for f in listdir("./") if (isdir(join("./", f)) and len(listdir(join("./", f))) > 0)]
# ----------------------------------------------------------------------------------------------- UI
def create_layout():
# Fetch the frame count from the dirs.
frame_count = -1
frames_iterations = {}
frames_description = {}
for dir in directories:
dir_path = join("./", dir)
file_names = [f for f in listdir(dir_path) if isfile(join(dir_path, f))]
for file_name in file_names:
file_extension_index = file_name.find("fd-")
if file_extension_index == 0:
# This file contains information about the frame.
# Extract the frame index
frame_iteration = file_name.count("@") + 1
if frame_iteration > 1:
frame_index = int(file_name[3:file_name.index("@")])
else:
frame_index = int(file_name[3:file_name.index(".json")])
if frame_index in frames_iterations:
frames_iterations[frame_index] = max(frame_iteration, frames_iterations[frame_index])
else:
frames_iterations[frame_index] = frame_iteration
frame_count = max(frame_count, frame_index)
file_path = join(dir_path, file_name)
file_json = load_json(file_path)
if "frame_summary" in file_json:
if frame_index in frames_description:
frames_description[frame_index] += " " + file_json["frame_summary"]
else:
frames_description[frame_index] = file_json["frame_summary"]
else:
frames_description[frame_index] = ""
frame_count += 1
# --- UI - Compose timeline ---
frame_list_values = []
for frame_index in range(frame_count):
frame_description = frames_description[frame_index]
frame_iterations = frames_iterations[frame_index]
for i in range(0, frame_iterations):
if i == 0:
frame_list_values.append("# " + str(frame_index) + " - " + frame_description)
else:
frame_list_values.append("# " + str(frame_index) + " @" + str(i + 1) + " - " + frame_description)
# Release this array, we don't need anylonger.
frames_description.clear()
frames_list = sg.Listbox(frame_list_values, key="FRAMES_LIST", size = [45, 30], enable_events=True, horizontal_scroll=True, select_mode=sg.LISTBOX_SELECT_MODE_BROWSE)
frames_list = sg.Frame("Frames", layout=[[frames_list]], vertical_alignment="top")
# --- UI - Compose frame detail ---
# Node list
nodes_list_listbox = sg.Listbox([], key="NODE_LIST", size = [45, 0], enable_events=True, horizontal_scroll=True, expand_y=True, expand_x=True, select_mode=sg.LISTBOX_SELECT_MODE_MULTIPLE)
nodes_list_listbox = sg.Frame("Nodes", layout=[[nodes_list_listbox]], vertical_alignment="top", expand_y=True, expand_x=True);
# Selected nodes title.
node_tile_txt = sg.Text("", key="FRAME_SUMMARY", font="Any, " + str(font_size - 1), justification="left", border_width=1, text_color="dark red")
# States table
table_status_header = ["name"]
table_status_widths = [30]
for dir in directories:
table_status_header.append("Begin ["+dir+"]")
table_status_widths.append(30)
for dir in directories:
table_status_header.append("End ("+dir+")")
table_status_widths.append(30)
table_status = sg.Table([], table_status_header, key="TABLE_STATUS", justification='left', auto_size_columns=False, col_widths=table_status_widths, vertical_scroll_only=False, num_rows=38)
table_status = sg.Frame("States", layout=[[table_status]], vertical_alignment="top")
# Messages table
tables_logs = []
for dir in directories:
tables_logs.append(sg.Frame("Log: " + dir + " Iteration: ", key=dir+"_FRAME_TABLE_LOG", layout=[[sg.Table([], [" #", "Log"], key=dir+"_TABLE_LOG", justification='left', auto_size_columns=False, col_widths=[4, 70], vertical_scroll_only=False, num_rows=25)]], vertical_alignment="top"))
logs = sg.Frame("Messages", layout=[tables_logs], vertical_alignment="top")
# --- UI - Main Window ---
layout = [
[
sg.Frame("", [[frames_list], [nodes_list_listbox]], vertical_alignment="top", expand_y=True),
sg.Frame("Frame detail", [[node_tile_txt], [table_status], [logs]], key="FRAME_FRAME_DETAIL", vertical_alignment="top")
],
[
sg.Button("Exit")
]
]
return layout
# ------------------------------------------------------------------------------------ Create window
window = sg.Window(title="Network Synchronizer Debugger.", layout=create_layout(), margins=(5, 5), font="Any, " + str(font_size), resizable=True)
# ----------------------------------------------------------------------------------- Event handling
frame_data = {}
nodes_list = []
selected_nodes = []
used_frame_iteration = {}
while True:
event, event_values = window.read()
# EVENT: Close the program.
if event == "Exit" or event == sg.WIN_CLOSED:
window.close()
break
# EVENT: Show frame
if event == "FRAMES_LIST":
window["NODE_LIST"].update([])
if event_values["FRAMES_LIST"] != []:
frame_description = event_values["FRAMES_LIST"][0]
if "@" in frame_description:
frame_iteration = int(frame_description[frame_description.index(" @") + 2:frame_description.index(" - ")])
selected_frame_index = int(frame_description[2:frame_description.index(" @")])
else:
frame_iteration = 1
selected_frame_index = int(frame_description[2:frame_description.index(" - ")])
print("Show frame: ", selected_frame_index, " Iteration: ", frame_iteration)
frame_data = {}
nodes_list = []
used_frame_iteration = {}
for dir in directories:
used_frame_iteration[dir] = frame_iteration
frame_file_path = join("./", dir, "fd-" + str(selected_frame_index) + repeat_characters("@", frame_iteration - 1) + ".json")
if not exists(frame_file_path):
print("The path: ", frame_file_path, " was not found. Falling back to no iteration path.")
used_frame_iteration[dir] = 1
frame_file_path = join("./", dir, "fd-" + str(selected_frame_index) + ".json")
print("Path: ", frame_file_path)
if exists(frame_file_path):
frame_data_json = load_json(frame_file_path)
frame_data[dir] = frame_data_json
for node_path in frame_data_json["begin_state"]:
if node_path not in nodes_list:
# Add this node to the nodelist
nodes_list.append(node_path)
for node_path in frame_data_json["end_state"]:
if node_path not in nodes_list:
# Add this node to the nodelist
nodes_list.append(node_path)
for node_path in frame_data_json["node_log"]:
if node_path not in nodes_list:
# Add this node to the nodelist
nodes_list.append(node_path)
# Update the node list.
window["NODE_LIST"].update(nodes_list)
if len(selected_nodes) == 0:
if len(nodes_list) > 0:
selected_nodes = [nodes_list[0]]
else:
selected_nodes = []
window["NODE_LIST"].set_value(selected_nodes)
event = "NODE_LIST"
event_values = {"NODE_LIST": selected_nodes}
# EVENT: Show node data
if event == "NODE_LIST":
window["FRAME_SUMMARY"].update("")
window["TABLE_STATUS"].update([])
for dir_name in directories:
window[dir_name + "_FRAME_TABLE_LOG"].update("Log: " + dir_name + "Frame: " + str(selected_frame_index) + " Iteration: " + str(used_frame_iteration[dir_name]))
window[dir_name + "_TABLE_LOG"].update([["", "[Nothing for this node]"]])
if event_values["NODE_LIST"] != []:
instances_count = len(directories)
row_size = 1 + (instances_count * 2)
# Compose the status table
states_table_values = []
states_row_colors = []
table_logs = {}
log_row_colors = {}
selected_nodes = event_values["NODE_LIST"]
for node_path in selected_nodes:
# First collects the var names
vars_names = ["***"]
for dir in directories:
if dir in frame_data:
if "begin_state" in frame_data[dir]:
if node_path in frame_data[dir]["begin_state"]:
for var_name in frame_data[dir]["begin_state"][node_path]:
if var_name not in vars_names:
vars_names.append(var_name)
if "end_state" in frame_data[dir]:
if node_path in frame_data[dir]["end_state"]:
for var_name in frame_data[dir]["end_state"][node_path]:
if var_name not in vars_names:
vars_names.append(var_name)
vars_names.append("---")
# Add those to the table.
for var_name in vars_names:
# Initializes the row.
row = [""] * row_size
row_index = len(states_table_values)
# Special rows
if var_name == "***":
# This is a special row to signal the start of a new node data
row[0] = node_path
states_table_values.append(row)
states_row_colors.append((row_index, "black"))
continue
elif var_name == "---":
# This is a special row to signal the end of the node data
states_table_values.append(row)
continue
row[0] = var_name.replace("*", "🔄")
# Set the row data.
for dir_i, dir_name in enumerate(directories):
if dir_name in frame_data:
if "begin_state" in frame_data[dir_name]:
if node_path in frame_data[dir_name]["begin_state"]:
if var_name in frame_data[dir_name]["begin_state"][node_path]:
#print(1, " + (", instances_count, " * 0) + ", dir_i)
row[1 + (instances_count * 0) + dir_i] = str(frame_data[dir_name]["begin_state"][node_path][var_name])
if "end_state" in frame_data[dir_name]:
if node_path in frame_data[dir_name]["end_state"]:
if var_name in frame_data[dir_name]["end_state"][node_path]:
#print(1, " + (", instances_count, " * 1) + ", dir_i)
row[1 + (instances_count * 1) + dir_i] = str(frame_data[dir_name]["end_state"][node_path][var_name])
# Check if different, so mark a worning.
for state_index in range(2):
for i in range(instances_count - 1):
if row[1 + (state_index*instances_count) + i + 0] != row[1 + (state_index*instances_count) + i + 1]:
row[1 + (state_index*instances_count) + i + 0] = "⚠️ " + row[1 + (state_index*instances_count) + i + 0]
row[1 + (state_index*instances_count) + i + 1] = "⚠️ " + row[1 + (state_index*instances_count) + i + 1]
states_row_colors.append((row_index, "dark salmon"))
break
states_table_values.append(row)
# Compose the log
for dir_name in directories:
if dir_name in frame_data:
if "node_log" in frame_data[dir_name]:
if node_path in frame_data[dir_name]["node_log"]:
table_logs[dir_name] = table_logs.get(dir_name, [])
log_row_colors[dir_name] = log_row_colors.get(dir_name, [])
table_logs[dir_name] += [["", node_path]]
log_row_colors[dir_name] += [(len(table_logs[dir_name]) - 1, "black")]
for val in frame_data[dir_name]["node_log"][node_path]:
# Append the log
table_logs[dir_name] += [["{:4d}".format(val["i"]), val["m"]]]
row_index = len(table_logs[dir_name]) - 1
# Check if this line should be colored
if val["m"].find("[WARNING]") == 0:
log_row_colors[dir_name] += [(row_index, "dark salmon")]
elif val["m"].find("[ERROR]") == 0:
log_row_colors[dir_name] += [(row_index, "red")]
elif val["m"].find("[WRITE]") == 0:
log_row_colors[dir_name] += [(row_index, "cadet blue")]
elif val["m"].find("[READ]") == 0:
log_row_colors[dir_name] += [(row_index, "medium aquamarine")]
table_logs[dir_name] += [["", ""]]
window["FRAME_FRAME_DETAIL"].update("Frame " + str(selected_frame_index) + " details")
window["TABLE_STATUS"].update(states_table_values, row_colors=states_row_colors)
frame_summary = ""
for dir_name in directories:
if dir_name in frame_data:
frame_summary += frame_data[dir_name]["frame_summary"]
if dir_name in table_logs:
window[dir_name + "_TABLE_LOG"].update(table_logs[dir_name], row_colors=log_row_colors[dir_name]);
# Check if write and read databuffer is the same.
for dir_name in directories:
if dir_name in frame_data and (dir_name == "nonet" or dir_name == "client"):
are_the_same = compare_arrays(frame_data[dir_name]["data_buffer_writes"], frame_data[dir_name]["data_buffer_reads"])
if not are_the_same:
if frame_summary != "":
frame_summary += "\n"
frame_summary += "[BUG] The DataBuffer written by `_collect_inputs` and read by `_controller_process` is different. Both should be exactly the same."
# Check if the server read correctly the received buffer.
if "server" in frame_data and "client" in frame_data:
are_the_same = compare_arrays(frame_data["server"]["data_buffer_reads"], frame_data["client"]["data_buffer_reads"])
if not are_the_same:
if frame_summary != "":
frame_summary += "\n"
frame_summary += "[BUG] The DataBuffer written by the client is different from the one read on the server."
# Check if the client sent the correct inputs to the server.
if "client" in frame_data:
for other_frame_index, is_similar in frame_data["client"]["are_inputs_different_results"].items():
other_frame_index = int(other_frame_index)
other_file_path = join("./", "client", "fd-" + str(other_frame_index) + ".json")
other_frame_json = load_json(other_file_path)
if "data_buffer_reads" in other_frame_json:
is_really_similar = compare_arrays(other_frame_json["data_buffer_reads"], frame_data["client"]["data_buffer_reads"])
if is_really_similar != is_similar:
if frame_summary != "":
frame_summary += "\n"
frame_summary += "[BUG] The function `_are_inputs_different` doesn't seems to work:\n"
frame_summary += " As the inputs read on the frame " + str(frame_data["client"]["frame"]) + " and " + str(other_frame_index) + " are " + ("SIMILAR" if is_really_similar else "DIFFERENT") +" but the net sync considered it "+ ("SIMILAR" if is_similar else "DIFFERENT")
window["FRAME_SUMMARY"].update(frame_summary)
# --------------------------------------------------------------------------------------------- Exit