Chore: Stop tracking files based on .gitignore update.
Untracked files matching the following rules: - Rule "!.vscode/launch.json": 1 file - Rule "*.prof": 6 files
This commit is contained in:
parent
986aaa32de
commit
e766172c47
3
.gitignore
vendored
3
.gitignore
vendored
@ -148,3 +148,6 @@ dmypy.json
|
||||
# Temporary files
|
||||
*.swp
|
||||
*~
|
||||
|
||||
|
||||
*.prof
|
||||
18
launch_profiles.json
Normal file
18
launch_profiles.json
Normal file
@ -0,0 +1,18 @@
|
||||
[
|
||||
{
|
||||
"name": "radar_import",
|
||||
"run_as_module": true,
|
||||
"target_path": "C:/src/____GitProjects/radar_data_reader",
|
||||
"module_name": "radar_data_reader",
|
||||
"script_args": "",
|
||||
"python_interpreter": "c:\\Users\\admin\\AppData\\Local\\Programs\\Python\\Python313\\python.exe"
|
||||
},
|
||||
{
|
||||
"name": "test_overlay",
|
||||
"run_as_module": false,
|
||||
"target_path": "C:/src/____GitProjects/FlightMonitor/test_overlay.py",
|
||||
"module_name": "",
|
||||
"script_args": "",
|
||||
"python_interpreter": "c:\\Users\\admin\\AppData\\Local\\Programs\\Python\\Python313\\python.exe"
|
||||
}
|
||||
]
|
||||
@ -1,106 +1,132 @@
|
||||
# profileAnalyzer/core/core.py
|
||||
|
||||
"""
|
||||
Core logic for loading and analyzing cProfile .prof files.
|
||||
"""
|
||||
# ... (tutti gli import e le altre classi/funzioni restano invariate) ...
|
||||
import pstats
|
||||
from io import StringIO
|
||||
import subprocess
|
||||
import sys
|
||||
import os
|
||||
from dataclasses import dataclass, field
|
||||
from typing import List, Optional
|
||||
from datetime import datetime
|
||||
|
||||
@dataclass
|
||||
class LaunchProfile:
|
||||
name: str
|
||||
run_as_module: bool = False
|
||||
target_path: str = ""
|
||||
module_name: str = ""
|
||||
script_args: str = ""
|
||||
python_interpreter: str = sys.executable
|
||||
|
||||
class ProfileAnalyzer:
|
||||
"""
|
||||
Handles loading a profile data file and extracting statistics.
|
||||
"""
|
||||
# ... (nessuna modifica qui)
|
||||
def __init__(self):
|
||||
self.stats = None
|
||||
self.profile_path = None
|
||||
self.stats: Optional[pstats.Stats] = None
|
||||
self.profile_path: Optional[str] = None
|
||||
|
||||
def load_profile(self, filepath: str) -> bool:
|
||||
"""
|
||||
Loads a .prof file into a pstats.Stats object.
|
||||
|
||||
Args:
|
||||
filepath (str): The path to the .prof file.
|
||||
|
||||
Returns:
|
||||
bool: True if loading was successful, False otherwise.
|
||||
"""
|
||||
try:
|
||||
self.stats = pstats.Stats(filepath)
|
||||
self.stats.strip_dirs() # Clean up filenames for readability
|
||||
self.stats.strip_dirs()
|
||||
self.profile_path = filepath
|
||||
return True
|
||||
except (FileNotFoundError, TypeError, OSError) as e:
|
||||
# Handle cases where the file doesn't exist or is not a valid stats file
|
||||
print(f"Error loading profile file '{filepath}': {e}")
|
||||
self.stats = None
|
||||
self.profile_path = None
|
||||
return False
|
||||
|
||||
def get_stats(self, sort_by: str, limit: int = 50) -> list:
|
||||
"""
|
||||
Gets a list of formatted statistics sorted by a given key.
|
||||
|
||||
Args:
|
||||
sort_by (str): The key to sort statistics by. Valid keys include:
|
||||
'calls', 'ncalls', 'tottime', 'cumulative', 'cumtime'.
|
||||
limit (int): The maximum number of rows to return.
|
||||
|
||||
Returns:
|
||||
list: A list of tuples, where each tuple represents a function's
|
||||
profile data: (ncalls, tottime, percall_tottime, cumtime,
|
||||
percall_cumtime, filename:lineno(function)).
|
||||
Returns an empty list if no stats are loaded.
|
||||
"""
|
||||
def get_stats(self, sort_by: str, limit: int = 200) -> list:
|
||||
if not self.stats:
|
||||
return []
|
||||
|
||||
# Redirect stdout to capture the output of print_stats
|
||||
s = StringIO()
|
||||
# The 'pstats.Stats' constructor can take a stream argument.
|
||||
# We re-create the object to direct its output to our StringIO stream.
|
||||
# This is a standard pattern for capturing pstats output.
|
||||
stats_to_print = pstats.Stats(self.profile_path, stream=s)
|
||||
stats_to_print.strip_dirs()
|
||||
stats_to_print.sort_stats(sort_by)
|
||||
|
||||
# print_stats(limit) prints the top 'limit' entries
|
||||
stats_to_print.print_stats(limit)
|
||||
|
||||
s.seek(0) # Rewind the stream to the beginning
|
||||
|
||||
# --- Parse the captured string output into a structured list ---
|
||||
s.seek(0)
|
||||
lines = s.getvalue().splitlines()
|
||||
|
||||
results = []
|
||||
# Find the start of the data table (after the header lines)
|
||||
data_started = False
|
||||
for line in lines:
|
||||
if not line.strip():
|
||||
continue
|
||||
if not line.strip(): continue
|
||||
if 'ncalls' in line and 'tottime' in line:
|
||||
data_started = True
|
||||
continue
|
||||
|
||||
if data_started:
|
||||
# pstats output is space-separated, but function names can have spaces.
|
||||
# A robust way is to split by whitespace a fixed number of times.
|
||||
parts = line.strip().split(maxsplit=5)
|
||||
if len(parts) == 6:
|
||||
try:
|
||||
# Convert numeric parts to float, leave function name as string
|
||||
ncalls = parts[0] # ncalls can be "x/y", so keep as string
|
||||
tottime = float(parts[1])
|
||||
percall_tottime = float(parts[2])
|
||||
cumtime = float(parts[3])
|
||||
percall_cumtime = float(parts[4])
|
||||
func_info = parts[5]
|
||||
|
||||
results.append((
|
||||
ncalls, tottime, percall_tottime, cumtime,
|
||||
percall_cumtime, func_info
|
||||
))
|
||||
results.append((parts[0], float(parts[1]), float(parts[2]), float(parts[3]), float(parts[4]), parts[5]))
|
||||
except (ValueError, IndexError):
|
||||
# Skip lines that don't parse correctly
|
||||
continue
|
||||
|
||||
return results
|
||||
|
||||
def run_and_profile_script(profile: LaunchProfile) -> Optional[str]:
|
||||
"""Runs a target Python script/module under cProfile."""
|
||||
|
||||
# --- Output File Path Definition (prima di tutto) ---
|
||||
output_dir = os.path.join(os.getcwd(), "execution_profiles")
|
||||
os.makedirs(output_dir, exist_ok=True)
|
||||
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||
profile_name_sanitized = profile.name.replace(' ', '_').replace('.', '_')
|
||||
# **CORREZIONE**: Creare un percorso assoluto fin da subito
|
||||
profile_output_path = os.path.abspath(os.path.join(output_dir, f"{timestamp}_{profile_name_sanitized}.prof"))
|
||||
|
||||
# --- Command and Working Directory Definition ---
|
||||
command = [profile.python_interpreter, "-m", "cProfile", "-o", profile_output_path]
|
||||
working_directory = None
|
||||
|
||||
if profile.run_as_module:
|
||||
if not profile.module_name:
|
||||
print("Error: Module name is required.")
|
||||
return None
|
||||
# target_path in module mode is the project root
|
||||
if not profile.target_path or not os.path.isdir(profile.target_path):
|
||||
print(f"Error: Project Root Folder is not a valid directory: {profile.target_path}")
|
||||
return None
|
||||
|
||||
# **CORREZIONE**: la sintassi corretta è '-m' seguito dal nome del modulo
|
||||
command.extend(["-m", profile.module_name])
|
||||
working_directory = profile.target_path
|
||||
else:
|
||||
# **CORREZIONE**: target_path è il percorso dello script
|
||||
if not profile.target_path or not os.path.exists(profile.target_path):
|
||||
print(f"Error: Script path does not exist: {profile.target_path}")
|
||||
return None
|
||||
command.append(profile.target_path)
|
||||
working_directory = os.path.dirname(profile.target_path)
|
||||
|
||||
if profile.script_args:
|
||||
command.extend(profile.script_args.split())
|
||||
|
||||
try:
|
||||
print(f"Executing profiling command: {' '.join(command)}")
|
||||
print(f"Working Directory for subprocess: {working_directory}")
|
||||
|
||||
process = subprocess.Popen(
|
||||
command,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
text=True,
|
||||
cwd=working_directory
|
||||
)
|
||||
stdout, stderr = process.communicate()
|
||||
|
||||
if stdout: print(f"--- STDOUT ---\n{stdout}")
|
||||
if stderr: print(f"--- STDERR ---\n{stderr}")
|
||||
|
||||
if process.returncode != 0:
|
||||
print(f"Warning: Profiled script exited with non-zero status: {process.returncode}")
|
||||
|
||||
if not os.path.exists(profile_output_path) or os.path.getsize(profile_output_path) == 0:
|
||||
print("Error: Profiling failed and no output file was generated.")
|
||||
return None
|
||||
|
||||
print(f"Profiling data saved to: {profile_output_path}")
|
||||
return profile_output_path
|
||||
|
||||
except Exception as e:
|
||||
print(f"Failed to run and profile script: {e}")
|
||||
return None
|
||||
79
profileanalyzer/core/profile_manager.py
Normal file
79
profileanalyzer/core/profile_manager.py
Normal file
@ -0,0 +1,79 @@
|
||||
# profileAnalyzer/core/profile_manager.py
|
||||
|
||||
"""
|
||||
Manages loading and saving of launch profiles from a JSON file.
|
||||
"""
|
||||
import json
|
||||
import os
|
||||
from dataclasses import asdict, is_dataclass, fields
|
||||
from typing import List, Optional
|
||||
|
||||
from .core import LaunchProfile
|
||||
|
||||
class LaunchProfileManager:
|
||||
"""Handles the persistence of a list of LaunchProfile objects."""
|
||||
def __init__(self, filepath: str):
|
||||
self.filepath = filepath
|
||||
self.profiles: List[LaunchProfile] = []
|
||||
self.load_profiles()
|
||||
|
||||
def load_profiles(self):
|
||||
"""Loads launch profiles from the JSON file."""
|
||||
if not os.path.exists(self.filepath):
|
||||
self.profiles = []
|
||||
return
|
||||
|
||||
try:
|
||||
with open(self.filepath, 'r', encoding='utf-8') as f:
|
||||
data = json.load(f)
|
||||
self.profiles = []
|
||||
for profile_data in data:
|
||||
# To handle old profiles gracefully, only pass known fields
|
||||
known_fields = {f.name for f in fields(LaunchProfile)}
|
||||
filtered_data = {k: v for k, v in profile_data.items() if k in known_fields}
|
||||
self.profiles.append(LaunchProfile(**filtered_data))
|
||||
except (json.JSONDecodeError, TypeError) as e:
|
||||
print(f"Error reading or parsing profiles file '{self.filepath}': {e}")
|
||||
self.profiles = []
|
||||
|
||||
def save_profiles(self):
|
||||
"""Saves the current list of profiles to the JSON file."""
|
||||
try:
|
||||
data_to_save = [asdict(profile) for profile in self.profiles]
|
||||
with open(self.filepath, 'w', encoding='utf-8') as f:
|
||||
json.dump(data_to_save, f, indent=4)
|
||||
except IOError as e:
|
||||
print(f"Error saving profiles to file '{self.filepath}': {e}")
|
||||
|
||||
def get_profiles(self) -> List[LaunchProfile]:
|
||||
return self.profiles
|
||||
|
||||
def get_profile_by_name(self, name: str) -> Optional[LaunchProfile]:
|
||||
return next((p for p in self.profiles if p.name == name), None)
|
||||
|
||||
def add_profile(self, profile: LaunchProfile) -> bool:
|
||||
if self.get_profile_by_name(profile.name):
|
||||
print(f"Error: A profile with the name '{profile.name}' already exists.")
|
||||
return False
|
||||
self.profiles.append(profile)
|
||||
self.save_profiles()
|
||||
return True
|
||||
|
||||
def update_profile(self, profile_name: str, updated_profile: LaunchProfile) -> bool:
|
||||
if profile_name != updated_profile.name and self.get_profile_by_name(updated_profile.name):
|
||||
print(f"Error: Cannot rename to '{updated_profile.name}', as it already exists.")
|
||||
return False
|
||||
for i, p in enumerate(self.profiles):
|
||||
if p.name == profile_name:
|
||||
self.profiles[i] = updated_profile
|
||||
self.save_profiles()
|
||||
return True
|
||||
return False
|
||||
|
||||
def delete_profile(self, profile_name: str) -> bool:
|
||||
profile_to_delete = self.get_profile_by_name(profile_name)
|
||||
if profile_to_delete:
|
||||
self.profiles.remove(profile_to_delete)
|
||||
self.save_profiles()
|
||||
return True
|
||||
return False
|
||||
@ -6,8 +6,15 @@ 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
|
||||
import threading
|
||||
import os
|
||||
from typing import Optional
|
||||
|
||||
# Correctly import from sibling directories
|
||||
from profileanalyzer.core.core import ProfileAnalyzer, LaunchProfile, run_and_profile_script
|
||||
from profileanalyzer.core.profile_manager import LaunchProfileManager
|
||||
from profileanalyzer.gui.launch_manager_window import LaunchManagerWindow
|
||||
|
||||
class ProfileAnalyzerGUI(tk.Frame):
|
||||
"""
|
||||
@ -21,12 +28,9 @@ class ProfileAnalyzerGUI(tk.Frame):
|
||||
"Function Name": "filename"
|
||||
}
|
||||
|
||||
# Mapping from Treeview column ID to pstats sort key
|
||||
COLUMN_SORT_MAP = {
|
||||
"ncalls": "ncalls",
|
||||
"tottime": "tottime",
|
||||
"cumtime": "cumulative",
|
||||
"function": "filename"
|
||||
"ncalls": "ncalls", "tottime": "tottime",
|
||||
"cumtime": "cumulative", "function": "filename"
|
||||
}
|
||||
|
||||
def __init__(self, master: tk.Tk):
|
||||
@ -36,6 +40,7 @@ class ProfileAnalyzerGUI(tk.Frame):
|
||||
self.master.geometry("1200x700")
|
||||
|
||||
self.analyzer = ProfileAnalyzer()
|
||||
self.profile_manager = LaunchProfileManager("launch_profiles.json")
|
||||
self.current_stats_data = []
|
||||
|
||||
self.loaded_filepath_var = tk.StringVar(value="No profile loaded.")
|
||||
@ -49,22 +54,19 @@ class ProfileAnalyzerGUI(tk.Frame):
|
||||
|
||||
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 ---
|
||||
ttk.Button(top_frame, text="Load Profile File...", command=self._load_profile).grid(row=0, column=0, padx=(0, 5))
|
||||
ttk.Button(top_frame, text="Profile a Script...", command=self._open_launch_manager).grid(row=0, column=1, padx=5)
|
||||
ttk.Button(top_frame, text="Export to CSV...", command=self._export_to_csv).grid(row=0, column=2, padx=5)
|
||||
|
||||
loaded_file_label = ttk.Label(top_frame, text="Current Profile:")
|
||||
loaded_file_label.grid(row=0, column=3, sticky="w", padx=(10, 0))
|
||||
|
||||
loaded_file_display = ttk.Label(top_frame, textvariable=self.loaded_filepath_var, anchor="w", relief="sunken")
|
||||
loaded_file_display.grid(row=0, column=4, sticky="ew", padx=5)
|
||||
top_frame.columnconfigure(4, weight=1)
|
||||
|
||||
notebook = ttk.Notebook(self)
|
||||
notebook.pack(fill=tk.BOTH, expand=True)
|
||||
|
||||
@ -80,47 +82,39 @@ class ProfileAnalyzerGUI(tk.Frame):
|
||||
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))
|
||||
ttk.Label(bottom_frame, text="Sort by:").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)
|
||||
self.tree.column(col_id, width=width, stretch=(col_id == "function"), anchor="w" if col_id == "function" else "e")
|
||||
|
||||
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 = scrolledtext.ScrolledText(parent_frame, wrap=tk.NONE, state=tk.DISABLED, font=("Courier New", 9))
|
||||
self.text_view.pack(fill=tk.BOTH, expand=True)
|
||||
|
||||
def _load_profile(self):
|
||||
def _load_profile(self, filepath=None):
|
||||
if not filepath:
|
||||
filepath = filedialog.askopenfilename(
|
||||
title="Select a .prof file",
|
||||
filetypes=[("Profile files", "*.prof"), ("All files", "*.*")]
|
||||
@ -135,17 +129,48 @@ class ProfileAnalyzerGUI(tk.Frame):
|
||||
self._clear_views()
|
||||
messagebox.showerror("Error", f"Could not load or parse profile file:\n{filepath}")
|
||||
|
||||
def _open_launch_manager(self):
|
||||
"""Opens the Launch Profile Manager window."""
|
||||
manager_window = LaunchManagerWindow(self, self.profile_manager)
|
||||
self.wait_window(manager_window) # Wait for the manager window to close
|
||||
|
||||
if manager_window.selected_profile_to_run:
|
||||
profile_to_run = manager_window.selected_profile_to_run
|
||||
self._start_profiling_thread(profile_to_run)
|
||||
|
||||
def _start_profiling_thread(self, profile: LaunchProfile):
|
||||
"""Runs the profiling in a separate thread to avoid blocking the GUI."""
|
||||
self.master.config(cursor="watch")
|
||||
self.loaded_filepath_var.set(f"Profiling '{profile.name}' in progress...")
|
||||
|
||||
thread = threading.Thread(
|
||||
target=lambda: self._run_profiling_and_callback(profile),
|
||||
daemon=True
|
||||
)
|
||||
thread.start()
|
||||
|
||||
def _run_profiling_and_callback(self, profile: LaunchProfile):
|
||||
"""Target for the thread: runs profiling and schedules the callback."""
|
||||
output_path = run_and_profile_script(profile)
|
||||
# Schedule the completion handler to run in the main GUI thread
|
||||
self.master.after(0, self._on_profiling_complete, output_path)
|
||||
|
||||
def _on_profiling_complete(self, profile_path: Optional[str]):
|
||||
"""Callback executed in the main thread after profiling is done."""
|
||||
self.master.config(cursor="")
|
||||
if profile_path and os.path.exists(profile_path):
|
||||
self._load_profile(filepath=profile_path)
|
||||
else:
|
||||
self.loaded_filepath_var.set("Profiling failed or no output file generated.")
|
||||
messagebox.showerror("Profiling Failed", "The script did not generate a valid profile file. See the console output for details.")
|
||||
|
||||
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):
|
||||
@ -158,41 +183,31 @@ class ProfileAnalyzerGUI(tk.Frame):
|
||||
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"
|
||||
header = f"{'N-Calls':>12s} {'Total Time':>15s} {'Per Call':>12s} {'Cum. Time':>15s} {'Per Call':>12s} {'Function'}\n"
|
||||
separator = f"{'-'*12} {'-'*15} {'-'*12} {'-'*15} {'-'*12} {'-'*40}\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"
|
||||
line = f"{str(row[0]):>12s} {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.")
|
||||
messagebox.showwarning("No Data", "No profile data to export.")
|
||||
return
|
||||
|
||||
filepath = filedialog.asksaveasfilename(
|
||||
@ -200,22 +215,18 @@ class ProfileAnalyzerGUI(tk.Frame):
|
||||
filetypes=[("CSV files", "*.csv"), ("All files", "*.*")],
|
||||
title="Save Profile Stats as CSV"
|
||||
)
|
||||
if not filepath:
|
||||
return
|
||||
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)
|
||||
|
||||
215
profileanalyzer/gui/launch_manager_window.py
Normal file
215
profileanalyzer/gui/launch_manager_window.py
Normal file
@ -0,0 +1,215 @@
|
||||
# profileAnalyzer/gui/launch_manager_window.py
|
||||
|
||||
"""
|
||||
GUI Window for managing and executing launch profiles.
|
||||
"""
|
||||
|
||||
import tkinter as tk
|
||||
from tkinter import ttk, filedialog, messagebox
|
||||
import os
|
||||
from typing import Optional
|
||||
|
||||
from ..core.core import LaunchProfile
|
||||
from ..core.profile_manager import LaunchProfileManager
|
||||
|
||||
class LaunchManagerWindow(tk.Toplevel):
|
||||
"""A Toplevel window for managing and running profiling launch profiles."""
|
||||
|
||||
def __init__(self, master, profile_manager: LaunchProfileManager):
|
||||
super().__init__(master)
|
||||
self.profile_manager = profile_manager
|
||||
self.selected_profile_to_run: Optional[LaunchProfile] = None
|
||||
|
||||
self._init_window()
|
||||
self._init_vars()
|
||||
self._create_widgets()
|
||||
self._populate_profile_list()
|
||||
|
||||
self.protocol("WM_DELETE_WINDOW", self._on_close)
|
||||
self._toggle_view() # Set initial view state
|
||||
|
||||
def _init_window(self):
|
||||
self.title("Launch Profile Manager")
|
||||
self.geometry("900x400")
|
||||
self.transient(self.master)
|
||||
self.grab_set()
|
||||
|
||||
def _init_vars(self):
|
||||
self.profile_name_var = tk.StringVar()
|
||||
self.target_path_var = tk.StringVar()
|
||||
self.module_name_var = tk.StringVar()
|
||||
self.script_args_var = tk.StringVar()
|
||||
self.run_as_module_var = tk.BooleanVar()
|
||||
self.selected_listbox_item = None
|
||||
self.run_as_module_var.trace_add("write", self._toggle_view)
|
||||
|
||||
def _create_widgets(self):
|
||||
main_frame = ttk.Frame(self, padding="10")
|
||||
main_frame.pack(fill=tk.BOTH, expand=True)
|
||||
main_frame.rowconfigure(0, weight=1)
|
||||
main_frame.columnconfigure(1, weight=3) # Detail frame expands more
|
||||
|
||||
# --- Left and Middle Frames ---
|
||||
list_action_frame = ttk.Frame(main_frame)
|
||||
list_action_frame.grid(row=0, column=0, sticky="ns", padx=(0, 10))
|
||||
list_action_frame.rowconfigure(0, weight=1)
|
||||
|
||||
list_frame = ttk.LabelFrame(list_action_frame, text="Profiles")
|
||||
list_frame.pack(fill=tk.BOTH, expand=True)
|
||||
list_frame.rowconfigure(0, weight=1)
|
||||
list_frame.columnconfigure(0, weight=1)
|
||||
|
||||
self.profile_listbox = tk.Listbox(list_frame, exportselection=False)
|
||||
self.profile_listbox.grid(row=0, column=0, sticky="nsew")
|
||||
self.profile_listbox.bind("<<ListboxSelect>>", self._on_profile_select)
|
||||
|
||||
list_scrollbar = ttk.Scrollbar(list_frame, orient="vertical", command=self.profile_listbox.yview)
|
||||
list_scrollbar.grid(row=0, column=1, sticky="ns")
|
||||
self.profile_listbox.config(yscrollcommand=list_scrollbar.set)
|
||||
|
||||
action_btn_frame = ttk.Frame(list_action_frame)
|
||||
action_btn_frame.pack(fill=tk.X, pady=5)
|
||||
ttk.Button(action_btn_frame, text="New", command=self._on_new).pack(side=tk.LEFT, expand=True, fill=tk.X)
|
||||
ttk.Button(action_btn_frame, text="Delete", command=self._on_delete).pack(side=tk.LEFT, expand=True, fill=tk.X)
|
||||
|
||||
# --- Right Frame: Profile Details ---
|
||||
self.detail_frame = ttk.LabelFrame(main_frame, text="Profile Details")
|
||||
self.detail_frame.grid(row=0, column=1, sticky="nsew")
|
||||
self.detail_frame.columnconfigure(1, weight=1)
|
||||
|
||||
# --- Common Details (Name, Module Checkbox) ---
|
||||
name_frame = ttk.Frame(self.detail_frame)
|
||||
name_frame.grid(row=0, column=0, columnspan=2, sticky="ew", padx=5, pady=5)
|
||||
name_frame.columnconfigure(0, weight=1)
|
||||
|
||||
ttk.Label(name_frame, text="Profile Name:").pack(side=tk.LEFT)
|
||||
ttk.Entry(name_frame, textvariable=self.profile_name_var).pack(side=tk.LEFT, fill=tk.X, expand=True, padx=5)
|
||||
ttk.Checkbutton(name_frame, text="Run as Module", variable=self.run_as_module_var).pack(side=tk.LEFT, padx=5)
|
||||
|
||||
# --- Dynamic View Frames ---
|
||||
self.script_frame = ttk.Frame(self.detail_frame)
|
||||
self.script_frame.grid(row=1, column=0, columnspan=2, sticky="ew")
|
||||
self.script_frame.columnconfigure(1, weight=1)
|
||||
|
||||
self.module_frame = ttk.Frame(self.detail_frame)
|
||||
self.module_frame.grid(row=1, column=0, columnspan=2, sticky="ew")
|
||||
self.module_frame.columnconfigure(1, weight=1)
|
||||
|
||||
# --- Widgets for Script Mode ---
|
||||
ttk.Label(self.script_frame, text="Script Path:").grid(row=0, column=0, sticky="w", padx=5, pady=5)
|
||||
ttk.Entry(self.script_frame, textvariable=self.target_path_var).grid(row=0, column=1, sticky="ew", padx=5)
|
||||
ttk.Button(self.script_frame, text="...", width=3, command=self._browse_script).grid(row=0, column=2, padx=5)
|
||||
|
||||
# --- Widgets for Module Mode ---
|
||||
ttk.Label(self.module_frame, text="Project Root Folder:").grid(row=0, column=0, sticky="w", padx=5, pady=5)
|
||||
ttk.Entry(self.module_frame, textvariable=self.target_path_var).grid(row=0, column=1, sticky="ew", padx=5)
|
||||
ttk.Button(self.module_frame, text="...", width=3, command=self._browse_folder).grid(row=0, column=2, padx=5)
|
||||
|
||||
ttk.Label(self.module_frame, text="Module Name:").grid(row=1, column=0, sticky="w", padx=5, pady=5)
|
||||
ttk.Entry(self.module_frame, textvariable=self.module_name_var).grid(row=1, column=1, sticky="ew", padx=5)
|
||||
|
||||
# --- Common Arguments and Save Button ---
|
||||
args_frame = ttk.Frame(self.detail_frame)
|
||||
args_frame.grid(row=2, column=0, columnspan=2, sticky="ew", padx=5, pady=5)
|
||||
args_frame.columnconfigure(1, weight=1)
|
||||
ttk.Label(args_frame, text="Arguments:").grid(row=0, column=0, sticky="w")
|
||||
ttk.Entry(args_frame, textvariable=self.script_args_var).grid(row=0, column=1, sticky="ew", padx=5)
|
||||
|
||||
ttk.Button(self.detail_frame, text="Save Changes", command=self._on_save).grid(row=3, column=1, sticky="e", padx=5, pady=10)
|
||||
|
||||
bottom_frame = ttk.Frame(main_frame)
|
||||
bottom_frame.grid(row=1, column=0, columnspan=2, sticky="ew", pady=(10, 0))
|
||||
|
||||
self.run_button = ttk.Button(bottom_frame, text="Run & Profile Selected", command=self._on_run_profile, state=tk.DISABLED)
|
||||
self.run_button.pack(side=tk.RIGHT)
|
||||
|
||||
def _toggle_view(self, *args):
|
||||
if self.run_as_module_var.get():
|
||||
self.script_frame.grid_remove()
|
||||
self.module_frame.grid()
|
||||
else:
|
||||
self.module_frame.grid_remove()
|
||||
self.script_frame.grid()
|
||||
|
||||
def _populate_profile_list(self):
|
||||
self.profile_listbox.delete(0, tk.END)
|
||||
for profile in self.profile_manager.get_profiles():
|
||||
self.profile_listbox.insert(tk.END, profile.name)
|
||||
|
||||
def _on_profile_select(self, event=None):
|
||||
selection_indices = self.profile_listbox.curselection()
|
||||
if not selection_indices:
|
||||
self.selected_listbox_item = None
|
||||
self.run_button.config(state=tk.DISABLED)
|
||||
return
|
||||
|
||||
self.selected_listbox_item = self.profile_listbox.get(selection_indices[0])
|
||||
profile = self.profile_manager.get_profile_by_name(self.selected_listbox_item)
|
||||
if profile:
|
||||
self.profile_name_var.set(profile.name)
|
||||
self.target_path_var.set(profile.target_path)
|
||||
self.module_name_var.set(profile.module_name)
|
||||
self.script_args_var.set(profile.script_args)
|
||||
self.run_as_module_var.set(profile.run_as_module)
|
||||
self.run_button.config(state=tk.NORMAL)
|
||||
|
||||
def _on_new(self):
|
||||
self.profile_listbox.selection_clear(0, tk.END)
|
||||
self.selected_listbox_item = None
|
||||
self.profile_name_var.set("New Profile")
|
||||
self.target_path_var.set("")
|
||||
self.module_name_var.set("")
|
||||
self.script_args_var.set("")
|
||||
self.run_as_module_var.set(False)
|
||||
self.run_button.config(state=tk.DISABLED)
|
||||
|
||||
def _on_save(self):
|
||||
new_name = self.profile_name_var.get().strip()
|
||||
if not new_name:
|
||||
messagebox.showerror("Error", "Profile Name cannot be empty.", parent=self)
|
||||
return
|
||||
|
||||
new_profile = LaunchProfile(
|
||||
name=new_name,
|
||||
run_as_module=self.run_as_module_var.get(),
|
||||
target_path=self.target_path_var.get().strip(),
|
||||
module_name=self.module_name_var.get().strip(),
|
||||
script_args=self.script_args_var.get().strip()
|
||||
)
|
||||
|
||||
if self.selected_listbox_item and self.selected_listbox_item != new_name:
|
||||
if not self.profile_manager.update_profile(self.selected_listbox_item, new_profile):
|
||||
messagebox.showerror("Error", f"Could not rename profile. A profile with the name '{new_name}' might already exist.", parent=self)
|
||||
else:
|
||||
self.selected_listbox_item = new_name
|
||||
elif self.selected_listbox_item:
|
||||
self.profile_manager.update_profile(self.selected_listbox_item, new_profile)
|
||||
else:
|
||||
if not self.profile_manager.add_profile(new_profile):
|
||||
messagebox.showerror("Error", f"A profile with the name '{new_name}' already exists.", parent=self)
|
||||
|
||||
self._populate_profile_list()
|
||||
|
||||
def _on_delete(self):
|
||||
if not self.selected_listbox_item: return
|
||||
if messagebox.askyesno("Confirm Delete", f"Delete '{self.selected_listbox_item}'?", parent=self):
|
||||
self.profile_manager.delete_profile(self.selected_listbox_item)
|
||||
self._populate_profile_list()
|
||||
self._on_new()
|
||||
|
||||
def _browse_script(self):
|
||||
filepath = filedialog.askopenfilename(title="Select Python Script", filetypes=[("Python files", "*.py"), ("All files", "*.*")])
|
||||
if filepath: self.target_path_var.set(filepath)
|
||||
|
||||
def _browse_folder(self):
|
||||
folder_path = filedialog.askdirectory(title="Select Project Root Folder")
|
||||
if folder_path: self.target_path_var.set(folder_path)
|
||||
|
||||
def _on_run_profile(self):
|
||||
if not self.selected_listbox_item: return
|
||||
self.selected_profile_to_run = self.profile_manager.get_profile_by_name(self.selected_listbox_item)
|
||||
self.destroy()
|
||||
|
||||
def _on_close(self):
|
||||
self.selected_profile_to_run = None
|
||||
self.destroy()
|
||||
59
todo.md
Normal file
59
todo.md
Normal file
@ -0,0 +1,59 @@
|
||||
Certamente. Ora che abbiamo una base solida, possiamo pensare a funzionalità più avanzate che trasformerebbero questo semplice visualizzatore in un potente strumento di analisi, utile anche per il progetto principale.
|
||||
|
||||
Ecco alcune idee, dalle più semplici alle più complesse, che potremmo implementare:
|
||||
|
||||
### 1. Visualizzazione Grafica dei "Callers" e "Callees"
|
||||
|
||||
**Idea**: Quando l'utente seleziona una riga nel `Treeview`, potremmo mostrare in due pannelli separati:
|
||||
* **Callers**: Quali funzioni hanno chiamato la funzione selezionata e quante volte.
|
||||
* **Callees**: Quali funzioni sono state chiamate dalla funzione selezionata.
|
||||
|
||||
**Perché è utile**: Questa è la funzionalità più potente di un profiler. Ti permette di "navigare" nel grafo delle chiamate per capire non solo *quale* funzione è lenta, ma *perché* viene chiamata così spesso o da quale percorso critico. Aiuta a rispondere a domande come: "Questa funzione lenta è chiamata da una sola parte del codice o da molte? Qual è il suo impatto a cascata?".
|
||||
|
||||
**Come implementarlo**:
|
||||
1. Modificheremo `core.py`. La classe `pstats.Stats` ha già i metodi `print_callers()` e `print_callees()`. Possiamo creare due nuovi metodi in `ProfileAnalyzer` (es. `get_callers_for_func()` e `get_callees_for_func()`) che catturano e parsano l'output di questi comandi per una funzione specifica.
|
||||
2. Modificheremo `gui.py` per aggiungere due nuovi `Treeview` (o `Listbox`) in un'area inferiore della finestra, che verranno popolati quando l'utente clicca su una riga della tabella principale.
|
||||
|
||||
### 2. Filtraggio per Nome di Funzione o File
|
||||
|
||||
**Idea**: Aggiungere una casella di testo (un campo di ricerca) che permetta di filtrare la tabella per mostrare solo le funzioni che contengono una certa stringa (es. `_stream_srio_blocks`, `file_reader.py`).
|
||||
|
||||
**Perché è utile**: Quando i risultati sono centinaia, scorrere la lista è difficile. Un filtro ti permette di concentrarti immediatamente su un modulo o una funzione specifica di cui sospetti, rendendo l'analisi molto più rapida.
|
||||
|
||||
**Come implementarlo**:
|
||||
1. In `gui.py`, aggiungeremo un `ttk.Entry` per la ricerca.
|
||||
2. Collegheremo l'evento di modifica del testo (`<KeyRelease>`) a una funzione di aggiornamento.
|
||||
3. Il metodo `_update_stats_display` verrà modificato per passare il termine di ricerca al `ProfileAnalyzer`.
|
||||
4. In `core.py`, il metodo `get_stats` accetterà un nuovo argomento opzionale `filter_term`. Prima di restituire i risultati, filtrerà la lista per includere solo le righe in cui il nome della funzione (`func_info`) contiene il termine di ricerca.
|
||||
|
||||
### 3. "Flame Graph" Semplificato (Avanzato)
|
||||
|
||||
**Idea**: Un Flame Graph è una visualizzazione gerarchica che mostra lo stack di chiamate e quanto tempo viene speso in ogni funzione. Quelli completi sono complessi, ma possiamo crearne una versione testuale semplificata. Quando l'utente clicca su una funzione, potremmo mostrare un albero di testo che rappresenta il suo stack di chiamate e il tempo cumulativo.
|
||||
|
||||
**Perché è utile**: Fornisce una comprensione visiva e immediata di dove il tempo viene "consumato" all'interno di una singola operazione. È lo strumento definitivo per l'analisi delle performance.
|
||||
|
||||
**Come implementarlo**:
|
||||
* Questa è la funzionalità più complessa. Richiede di analizzare in modo ricorsivo le relazioni "caller-callee" fornite da `pstats` per costruire la gerarchia.
|
||||
* Potremmo usare un `ttk.Treeview` per visualizzare il grafo, dove ogni nodo è una funzione e può essere espanso per vedere le sue sotto-chiamate.
|
||||
|
||||
### 4. Confronto tra Due Profili (Avanzato)
|
||||
|
||||
**Idea**: Permettere all'utente di caricare due file `.prof` (es. "prima" e "dopo" un'ottimizzazione) e mostrare una vista comparativa che evidenzi le differenze:
|
||||
* Funzioni che sono diventate più veloci (regressioni positive).
|
||||
* Funzioni che sono diventate più lente (regressioni negative).
|
||||
* Funzioni che sono apparse o scomparse.
|
||||
|
||||
**Perché è utile**: È essenziale per verificare che una modifica abbia effettivamente migliorato le performance e non abbia introdotto nuovi colli di bottiglia.
|
||||
|
||||
**Come implementarlo**:
|
||||
1. La GUI dovrebbe avere due pulsanti "Load Profile A" e "Load Profile B".
|
||||
2. Il `core.py` avrebbe bisogno di una logica per "unire" i dati dei due profili, calcolare i delta percentuali per `tottime` e `cumtime`, e restituire una struttura dati comparativa.
|
||||
3. Il `Treeview` nella GUI mostrerebbe colonne aggiuntive come "Delta Time", "Delta Calls", etc., magari usando colori (verde/rosso) per evidenziare i miglioramenti e i peggioramenti.
|
||||
|
||||
---
|
||||
|
||||
**La mia raccomandazione su cosa fare ora:**
|
||||
|
||||
La **funzionalità #1 (Visualizzazione Callers/Callees)** è il passo successivo più logico e utile. È relativamente semplice da implementare usando le capacità intrinseche di `pstats` e aggiunge un valore enorme all'analisi. Ti permette di "scavare" nei dati invece di guardarli solo in superficie.
|
||||
|
||||
Se sei d'accordo, possiamo procedere con l'implementazione di questa funzionalità.
|
||||
Loading…
Reference in New Issue
Block a user