250 lines
9.8 KiB
Python
250 lines
9.8 KiB
Python
|
|
import tkinter as tk
|
|
from tkinter import ttk, messagebox
|
|
import os
|
|
import json
|
|
import sys
|
|
import subprocess
|
|
from datetime import datetime
|
|
from typing import TYPE_CHECKING, List, Dict, Any, Optional
|
|
|
|
# Importa i moduli necessari
|
|
from cpp_python_debug.core.timeline_analyzer import create_timeline_from_session
|
|
from cpp_python_debug.gui.timeline_window import TimelineWindow
|
|
|
|
if TYPE_CHECKING:
|
|
from cpp_python_debug.core.config_manager import AppSettings
|
|
|
|
class DumpAnalysisTab(ttk.Frame):
|
|
"""
|
|
A tab for analyzing and visualizing past debug dump runs.
|
|
"""
|
|
def __init__(self, parent: ttk.Notebook, app_settings: "AppSettings", log_directory_path: str, **kwargs):
|
|
super().__init__(parent, **kwargs)
|
|
self.app_settings = app_settings
|
|
self.parent_notebook = parent
|
|
|
|
# Percorso della cartella principale dei dump di profilo
|
|
self.profile_dumps_dir = os.path.join(
|
|
log_directory_path, "profile_dumps"
|
|
)
|
|
|
|
self._create_widgets()
|
|
self.load_dump_runs()
|
|
|
|
# Ricarica i run quando la tab diventa visibile
|
|
self.bind("<Visibility>", lambda event: self.load_dump_runs())
|
|
|
|
def _create_widgets(self):
|
|
self.columnconfigure(0, weight=1)
|
|
self.rowconfigure(0, weight=1)
|
|
|
|
# Frame per la tabella dei run
|
|
runs_frame = ttk.LabelFrame(self, text="Available Dump Runs", padding="10")
|
|
runs_frame.grid(row=0, column=0, sticky="nsew", padx=10, pady=10)
|
|
runs_frame.columnconfigure(0, weight=1)
|
|
runs_frame.rowconfigure(0, weight=1)
|
|
|
|
# Tabella (Treeview)
|
|
self.runs_tree = ttk.Treeview(
|
|
runs_frame,
|
|
columns=("profile_name", "start_time", "end_time", "status", "dumps"),
|
|
show="headings",
|
|
selectmode="browse"
|
|
)
|
|
self.runs_tree.grid(row=0, column=0, sticky="nsew")
|
|
|
|
# Headings
|
|
self.runs_tree.heading("profile_name", text="Profile Name")
|
|
self.runs_tree.heading("start_time", text="Start Time")
|
|
self.runs_tree.heading("end_time", text="End Time")
|
|
self.runs_tree.heading("status", text="Status")
|
|
self.runs_tree.heading("dumps", text="Dumps")
|
|
|
|
# Column widths
|
|
self.runs_tree.column("profile_name", width=150, stretch=True)
|
|
self.runs_tree.column("start_time", width=160, stretch=False)
|
|
self.runs_tree.column("end_time", width=160, stretch=False)
|
|
self.runs_tree.column("status", width=150, stretch=True)
|
|
self.runs_tree.column("dumps", width=60, stretch=False, anchor="center")
|
|
|
|
# Scrollbar
|
|
scrollbar = ttk.Scrollbar(runs_frame, orient="vertical", command=self.runs_tree.yview)
|
|
self.runs_tree.configure(yscrollcommand=scrollbar.set)
|
|
scrollbar.grid(row=0, column=1, sticky="ns")
|
|
|
|
self.runs_tree.bind("<<TreeviewSelect>>", self._on_run_selected)
|
|
|
|
# Frame per i bottoni
|
|
button_frame = ttk.Frame(self)
|
|
button_frame.grid(row=1, column=0, sticky="se", padx=10, pady=(0, 10))
|
|
|
|
self.analyze_button = ttk.Button(
|
|
button_frame, text="Analyze...", state=tk.DISABLED, command=self._on_analyze_clicked
|
|
)
|
|
self.analyze_button.pack(side=tk.RIGHT, padx=5)
|
|
|
|
self.open_folder_button = ttk.Button(
|
|
button_frame, text="Open Folder", state=tk.DISABLED, command=self._open_selected_run_folder
|
|
)
|
|
self.open_folder_button.pack(side=tk.RIGHT, padx=5)
|
|
|
|
self.refresh_button = ttk.Button(
|
|
button_frame, text="Refresh", command=self.load_dump_runs
|
|
)
|
|
self.refresh_button.pack(side=tk.RIGHT)
|
|
|
|
def load_dump_runs(self):
|
|
"""Scans the dump directory, parses summary files, and populates the treeview."""
|
|
# Clear existing items
|
|
for item in self.runs_tree.get_children():
|
|
self.runs_tree.delete(item)
|
|
|
|
self.analyze_button.config(state=tk.DISABLED)
|
|
self.open_folder_button.config(state=tk.DISABLED)
|
|
|
|
if not os.path.isdir(self.profile_dumps_dir):
|
|
return
|
|
|
|
run_folders = sorted(
|
|
[d for d in os.listdir(self.profile_dumps_dir) if os.path.isdir(os.path.join(self.profile_dumps_dir, d))],
|
|
reverse=True # Ordina dal più recente al più vecchio
|
|
)
|
|
|
|
for folder_name in run_folders:
|
|
run_path = os.path.join(self.profile_dumps_dir, folder_name)
|
|
summary_file = self._find_summary_file(run_path)
|
|
|
|
if not summary_file:
|
|
continue
|
|
|
|
try:
|
|
with open(summary_file, 'r', encoding='utf-8') as f:
|
|
summary_data = json.load(f)
|
|
|
|
profile_name = summary_data.get("profile_name", "N/A")
|
|
start_time = self._format_timestamp(summary_data.get("start_time"))
|
|
end_time = self._format_timestamp(summary_data.get("end_time"))
|
|
|
|
# Determine accurate status
|
|
base_status = summary_data.get("status", "Unknown")
|
|
has_errors = False
|
|
if any(action.get('status', 'Success') != 'CompletedThisHit' for action in summary_data.get('actions_summary', [])):
|
|
has_errors = True
|
|
if not has_errors:
|
|
if any(file.get('status', 'Success') != 'Success' for file in summary_data.get('files_produced_detailed', [])):
|
|
has_errors = True
|
|
|
|
status = base_status
|
|
if has_errors and "Completed" in base_status:
|
|
status = "Completed with Errors"
|
|
elif has_errors:
|
|
status = "Error"
|
|
|
|
dump_count = len(summary_data.get("files_produced_detailed", []))
|
|
|
|
# Inserisce i dati nella tabella, passando il path come valore nascosto
|
|
self.runs_tree.insert(
|
|
"",
|
|
tk.END,
|
|
values=(profile_name, start_time, end_time, status, dump_count),
|
|
iid=run_path # Usa il path come ID univoco della riga
|
|
)
|
|
|
|
except (IOError, json.JSONDecodeError):
|
|
continue # Salta le cartelle con sommari corrotti
|
|
|
|
# Evidenzia il primo elemento (il più recente)
|
|
children = self.runs_tree.get_children()
|
|
if children:
|
|
self.runs_tree.selection_set(children[0])
|
|
self.runs_tree.focus(children[0])
|
|
|
|
def _find_summary_file(self, dir_path: str) -> Optional[str]:
|
|
for filename in os.listdir(dir_path):
|
|
if 'summary' in filename and filename.endswith('.json'):
|
|
return os.path.join(dir_path, filename)
|
|
return None
|
|
|
|
def _format_timestamp(self, ts_string: Optional[str]) -> str:
|
|
if not ts_string:
|
|
return "N/A"
|
|
try:
|
|
# Formato ISO 8601 con microsecondi
|
|
dt_obj = datetime.fromisoformat(ts_string)
|
|
return dt_obj.strftime('%Y-%m-%d %H:%M:%S')
|
|
except (ValueError, TypeError):
|
|
return ts_string # Ritorna la stringa originale se il parsing fallisce
|
|
|
|
def _on_run_selected(self, event=None):
|
|
"""Enable the analyze button when a run is selected."""
|
|
if self.runs_tree.selection():
|
|
self.analyze_button.config(state=tk.NORMAL)
|
|
self.open_folder_button.config(state=tk.NORMAL)
|
|
else:
|
|
self.analyze_button.config(state=tk.DISABLED)
|
|
self.open_folder_button.config(state=tk.DISABLED)
|
|
|
|
def _on_analyze_clicked(self):
|
|
"""Handles the click of the 'Analyze' button."""
|
|
selection = self.runs_tree.selection()
|
|
if not selection:
|
|
return
|
|
|
|
session_path = selection[0]
|
|
summary_file = self._find_summary_file(session_path)
|
|
if not summary_file:
|
|
messagebox.showerror("Error", "Could not find summary file for the selected run.", parent=self)
|
|
return
|
|
|
|
try:
|
|
with open(summary_file, 'r', encoding='utf-8') as f:
|
|
summary_data = json.load(f)
|
|
except (IOError, json.JSONDecodeError) as e:
|
|
messagebox.showerror("Error", f"Could not read or parse summary file:\n{e}", parent=self)
|
|
return
|
|
|
|
try:
|
|
self.parent_notebook.config(cursor="watch")
|
|
self.update_idletasks()
|
|
|
|
timeline_df = create_timeline_from_session(session_path)
|
|
|
|
self.parent_notebook.config(cursor="")
|
|
|
|
if timeline_df.empty:
|
|
messagebox.showinfo("Analysis Complete", "No valid dump data found in the selected run.", parent=self)
|
|
return
|
|
|
|
# Open the analysis window, now passing the full summary data
|
|
TimelineWindow(
|
|
self,
|
|
timeline_df=timeline_df,
|
|
summary_data=summary_data
|
|
)
|
|
|
|
except Exception as e:
|
|
self.parent_notebook.config(cursor="")
|
|
messagebox.showerror("Analysis Error", f"An unexpected error occurred during analysis:\n{e}", parent=self)
|
|
|
|
def _open_selected_run_folder(self):
|
|
"""Opens the selected run's directory in the system's file explorer."""
|
|
selection = self.runs_tree.selection()
|
|
if not selection:
|
|
return
|
|
|
|
folder_path = selection[0] # The IID of the row is the session path
|
|
|
|
try:
|
|
if os.path.isdir(folder_path):
|
|
if sys.platform == "win32":
|
|
os.startfile(os.path.normpath(folder_path))
|
|
elif sys.platform == "darwin":
|
|
subprocess.run(["open", folder_path], check=True)
|
|
else:
|
|
subprocess.run(["xdg-open", folder_path], check=True)
|
|
else:
|
|
messagebox.showwarning("Directory Not Found", f"The directory no longer exists:\n{folder_path}", parent=self)
|
|
except Exception as e:
|
|
messagebox.showerror("Error Opening Folder", f"Could not open the folder:\n{e}", parent=self)
|