223 lines
8.8 KiB
Python
223 lines
8.8 KiB
Python
# profileAnalyzer/gui/gui.py
|
|
|
|
"""
|
|
GUI for the Profile Analyzer application.
|
|
"""
|
|
|
|
import tkinter as tk
|
|
from tkinter import ttk, filedialog, messagebox, scrolledtext
|
|
from profileanalyzer.core.core import ProfileAnalyzer
|
|
import csv
|
|
|
|
class ProfileAnalyzerGUI(tk.Frame):
|
|
"""
|
|
The main GUI frame for the profile analyzer application.
|
|
"""
|
|
|
|
SORT_OPTIONS = {
|
|
"Total Time (tottime)": "tottime",
|
|
"Cumulative Time (cumtime)": "cumulative",
|
|
"Number of Calls (ncalls)": "ncalls",
|
|
"Function Name": "filename"
|
|
}
|
|
|
|
# Mapping from Treeview column ID to pstats sort key
|
|
COLUMN_SORT_MAP = {
|
|
"ncalls": "ncalls",
|
|
"tottime": "tottime",
|
|
"cumtime": "cumulative",
|
|
"function": "filename"
|
|
}
|
|
|
|
def __init__(self, master: tk.Tk):
|
|
super().__init__(master)
|
|
self.master = master
|
|
self.master.title("Python Profile Analyzer")
|
|
self.master.geometry("1200x700")
|
|
|
|
self.analyzer = ProfileAnalyzer()
|
|
self.current_stats_data = []
|
|
|
|
self.loaded_filepath_var = tk.StringVar(value="No profile loaded.")
|
|
self.sort_by_var = tk.StringVar(value=list(self.SORT_OPTIONS.keys())[0])
|
|
|
|
self._create_widgets()
|
|
self.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)
|
|
|
|
def _create_widgets(self):
|
|
"""Create and lay out the main widgets."""
|
|
|
|
top_frame = ttk.Frame(self)
|
|
top_frame.pack(fill=tk.X, pady=(0, 10))
|
|
top_frame.columnconfigure(2, weight=1)
|
|
|
|
load_button = ttk.Button(top_frame, text="Load Profile File...", command=self._load_profile)
|
|
load_button.grid(row=0, column=0, padx=(0, 10))
|
|
|
|
export_button = ttk.Button(top_frame, text="Export to CSV...", command=self._export_to_csv)
|
|
export_button.grid(row=0, column=1, padx=(0, 10))
|
|
|
|
loaded_file_label = ttk.Label(top_frame, text="Current Profile:")
|
|
loaded_file_label.grid(row=0, column=2, sticky="w")
|
|
|
|
loaded_file_display = ttk.Label(top_frame, textvariable=self.loaded_filepath_var, anchor="w", relief="sunken")
|
|
loaded_file_display.grid(row=0, column=3, sticky="ew", padx=5)
|
|
top_frame.columnconfigure(3, weight=1)
|
|
|
|
# --- Notebook for different views ---
|
|
notebook = ttk.Notebook(self)
|
|
notebook.pack(fill=tk.BOTH, expand=True)
|
|
|
|
table_tab = ttk.Frame(notebook)
|
|
text_tab = ttk.Frame(notebook)
|
|
|
|
notebook.add(table_tab, text="Table View")
|
|
notebook.add(text_tab, text="Text View")
|
|
|
|
self._create_table_tab(table_tab)
|
|
self._create_text_tab(text_tab)
|
|
|
|
bottom_frame = ttk.Frame(self)
|
|
bottom_frame.pack(fill=tk.X, pady=(10, 0))
|
|
|
|
sort_label = ttk.Label(bottom_frame, text="Sort by:")
|
|
sort_label.pack(side=tk.LEFT, padx=(0, 5))
|
|
|
|
self.sort_combo = ttk.Combobox(bottom_frame, textvariable=self.sort_by_var, values=list(self.SORT_OPTIONS.keys()), state="readonly")
|
|
self.sort_combo.pack(side=tk.LEFT)
|
|
self.sort_combo.bind("<<ComboboxSelected>>", self._on_sort_change)
|
|
|
|
def _create_table_tab(self, parent_frame):
|
|
"""Creates the Treeview widget for displaying stats."""
|
|
columns = ("ncalls", "tottime", "percall_tottime", "cumtime", "percall_cumtime", "function")
|
|
self.tree = ttk.Treeview(parent_frame, columns=columns, show="headings")
|
|
|
|
col_map = {
|
|
"ncalls": ("N-Calls", 80), "tottime": ("Total Time (s)", 120),
|
|
"percall_tottime": ("Per Call (s)", 100), "cumtime": ("Cum. Time (s)", 120),
|
|
"percall_cumtime": ("Per Call (s)", 100), "function": ("Function", 400)
|
|
}
|
|
|
|
for col_id, (text, width) in col_map.items():
|
|
anchor = "w" if col_id == "function" else "e"
|
|
stretch = True if col_id == "function" else False
|
|
self.tree.heading(col_id, text=text, anchor="w", command=lambda c=col_id: self._on_header_click(c))
|
|
self.tree.column(col_id, width=width, stretch=stretch, anchor=anchor)
|
|
|
|
vsb = ttk.Scrollbar(parent_frame, orient="vertical", command=self.tree.yview)
|
|
hsb = ttk.Scrollbar(parent_frame, 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")
|
|
|
|
parent_frame.columnconfigure(0, weight=1)
|
|
parent_frame.rowconfigure(0, weight=1)
|
|
|
|
def _create_text_tab(self, parent_frame):
|
|
"""Creates the ScrolledText widget for raw text view."""
|
|
self.text_view = scrolledtext.ScrolledText(parent_frame, wrap=tk.WORD, state=tk.DISABLED, font=("Courier New", 9))
|
|
self.text_view.pack(fill=tk.BOTH, expand=True)
|
|
|
|
def _load_profile(self):
|
|
filepath = filedialog.askopenfilename(
|
|
title="Select a .prof file",
|
|
filetypes=[("Profile files", "*.prof"), ("All files", "*.*")]
|
|
)
|
|
if not filepath: return
|
|
|
|
if self.analyzer.load_profile(filepath):
|
|
self.loaded_filepath_var.set(filepath)
|
|
self._update_stats_display()
|
|
else:
|
|
self.loaded_filepath_var.set("Failed to load profile.")
|
|
self._clear_views()
|
|
messagebox.showerror("Error", f"Could not load or parse profile file:\n{filepath}")
|
|
|
|
def _on_header_click(self, column_id):
|
|
"""Handles sorting when a treeview header is clicked."""
|
|
sort_key = self.COLUMN_SORT_MAP.get(column_id)
|
|
if not sort_key: return
|
|
|
|
# Find the display name corresponding to the sort key to update the combobox
|
|
for display_name, key in self.SORT_OPTIONS.items():
|
|
if key == sort_key:
|
|
self.sort_by_var.set(display_name)
|
|
break
|
|
|
|
self._update_stats_display()
|
|
|
|
def _on_sort_change(self, event=None):
|
|
self._update_stats_display()
|
|
|
|
def _update_stats_display(self):
|
|
self._clear_views()
|
|
sort_key = self.SORT_OPTIONS.get(self.sort_by_var.get())
|
|
if not sort_key or not self.analyzer.stats:
|
|
return
|
|
|
|
self.current_stats_data = self.analyzer.get_stats(sort_by=sort_key, limit=200)
|
|
|
|
# Populate Table View
|
|
for row in self.current_stats_data:
|
|
formatted_row = (row[0], f"{row[1]:.6f}", f"{row[2]:.6f}", f"{row[3]:.6f}", f"{row[4]:.6f}", row[5])
|
|
self.tree.insert("", "end", values=formatted_row)
|
|
|
|
# Populate Text View
|
|
self._update_text_view()
|
|
|
|
def _update_text_view(self):
|
|
"""Formats and displays the current stats data in the text widget."""
|
|
self.text_view.config(state=tk.NORMAL)
|
|
self.text_view.delete("1.0", tk.END)
|
|
|
|
if not self.current_stats_data:
|
|
self.text_view.config(state=tk.DISABLED)
|
|
return
|
|
|
|
# Create header
|
|
header = f"{'N-Calls':>10s} {'Total Time':>15s} {'Per Call':>12s} {'Cum. Time':>15s} {'Per Call':>12s} {'Function':<}\n"
|
|
separator = f"{'-'*10} {'-'*15} {'-'*12} {'-'*15} {'-'*12} {'-'*8}\n"
|
|
self.text_view.insert(tk.END, header)
|
|
self.text_view.insert(tk.END, separator)
|
|
|
|
# Create data rows
|
|
for row in self.current_stats_data:
|
|
line = f"{str(row[0]):>10s} {row[1]:15.6f} {row[2]:12.6f} {row[3]:15.6f} {row[4]:12.6f} {row[5]:<}\n"
|
|
self.text_view.insert(tk.END, line)
|
|
|
|
self.text_view.config(state=tk.DISABLED)
|
|
|
|
def _export_to_csv(self):
|
|
"""Exports the currently displayed statistics to a CSV file."""
|
|
if not self.current_stats_data:
|
|
messagebox.showwarning("No Data", "No profile data to export. Please load a profile first.")
|
|
return
|
|
|
|
filepath = filedialog.asksaveasfilename(
|
|
defaultextension=".csv",
|
|
filetypes=[("CSV files", "*.csv"), ("All files", "*.*")],
|
|
title="Save Profile Stats as CSV"
|
|
)
|
|
if not filepath:
|
|
return
|
|
|
|
try:
|
|
with open(filepath, 'w', newline='', encoding='utf-8') as f:
|
|
writer = csv.writer(f)
|
|
# Write header
|
|
writer.writerow(["ncalls", "tottime", "percall_tottime", "cumtime", "percall_cumtime", "function_details"])
|
|
# Write data rows
|
|
writer.writerows(self.current_stats_data)
|
|
messagebox.showinfo("Export Successful", f"Stats successfully exported to:\n{filepath}")
|
|
except IOError as e:
|
|
messagebox.showerror("Export Error", f"Failed to save CSV file:\n{e}")
|
|
|
|
def _clear_views(self):
|
|
"""Clears both the tree and text views."""
|
|
for item in self.tree.get_children():
|
|
self.tree.delete(item)
|
|
self.text_view.config(state=tk.NORMAL)
|
|
self.text_view.delete("1.0", tk.END)
|
|
self.text_view.config(state=tk.DISABLED) |