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