Skip to content

Commit e5d5c57

Browse files
committed
CPU Scheduling Interface
1 parent af17867 commit e5d5c57

File tree

1 file changed

+373
-0
lines changed

1 file changed

+373
-0
lines changed
Lines changed: 373 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,373 @@
1+
import copy
2+
import threading
3+
import time
4+
import tkinter as tk
5+
from tkinter import messagebox, ttk
6+
7+
import matplotlib.pyplot as plt
8+
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
9+
from typing import Generator
10+
11+
12+
# ===================== Scheduler Engine ===================== #
13+
class SchedulerEngine:
14+
def __init__(self, processes: list[dict], algorithm: str, quantum: int = 2) -> None:
15+
"""
16+
Initializes the scheduler engine.
17+
18+
>>> processes = [{"pid": "P1", "arrival": 0, "burst": 4}]
19+
>>> engine = SchedulerEngine(processes, "FCFS")
20+
>>> isinstance(engine, SchedulerEngine)
21+
True
22+
"""
23+
self.original = copy.deepcopy(processes)
24+
self.processes = [p.copy() for p in processes]
25+
self.algorithm = algorithm
26+
self.quantum = quantum
27+
for process in self.processes:
28+
process["remaining"] = process["burst"]
29+
self.timeline: list[tuple[int, str]] = [] # [(time, pid)]
30+
self.stats: list[tuple] = []
31+
32+
def simulate(self) -> Generator[tuple[int, str | None, list[str]], None, None]:
33+
"""
34+
Runs the selected CPU scheduling algorithm.
35+
36+
>>> processes = [{"pid": "P1", "arrival": 0, "burst": 2}]
37+
>>> engine = SchedulerEngine(processes, "FCFS")
38+
>>> list(engine.simulate())[:2]
39+
[(0, 'P1', []), (1, 'P1', [])]
40+
"""
41+
algo = self.algorithm.lower()
42+
if algo == "fcfs":
43+
yield from self._simulate_fcfs()
44+
elif algo == "sjf (non-preemptive)":
45+
yield from self._simulate_sjf_np()
46+
elif algo == "sjf (preemptive)":
47+
yield from self._simulate_sjf_p()
48+
elif algo == "priority (non-preemptive)":
49+
yield from self._simulate_priority_np()
50+
elif algo == "priority (preemptive)":
51+
yield from self._simulate_priority_p()
52+
elif algo == "round robin":
53+
yield from self._simulate_rr()
54+
self._calculate_stats()
55+
56+
# first come first serve
57+
def _simulate_fcfs(self) -> Generator[tuple[int, str, list[str]], None, None]:
58+
"""
59+
Simulates First Come First Serve scheduling.
60+
61+
>>> processes = [{"pid": "P1", "arrival": 0, "burst": 2}]
62+
>>> engine = SchedulerEngine(processes, "FCFS")
63+
>>> next(engine._simulate_fcfs())
64+
(0, 'P1', [])
65+
"""
66+
t = 0
67+
processes = sorted(self.processes, key=lambda process: process["arrival"])
68+
for process in processes:
69+
t = max(t, process["arrival"])
70+
for _ in range(process["burst"]):
71+
self.timeline.append((t, process["pid"]))
72+
yield (t, process["pid"], [])
73+
t += 1
74+
process["completion"] = t
75+
76+
# shortest job first non preemptive
77+
def _simulate_sjf_np(self) -> Generator[tuple[int, str | None, list[str]], None, None]:
78+
"""
79+
Simulates Shortest Job First (Non-Preemptive).
80+
81+
>>> processes = [{"pid": "P1", "arrival": 0, "burst": 2}]
82+
>>> engine = SchedulerEngine(processes, "SJF (Non-Preemptive)")
83+
>>> isinstance(engine._simulate_sjf_np(), Generator)
84+
True
85+
"""
86+
t = 0
87+
processes = sorted(self.processes, key=lambda process: process["arrival"])
88+
done = 0
89+
while done < len(processes):
90+
ready = [process for process in processes if process["arrival"] <= t and "completion" not in process]
91+
if not ready:
92+
t += 1
93+
yield (t, None, [])
94+
continue
95+
process = min(ready, key=lambda x: x["burst"])
96+
for _ in range(process["burst"]):
97+
self.timeline.append((t, process["pid"]))
98+
yield (t, process["pid"], [])
99+
t += 1
100+
process["completion"] = t
101+
done += 1
102+
103+
# shortest job first preemptive
104+
def _simulate_sjf_p(self) -> Generator[tuple[int, str | None, list[str]], None, None]:
105+
"""Simulates SJF Preemptive scheduling."""
106+
t = 0
107+
processes = sorted(self.processes, key=lambda process: process["arrival"])
108+
done = 0
109+
while done < len(processes):
110+
ready = [process for process in processes if process["arrival"] <= t and process["remaining"] > 0]
111+
if not ready:
112+
t += 1
113+
yield (t, None, [])
114+
continue
115+
process = min(ready, key=lambda x: x["remaining"])
116+
self.timeline.append((t, process["pid"]))
117+
yield (t, process["pid"], [])
118+
process["remaining"] -= 1
119+
if process["remaining"] == 0:
120+
process["completion"] = t + 1
121+
done += 1
122+
t += 1
123+
124+
# priority non preemptive
125+
def _simulate_priority_np(self) -> Generator[tuple[int, str | None, list[str]], None, None]:
126+
"""Simulates Priority (Non-Preemptive) scheduling."""
127+
t = 0
128+
done = 0
129+
while done < len(self.processes):
130+
ready = [process for process in self.processes if process["arrival"] <= t and "completion" not in process]
131+
if not ready:
132+
t += 1
133+
yield (t, None, [])
134+
continue
135+
process = min(ready, key=lambda x: x["priority"])
136+
for _ in range(process["burst"]):
137+
self.timeline.append((t, process["pid"]))
138+
yield (t, process["pid"], [])
139+
t += 1
140+
process["completion"] = t
141+
done += 1
142+
143+
# priority preemptive
144+
def _simulate_priority_p(self) -> Generator[tuple[int, str | None, list[str]], None, None]:
145+
"""Simulates Priority (Preemptive) scheduling."""
146+
t = 0
147+
done = 0
148+
while done < len(self.processes):
149+
ready = [process for process in self.processes if process["arrival"] <= t and process["remaining"] > 0]
150+
if not ready:
151+
t += 1
152+
yield (t, None, [])
153+
continue
154+
process = min(ready, key=lambda x: x["priority"])
155+
self.timeline.append((t, process["pid"]))
156+
yield (t, process["pid"], [])
157+
process["remaining"] -= 1
158+
if process["remaining"] == 0:
159+
process["completion"] = t + 1
160+
done += 1
161+
t += 1
162+
163+
# round robin
164+
def _simulate_rr(self) -> Generator[tuple[int, str | None, list[str]], None, None]:
165+
"""Simulates Round Robin scheduling."""
166+
t = 0
167+
q: list[dict] = []
168+
processes = sorted(self.processes, key=lambda process: process["arrival"])
169+
i = 0
170+
done = 0
171+
while done < len(processes):
172+
while i < len(processes) and processes[i]["arrival"] <= t:
173+
q.append(processes[i])
174+
i += 1
175+
if not q:
176+
t += 1
177+
yield (t, None, [])
178+
continue
179+
process = q.pop(0)
180+
burst = min(self.quantum, process["remaining"])
181+
for _ in range(burst):
182+
self.timeline.append((t, process["pid"]))
183+
yield (t, process["pid"], [x["pid"] for x in q])
184+
t += 1
185+
process["remaining"] -= 1
186+
while i < len(processes) and processes[i]["arrival"] <= t:
187+
q.append(processes[i])
188+
i += 1
189+
if process["remaining"] > 0:
190+
q.append(process)
191+
else:
192+
process["completion"] = t
193+
done += 1
194+
195+
def _calculate_stats(self) -> None:
196+
"""Calculates turnaround, waiting, and response times."""
197+
for process in self.processes:
198+
pid = process["pid"]
199+
arrival = process["arrival"]
200+
burst = process["burst"]
201+
completion = process["completion"]
202+
first_exec = next((t for t, pid2 in self.timeline if pid2 == pid), arrival)
203+
tat = completion - arrival
204+
wt = tat - burst
205+
rt = first_exec - arrival
206+
self.stats.append((pid, arrival, burst, completion, tat, wt, rt))
207+
208+
209+
# ===================== Interface ===================== #
210+
class CPUSchedulerGUI:
211+
def __init__(self, root: tk.Tk) -> None:
212+
"""Initializes the GUI window."""
213+
self.root = root
214+
self.root.title("CPU Scheduling Visualizer")
215+
self.root.geometry("1000x700")
216+
self.processes: list[dict] = []
217+
self.setup_ui()
218+
219+
def setup_ui(self) -> None:
220+
"""Sets up GUI widgets."""
221+
top_frame = ttk.Frame(self.root)
222+
top_frame.pack(pady=10)
223+
224+
self.tree = ttk.Treeview(
225+
top_frame,
226+
columns=("pid", "arrival", "burst", "priority"),
227+
show="headings",
228+
)
229+
for col in self.tree["columns"]:
230+
self.tree.heading(col, text=col.capitalize())
231+
self.tree.pack(side="left")
232+
233+
form = ttk.Frame(top_frame)
234+
form.pack(side="left", padx=10)
235+
ttk.Label(form, text="PID").grid(row=0, column=0)
236+
ttk.Label(form, text="Arrival").grid(row=1, column=0)
237+
ttk.Label(form, text="Burst").grid(row=2, column=0)
238+
ttk.Label(form, text="Priority").grid(row=3, column=0)
239+
self.pid_e = ttk.Entry(form)
240+
self.arrival_e = ttk.Entry(form)
241+
self.burst_e = ttk.Entry(form)
242+
self.priority_e = ttk.Entry(form)
243+
self.pid_e.grid(row=0, column=1)
244+
self.arrival_e.grid(row=1, column=1)
245+
self.burst_e.grid(row=2, column=1)
246+
self.priority_e.grid(row=3, column=1)
247+
ttk.Button(form, text="Add", command=self.add_process).grid(row=4, column=0, pady=5)
248+
ttk.Button(form, text="Delete", command=self.delete_process).grid(row=4, column=1)
249+
250+
algo_frame = ttk.Frame(self.root)
251+
algo_frame.pack(pady=10)
252+
ttk.Label(algo_frame, text="Algorithm:").pack(side="left")
253+
self.algo_cb = ttk.Combobox(
254+
algo_frame,
255+
values=[
256+
"FCFS",
257+
"SJF (Non-Preemptive)",
258+
"SJF (Preemptive)",
259+
"Priority (Non-Preemptive)",
260+
"Priority (Preemptive)",
261+
"Round Robin",
262+
],
263+
)
264+
self.algo_cb.current(0)
265+
self.algo_cb.pack(side="left", padx=5)
266+
ttk.Label(algo_frame, text="Quantum:").pack(side="left")
267+
self.quantum_e = ttk.Entry(algo_frame, width=5)
268+
self.quantum_e.insert(0, "2")
269+
self.quantum_e.pack(side="left")
270+
ttk.Button(algo_frame, text="Run", command=self.run_scheduling).pack(side="left", padx=10)
271+
272+
self.ready_label = ttk.Label(self.root, text="Ready Queue:")
273+
self.ready_list = tk.Listbox(self.root, height=3)
274+
self.ready_label.pack_forget()
275+
self.ready_list.pack_forget()
276+
277+
self.figure, self.ax = plt.subplots(figsize=(8, 3))
278+
self.canvas = FigureCanvasTkAgg(self.figure, master=self.root)
279+
self.canvas.get_tk_widget().pack()
280+
281+
self.result_box = ttk.Treeview(
282+
self.root,
283+
columns=("pid", "arrival", "burst", "completion", "tat", "wt", "rt"),
284+
show="headings",
285+
height=6,
286+
)
287+
for col in self.result_box["columns"]:
288+
self.result_box.heading(col, text=col.upper())
289+
self.result_box.pack(pady=10)
290+
291+
self.avg_label = ttk.Label(self.root, text="", font=("Arial", 11, "bold"))
292+
self.avg_label.pack()
293+
294+
def add_process(self) -> None:
295+
"""Adds a new process entry to the table."""
296+
try:
297+
pid = self.pid_e.get()
298+
arrival = int(self.arrival_e.get())
299+
burst = int(self.burst_e.get())
300+
priority = int(self.priority_e.get() or 0)
301+
self.processes.append(
302+
{"pid": pid, "arrival": arrival, "burst": burst, "priority": priority}
303+
)
304+
self.tree.insert("", "end", values=(pid, arrival, burst, priority))
305+
except ValueError:
306+
messagebox.showerror("Error", "Invalid input")
307+
308+
def delete_process(self) -> None:
309+
"""Deletes a selected process."""
310+
if sel := self.tree.selection():
311+
pid = self.tree.item(sel[0])["values"][0]
312+
self.processes = [p for p in self.processes if p["pid"] != pid]
313+
self.tree.delete(sel[0])
314+
315+
def run_scheduling(self) -> None:
316+
"""Runs the selected scheduling algorithm."""
317+
algo = self.algo_cb.get()
318+
quantum = int(self.quantum_e.get() or 2)
319+
if algo.lower() == "round robin":
320+
self.ready_label.pack()
321+
self.ready_list.pack()
322+
else:
323+
self.ready_label.pack_forget()
324+
self.ready_list.pack_forget()
325+
326+
self.engine = SchedulerEngine(self.processes, algo, quantum)
327+
threading.Thread(target=self.animate, daemon=True).start()
328+
329+
def animate(self) -> None:
330+
"""Animates the scheduling visualization."""
331+
self.ax.clear()
332+
x, colors, current_pid = 0, {}, None
333+
for step in self.engine.simulate():
334+
_, pid, rq = step
335+
if pid:
336+
if pid != current_pid:
337+
self.ax.axvline(x, color="black", linewidth=0.8)
338+
current_pid = pid
339+
colors.setdefault(pid, plt.cm.tab20(len(colors) % 20))
340+
self.ax.barh(0, 1, left=x, color=colors[pid])
341+
self.ax.text(x + 0.5, 0, pid, ha="center", va="center", color="white", fontsize=9)
342+
x += 1
343+
self.ax.set_xticks(range(x + 1))
344+
self.ax.set_yticks([])
345+
self.ax.set_xlabel("Time")
346+
self.canvas.draw()
347+
if rq:
348+
self.ready_list.delete(0, tk.END)
349+
for pid_r in rq:
350+
self.ready_list.insert(tk.END, pid_r)
351+
time.sleep(0.3)
352+
self.show_results()
353+
354+
def show_results(self) -> None:
355+
"""Displays scheduling results."""
356+
for item in self.result_box.get_children():
357+
self.result_box.delete(item)
358+
total_wt = total_tat = total_rt = 0
359+
for row in self.engine.stats:
360+
self.result_box.insert("", "end", values=row)
361+
total_wt += row[5]
362+
total_tat += row[4]
363+
total_rt += row[6]
364+
n = len(self.engine.stats) or 1
365+
self.avg_label.config(
366+
text=f"AVG WT = {total_wt/n:.2f} | AVG TAT = {total_tat/n:.2f} | AVG RT = {total_rt/n:.2f}"
367+
)
368+
369+
370+
if __name__ == "__main__":
371+
root = tk.Tk()
372+
CPUSchedulerGUI(root)
373+
root.mainloop()

0 commit comments

Comments
 (0)