198 lines
8.1 KiB
Python
198 lines
8.1 KiB
Python
import tkinter as tk
|
|
from tkinter import filedialog, messagebox, ttk
|
|
import cProfile
|
|
import pstats
|
|
import subprocess
|
|
import os
|
|
import threading
|
|
import snakeviz.main # Import snakeviz
|
|
import sys
|
|
|
|
|
|
class PythonProfilerGUI:
|
|
"""
|
|
A GUI application for profiling Python scripts using cProfile,
|
|
with advanced features like argument passing, profiling options,
|
|
report visualization, progress bar, and threading.
|
|
"""
|
|
|
|
def __init__(self, master):
|
|
"""
|
|
Initializes the main application window.
|
|
|
|
Args:
|
|
master (tk.Tk): The root Tk window.
|
|
"""
|
|
self.master = master
|
|
master.title("Python Script Profiler")
|
|
|
|
# --- GUI Elements ---
|
|
# Script Path
|
|
self.script_path_label = tk.Label(master, text="Script Path:")
|
|
self.script_path_label.grid(row=0, column=0, padx=5, pady=5, sticky="e")
|
|
|
|
self.script_path_entry = tk.Entry(master, width=50)
|
|
self.script_path_entry.grid(row=0, column=1, padx=5, pady=5, sticky="ew")
|
|
|
|
self.browse_button = tk.Button(master, text="Browse", command=self.browse_file)
|
|
self.browse_button.grid(row=0, column=2, padx=5, pady=5)
|
|
|
|
# Arguments
|
|
self.args_label = tk.Label(master, text="Arguments:")
|
|
self.args_label.grid(row=1, column=0, padx=5, pady=5, sticky="e")
|
|
|
|
self.args_entry = tk.Entry(master, width=50)
|
|
self.args_entry.grid(row=1, column=1, padx=5, pady=5, sticky="ew")
|
|
self.args_entry.insert(0, "") # Default arguments
|
|
|
|
# Output File
|
|
self.output_path_label = tk.Label(master, text="Output File:")
|
|
self.output_path_label.grid(row=2, column=0, padx=5, pady=5, sticky="e")
|
|
|
|
self.output_path_entry = tk.Entry(master, width=50)
|
|
self.output_path_entry.grid(row=2, column=1, padx=5, pady=5, sticky="ew")
|
|
self.output_path_entry.insert(0, "profile_output.prof") # Default output file
|
|
|
|
# Profiling Options (currently placeholder)
|
|
self.profiling_options_label = tk.Label(master, text="Profiling Options:")
|
|
self.profiling_options_label.grid(row=3, column=0, padx=5, pady=5, sticky="e")
|
|
|
|
# TODO: Add actual profiling options (e.g., sample rate) here.
|
|
# For now, a simple placeholder:
|
|
self.profiling_options_entry = tk.Entry(master, width=50)
|
|
self.profiling_options_entry.grid(row=3, column=1, padx=5, pady=5, sticky="ew")
|
|
self.profiling_options_entry.insert(0, "") # Placeholder, no options yet
|
|
|
|
# Profile Button
|
|
self.profile_button = tk.Button(master, text="Profile Script", command=self.start_profiling)
|
|
self.profile_button.grid(row=4, column=1, padx=5, pady=10)
|
|
|
|
# Progress Bar
|
|
self.progress_bar = ttk.Progressbar(master, orient="horizontal", length=300, mode="indeterminate")
|
|
self.progress_bar.grid(row=5, column=1, padx=5, pady=5)
|
|
|
|
# --- Menu ---
|
|
self.menu_bar = tk.Menu(master)
|
|
self.file_menu = tk.Menu(self.menu_bar, tearoff=0)
|
|
self.file_menu.add_command(label="Exit", command=master.quit)
|
|
self.menu_bar.add_cascade(label="File", menu=self.file_menu)
|
|
|
|
self.view_menu = tk.Menu(self.menu_bar, tearoff=0)
|
|
self.view_menu.add_command(label="View Report (Snakeviz)", command=self.view_report)
|
|
self.menu_bar.add_cascade(label="View", menu=self.view_menu)
|
|
|
|
self.help_menu = tk.Menu(self.menu_bar, tearoff=0)
|
|
self.help_menu.add_command(label="About", command=self.show_about)
|
|
self.menu_bar.add_cascade(label="Help", menu=self.help_menu)
|
|
|
|
master.config(menu=self.menu_bar)
|
|
|
|
self.profiling_thread = None # Store the profiling thread
|
|
|
|
def browse_file(self):
|
|
"""
|
|
Opens a file dialog to select the Python script to profile.
|
|
"""
|
|
filename = filedialog.askopenfilename(
|
|
initialdir=os.getcwd(),
|
|
title="Select a Python Script",
|
|
filetypes=(("Python files", "*.py"), ("All files", "*.*"))
|
|
)
|
|
self.script_path_entry.delete(0, tk.END)
|
|
self.script_path_entry.insert(0, filename)
|
|
|
|
def start_profiling(self):
|
|
"""
|
|
Starts the profiling process in a separate thread.
|
|
"""
|
|
script_path = self.script_path_entry.get()
|
|
output_file = self.output_path_entry.get()
|
|
|
|
if not script_path:
|
|
messagebox.showerror("Error", "Please select a script to profile.")
|
|
return
|
|
|
|
if not os.path.exists(script_path):
|
|
messagebox.showerror("Error", "Script file not found.")
|
|
return
|
|
|
|
# Disable the profile button and start the progress bar
|
|
self.profile_button.config(state="disabled")
|
|
self.progress_bar.start()
|
|
|
|
# Create a thread for profiling to avoid blocking the GUI
|
|
self.profiling_thread = threading.Thread(target=self.profile_script,
|
|
args=(script_path, output_file))
|
|
self.profiling_thread.start()
|
|
|
|
def profile_script(self, script_path, output_file):
|
|
"""
|
|
Profiles the selected Python script using cProfile and saves the output to a file.
|
|
Displays error messages if the script path is invalid or if an error occurs during profiling.
|
|
This function runs in a separate thread.
|
|
|
|
Args:
|
|
script_path (str): The path to the Python script.
|
|
output_file (str): The path to save the profiling results.
|
|
"""
|
|
try:
|
|
args_str = self.args_entry.get()
|
|
args = args_str.split() # Split arguments by space
|
|
|
|
# Use subprocess to execute the script, capturing output for error reporting.
|
|
with cProfile.Profile() as pr:
|
|
command = ["python", script_path] + args # Script path + arguments
|
|
process = subprocess.Popen(command,
|
|
stdout=subprocess.PIPE,
|
|
stderr=subprocess.PIPE,
|
|
text=True)
|
|
stdout, stderr = process.communicate()
|
|
return_code = process.returncode
|
|
|
|
if return_code != 0:
|
|
self.master.after(0, lambda: messagebox.showerror("Error", f"Script execution failed:\n{stderr}")) # Use after to update GUI from thread
|
|
return
|
|
|
|
stats = pstats.Stats(pr)
|
|
stats.sort_stats(pstats.SortKey.TIME)
|
|
stats.dump_stats(output_file)
|
|
|
|
self.master.after(0, lambda: messagebox.showinfo("Success", f"Profiling complete. Results saved to {output_file}")) # Use after to update GUI from thread
|
|
|
|
except Exception as e:
|
|
self.master.after(0, lambda: messagebox.showerror("Error", f"An error occurred during profiling: {e}")) # Use after to update GUI from thread
|
|
finally:
|
|
# Re-enable the profile button and stop the progress bar
|
|
self.master.after(0, lambda: self.profile_button.config(state="normal")) # Use after to update GUI from thread
|
|
self.master.after(0, lambda: self.progress_bar.stop()) # Use after to update GUI from thread
|
|
|
|
def view_report(self):
|
|
"""
|
|
Opens the profiling report using Snakeviz.
|
|
"""
|
|
output_file = self.output_path_entry.get()
|
|
|
|
if not os.path.exists(output_file):
|
|
messagebox.showerror("Error", "Profiling output file not found. Profile the script first.")
|
|
return
|
|
|
|
try:
|
|
# Use subprocess to run snakeviz
|
|
subprocess.run(["snakeviz", output_file], check=True)
|
|
|
|
except FileNotFoundError:
|
|
messagebox.showerror("Error", "Snakeviz is not installed or not in your PATH.")
|
|
except subprocess.CalledProcessError as e:
|
|
messagebox.showerror("Error", f"An error occurred while viewing the report with Snakeviz: {e}")
|
|
|
|
def show_about(self):
|
|
"""
|
|
Displays an "About" dialog with information about the application.
|
|
"""
|
|
messagebox.showinfo("About", "Python Script Profiler\nSimple tool for profiling Python scripts using cProfile.")
|
|
|
|
# --- Main ---
|
|
if __name__ == "__main__":
|
|
root = tk.Tk()
|
|
gui = PythonProfilerGUI(root)
|
|
root.mainloop() |