SXXXXXXX_ProfileAnalyzer/profileanalyzer/gui/compare_window.py
2025-06-23 15:07:01 +02:00

183 lines
7.6 KiB
Python

# profileAnalyzer/gui/compare_window.py
import tkinter as tk
from tkinter import ttk, filedialog, messagebox
import json
import os
from pstats import Stats
from typing import Optional, Dict
from ..core.core import ProfileAnalyzer
class CompareWindow(tk.Toplevel):
"""A Toplevel window for comparing two profile data files."""
CONFIG_FILE = "app_config.json"
SIGNIFICANT_CHANGE_THRESHOLD = 0.000001 # 1 microsecond
def __init__(self, master, analyzer: ProfileAnalyzer):
super().__init__(master)
self.analyzer = analyzer
self.base_stats: Optional[Stats] = None
self.comp_stats: Optional[Stats] = None
self._init_window()
self._init_vars()
self._load_config()
self._create_widgets()
self._configure_treeview_tags()
def _init_window(self):
self.title("Compare Profile Data")
self.geometry("1200x700")
self.transient(self.master)
self.grab_set()
def _init_vars(self):
self.base_path_var = tk.StringVar()
self.comp_path_var = tk.StringVar()
def _load_config(self):
try:
if os.path.exists(self.CONFIG_FILE):
with open(self.CONFIG_FILE, 'r', encoding='utf-8') as f:
config = json.load(f)
self.base_path_var.set(config.get("last_base_profile", ""))
self.comp_path_var.set(config.get("last_comp_profile", ""))
except (IOError, json.JSONDecodeError) as e:
print(f"Could not load config file: {e}")
def _save_config(self):
config = {
"last_base_profile": self.base_path_var.get(),
"last_comp_profile": self.comp_path_var.get()
}
try:
with open(self.CONFIG_FILE, 'w', encoding='utf-8') as f:
json.dump(config, f, indent=4)
except IOError as e:
print(f"Could not save config file: {e}")
def _create_widgets(self):
main_frame = ttk.Frame(self, padding="10")
main_frame.pack(fill=tk.BOTH, expand=True)
main_frame.rowconfigure(1, weight=1)
main_frame.columnconfigure(0, weight=1)
# --- Top frame for file selection ---
selection_frame = ttk.LabelFrame(main_frame, text="Select Profiles", padding="10")
selection_frame.grid(row=0, column=0, sticky="ew", pady=(0, 10))
selection_frame.columnconfigure(1, weight=1)
# Baseline profile
ttk.Label(selection_frame, text="Baseline Profile:").grid(row=0, column=0, sticky="w", padx=5, pady=2)
ttk.Entry(selection_frame, textvariable=self.base_path_var).grid(row=0, column=1, sticky="ew", padx=5)
ttk.Button(selection_frame, text="...", width=3, command=lambda: self._browse_file(self.base_path_var)).grid(row=0, column=2)
# Comparison profile
ttk.Label(selection_frame, text="Comparison Profile:").grid(row=1, column=0, sticky="w", padx=5, pady=2)
ttk.Entry(selection_frame, textvariable=self.comp_path_var).grid(row=1, column=1, sticky="ew", padx=5)
ttk.Button(selection_frame, text="...", width=3, command=lambda: self._browse_file(self.comp_path_var)).grid(row=1, column=2)
ttk.Button(selection_frame, text="Compare", command=self._run_comparison).grid(row=2, column=1, columnspan=2, sticky="e", pady=10)
# --- Results frame ---
results_frame = ttk.LabelFrame(main_frame, text="Comparison Results", padding="10")
results_frame.grid(row=1, column=0, sticky="nsew")
results_frame.rowconfigure(0, weight=1)
results_frame.columnconfigure(0, weight=1)
self._create_results_tree(results_frame)
def _browse_file(self, path_var: tk.StringVar):
current_path = path_var.get()
initial_dir = os.path.dirname(current_path) if current_path and os.path.exists(os.path.dirname(current_path)) else os.getcwd()
filepath = filedialog.askopenfilename(
title="Select a .prof file",
filetypes=[("Profile files", "*.prof"), ("All files", "*.*")],
initialdir=initial_dir
)
if filepath:
path_var.set(filepath)
def _create_results_tree(self, parent):
columns = ("func", "ncalls", "d_ncalls", "tottime", "d_tottime", "cumtime", "d_cumtime")
self.tree = ttk.Treeview(parent, columns=columns, show="headings")
col_map = {
"func": ("Function", 400), "ncalls": ("N-Calls", 80), "d_ncalls": ("Δ N-Calls", 80),
"tottime": ("Total Time (s)", 110), "d_tottime": ("Δ Total Time", 110),
"cumtime": ("Cum. Time (s)", 110), "d_cumtime": ("Δ Cum. Time", 110)
}
for col_id, (text, width) in col_map.items():
# --- INIZIO DELLA CORREZIONE ---
sort_key = ""
if col_id == "func":
sort_key = "func_str" # Usa la chiave corretta per il nome della funzione
else:
sort_key = col_id.replace('d_', 'delta_')
# --- FINE DELLA CORREZIONE ---
self.tree.heading(col_id, text=text, anchor="w", command=lambda c=sort_key: self._sort_tree(c))
self.tree.column(col_id, width=width, stretch=(col_id == "func"), anchor="w" if col_id == "func" else "e")
vsb = ttk.Scrollbar(parent, orient="vertical", command=self.tree.yview)
hsb = ttk.Scrollbar(parent, orient="horizontal", command=self.tree.xview)
self.tree.configure(yscrollcommand=vsb.set, xscrollcommand=hsb.set)
self.tree.grid(row=0, column=0, sticky="nsew")
vsb.grid(row=0, column=1, sticky="ns")
hsb.grid(row=1, column=0, sticky="ew")
def _configure_treeview_tags(self):
self.tree.tag_configure('better', background='#c8e6c9') # Light Green
self.tree.tag_configure('worse', background='#ffcdd2') # Light Red
def _run_comparison(self):
base_path = self.base_path_var.get()
comp_path = self.comp_path_var.get()
if not base_path or not comp_path:
messagebox.showerror("Error", "Please select both a baseline and a comparison profile.", parent=self)
return
# Use a separate analyzer instance to avoid state conflicts
loader = ProfileAnalyzer()
base_stats = loader.load_profile(base_path)
comp_stats = loader.load_profile(comp_path)
if not base_stats or not comp_stats:
messagebox.showerror("Error", "One or both profile files could not be loaded. Check console for details.", parent=self)
return
self.master.config(cursor="watch")
self.comparison_data = self.analyzer.compare_stats(base_stats, comp_stats)
self._populate_tree()
self.master.config(cursor="")
self._save_config()
def _populate_tree(self):
self.tree.delete(*self.tree.get_children())
for row in self.comparison_data:
d_tt = row['delta_tottime']
tags = ()
if d_tt > self.SIGNIFICANT_CHANGE_THRESHOLD:
tags = ('worse',)
elif d_tt < -self.SIGNIFICANT_CHANGE_THRESHOLD:
tags = ('better',)
formatted_row = (
row['func_str'],
row['ncalls'], f"{row['delta_ncalls']:+d}",
f"{row['tottime']:.6f}", f"{d_tt:+.6f}",
f"{row['cumtime']:.6f}", f"{row['delta_cumtime']:+.6f}"
)
self.tree.insert("", "end", values=formatted_row, tags=tags)
def _sort_tree(self, col: str):
# Sort internal data
self.comparison_data.sort(key=lambda x: x[col], reverse=True)
# Repopulate tree with sorted data
self._populate_tree()