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:
VALLONGOL 2025-06-23 14:07:01 +02:00
parent 986aaa32de
commit e766172c47
7 changed files with 537 additions and 126 deletions

3
.gitignore vendored
View File

@ -148,3 +148,6 @@ dmypy.json
# Temporary files
*.swp
*~
*.prof

18
launch_profiles.json Normal file
View 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"
}
]

View File

@ -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

View 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

View File

@ -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,51 +82,43 @@ 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):
filepath = filedialog.askopenfilename(
title="Select a .prof file",
filetypes=[("Profile files", "*.prof"), ("All files", "*.*")]
)
def _load_profile(self, filepath=None):
if not filepath:
filepath = filedialog.askopenfilename(
title="Select a .prof file",
filetypes=[("Profile files", "*.prof"), ("All files", "*.*")]
)
if not filepath: return
if self.analyzer.load_profile(filepath):
@ -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)

View 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
View 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à.