# 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()