183 lines
7.6 KiB
Python
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() |