SXXXXXXX_CppPythonDebug/cpp_python_debug/gui/dump_analysis_tab.py
2025-09-25 12:27:58 +02:00

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)