Saturday, June 13, 2026

How to Add a Real-Time Performance Graph to Your Python Task Manager

Monitoring process lists tells you what is happening right now, but it doesn't give you the full picture. To properly diagnose system performance spikes or track memory leaks over time, IT admins need a visual timeline showing resource historical usage trends.

Instead of introducing complex, heavy visualization packages like matplotlib, we can build a lightweight, native scrolling performance graph using Python's built-in tkinter canvas component. This keeps our file small, zero-dependency, and highly responsive.

Here is the complete HTML framework and Python script to add a real-time historical graph tracker to your helpdesk blog.


🐍 The Python Tkinter Script with Historical Graphing

This upgraded version maintains a rolling history of your last 50 CPU utilization measurements and plots them visually on an auto-refreshing tracking canvas grid. Save this script layout file as graph_task_manager.py:

import tkinter as tk
from tkinter import ttk, messagebox
import psutil

class GraphTaskManagerGUI:
    def __init__(self, root):
        self.root = root
        self.root.title("Ayouli IT Tech: Advanced Performance Tracker")
        self.root.geometry("700x550")
        self.root.minsize(600, 450)

        # Storage array for rolling historical CPU measurements
        self.cpu_history = [0] * 50 

        # Top Control Frame for global usage readouts
        self.stats_frame = ttk.LabelFrame(root, text=" Live Hardware Utilization ", padding=10)
        self.stats_frame.pack(fill="x", padx=10, pady=5)

        self.cpu_label = ttk.Label(self.stats_frame, text="CPU Usage: 0%", font=("Arial", 10, "bold"))
        self.cpu_label.pack(side="left", padx=20)

        self.ram_label = ttk.Label(self.stats_frame, text="RAM Usage: 0%", font=("Arial", 10, "bold"))
        self.ram_label.pack(side="left", padx=20)

        # Real-time Historical Graph Canvas Object
        self.graph_frame = ttk.LabelFrame(root, text=" CPU Usage History (Rolling Timeline) ", padding=5)
        self.graph_frame.pack(fill="x", padx=10, pady=5)

        # Drawing board layout matrix
        self.canvas = tk.Canvas(self.graph_frame, height=100, bg="#1e1e1e", highlightthickness=0)
        self.canvas.pack(fill="x", padx=5, pady=5)

        # Central Process List Treeview Grid Layout
        self.list_frame = ttk.Frame(root, padding=10)
        self.list_frame.pack(fill="both", expand=True)

        columns = ("pid", "name", "cpu", "ram")
        self.tree = ttk.Treeview(self.list_frame, columns=columns, show="headings", selectmode="browse")
        
        self.tree.heading("pid", text="PID")
        self.tree.heading("name", text="Process Name")
        self.tree.heading("cpu", text="CPU %")
        self.tree.heading("ram", text="RAM %")

        self.tree.column("pid", width=70, anchor="center")
        self.tree.column("name", width=250, anchor="w")
        self.tree.column("cpu", width=80, anchor="center")
        self.tree.column("ram", width=80, anchor="center")

        scrollbar = ttk.Scrollbar(self.list_frame, orient="vertical", command=self.tree.yview)
        self.tree.configure(yscrollcommand=scrollbar.set)
        
        self.tree.pack(side="left", fill="both", expand=True)
        scrollbar.pack(side="right", fill="y")

        # Bottom Functional Action Row
        self.action_frame = ttk.Frame(root, padding=10)
        self.action_frame.pack(fill="x")

        self.kill_button = ttk.Button(self.action_frame, text="End Task Process", command=self.kill_process)
        self.kill_button.pack(side="right", padx=10)

        # Kickoff structural application loops
        self.update_metrics()

    def draw_historical_graph(self):
        """Clears the drawing canvas and renders the line path graph."""
        self.canvas.delete("all")
        
        canvas_width = self.canvas.winfo_width()
        if canvas_width < 10:
            canvas_width = 650 # Fallback default width constraint values
            
        points_count = len(self.cpu_history)
        x_spacing = canvas_width / (points_count - 1)
        
        # Build graphical node coordinate array mapping layouts
        coordinates = []
        for index, val in enumerate(self.cpu_history):
            x = index * x_spacing
            # Invert coordinates because Canvas 0 point is the top-left edge
            y = 100 - val 
            coordinates.append((x, y))
            
        # Draw smooth interconnecting path segments
        for i in range(len(coordinates) - 1):
            x1, y1 = coordinates[i]
            x2, y2 = coordinates[i+1]
            self.canvas.create_line(x1, y1, x2, y2, fill="#0078d4", width=2, smooth=True)

    def update_metrics(self):
        """Fetches system process data and pushes tracking updates."""
        cpu_percent = psutil.cpu_percent()
        ram_percent = psutil.virtual_memory().percent
        
        self.cpu_label.config(text=f"CPU Usage: {cpu_percent}%")
        self.ram_label.config(text=f"RAM Usage: {ram_percent}%")

        # Shift rolling layout variables over to log history data
        self.cpu_history.pop(0)
        self.cpu_history.append(cpu_percent)
        
        # Redraw fresh timeline line chart paths
        self.draw_historical_graph()

        # Update process list view matching logic
        selected_item = self.tree.selection()
        selected_pid = None
        if selected_item:
            selected_pid = self.tree.item(selected_item)['values']

        for item in self.tree.get_children():
            self.tree.delete(item)

        processes = []
        for proc in psutil.process_iter(['pid', 'name', 'cpu_percent', 'memory_percent']):
            try:
                processes.append(proc.info)
            except (psutil.NoSuchProcess, psutil.AccessDenied):
                continue

        sorted_processes = sorted(processes, key=lambda x: x['memory_percent'] or 0, reverse=True)

        for p in sorted_processes[:20]:
            item_id = self.tree.insert("", "end", values=(
                p['pid'], 
                p['name'], 
                f"{p['cpu_percent']:.1f}", 
                f"{p['memory_percent']:.1f}"
            ))
            if selected_pid and p['pid'] == selected_pid:
                self.tree.selection_set(item_id)

        # Loop script refresh execution sequence every 1 second (1000 milliseconds)
        self.root.after(1000, self.update_metrics)

    def kill_process(self):
        """Identifies target rows and initializes program execution drops."""
        selected_item = self.tree.selection()
        if not selected_item:
            messagebox.showwarning("Selection Missing", "Please pick a live process trace row.")
            return

        pid, name, _, _ = self.tree.item(selected_item)['values']
        confirm = messagebox.askyesno("Confirm", f"Kill process {name} (PID: {pid})?")
        if confirm:
            try:
                psutil.Process(pid).terminate()
                messagebox.showinfo("Success", f"Process {name} terminated.")
            except psutil.AccessDenied:
                messagebox.showerror("Access Error", "Admin context required to kill this program loop.")
            except psutil.NoSuchProcess:
                messagebox.showerror("Missing Process", "This tracking thread already changed execution paths.")

if __name__ == "__main__":
    app_root = tk.Tk()
    manager = GraphTaskManagerGUI(app_root)
    app_root.mainloop()

🎯 Canvas Engineering Details for Tech Admins

  • The Origin Trap: In graphical UI rendering engines, coordinate (0,0) sits at the **top-left corner**. To prevent your graph from drawing upside down, our code explicitly maps performance using 100 - val, flipping the line tracking properly.
  • Responsive Resizing: The self.canvas.winfo_width() call dynamic calculates your exact application frame footprint. If you stretch the user interface window, the tracking line segments expand horizontally automatically.

No comments:

Post a Comment