Visualizing Simulation Results¶
This tutorial demonstrates how to visualize scheduling simulation results using matplotlib. You will learn how to produce response time histograms, frequency time-series plots, active-core count charts, and energy breakdown bar charts — all from a single simulation run.
Prerequisites¶
Note
This tutorial requires matplotlib (pip install matplotlib). All other
tutorials work without external Python dependencies.
Running a Simulation¶
Set up a four-core platform with GRUB and FFA, enable energy tracking, and
collect the trace in memory. The same MemoryTraceWriter is used for both
metrics and time-series analysis.
import pyschedsim as sim
engine = sim.Engine()
platform = engine.platform
cpu = platform.add_processor_type("cpu", 1.0)
clock = platform.add_clock_domain(500.0, 2000.0, 0.0001) # 100 us transition
power = platform.add_power_domain([
(0, 0, 0.0, 100.0),
(1, 0, 0.001, 10.0),
])
for _ in range(4):
platform.add_processor(cpu, clock, power)
# Generate a scenario: 6 tasks with utilizations summing to 2.4
utils = [0.25, 0.20, 0.15, 0.35, 0.30, 0.25]
scenario = sim.from_utilizations(utils, seed=42)
tasks = sim.inject_scenario(engine, scenario)
engine.enable_energy_tracking(True)
platform.finalize()
for i, tp in enumerate(scenario.tasks):
sim.schedule_arrivals(engine, tasks[i], tp.jobs)
procs = sim.get_all_processors(engine)
scheduler = sim.EdfScheduler(engine, procs)
for task in tasks:
scheduler.add_server(task)
scheduler.enable_grub()
scheduler.enable_ffa(0.001, 1) # FFA: frequency-first DVFS + DPM
allocator = sim.SingleSchedulerAllocator(engine, scheduler)
writer = sim.MemoryTraceWriter()
engine.set_trace_writer(writer)
engine.run()
metrics = writer.compute_metrics()
Response Time Distribution¶
Plot a histogram of response times for each task. Using one subplot per task keeps the chart readable when tasks have very different time scales.
import matplotlib.pyplot as plt
plt.style.use("seaborn-v0_8-whitegrid")
rt_map = metrics.get_response_times_per_task()
n_tasks = len(rt_map)
fig, axes = plt.subplots(1, n_tasks, figsize=(3 * n_tasks, 3),
sharey=True)
for ax, (task_id, times) in zip(axes, sorted(rt_map.items())):
times_ms = [t * 1e3 for t in times] # convert to milliseconds
ax.hist(times_ms, bins=20, edgecolor="white")
ax.set_title(f"Task {task_id}")
ax.set_xlabel("Response time (ms)")
ax.set_ylabel("Count" if ax is axes[0] else "")
fig.suptitle("Response Time Distribution per Task", y=1.02)
plt.tight_layout()
plt.savefig("response_times.png", dpi=150)
plt.show()
Frequency Time-Series¶
writer.track_frequency_changes() returns a list of FrequencyInterval
objects, each with start, stop, cluster_id, and frequency
attributes. Use a step plot to visualise the piecewise-constant signal.
freq_intervals = writer.track_frequency_changes()
# Collect one series per cluster
from collections import defaultdict
cluster_freqs = defaultdict(lambda: {"times": [], "freqs": []})
for iv in freq_intervals:
series = cluster_freqs[iv.cluster_id]
series["times"].extend([iv.start, iv.stop])
series["freqs"].extend([iv.frequency, iv.frequency])
fig, ax = plt.subplots(figsize=(10, 3))
for cluster_id, series in sorted(cluster_freqs.items()):
ax.step(series["times"], series["freqs"],
where="post", label=f"Cluster {cluster_id}")
ax.set_xlabel("Time (s)")
ax.set_ylabel("Frequency (MHz)")
ax.set_title("DVFS Frequency Over Time")
ax.legend(loc="upper right")
ax.set_ylim(0)
plt.tight_layout()
plt.savefig("frequency_trace.png", dpi=150)
plt.show()
Active Core Count Over Time¶
writer.track_core_changes() returns CoreCountInterval objects with
start, stop, cluster_id, and active_cores attributes. This
shows how the DPM policy powers down idle processors.
core_intervals = writer.track_core_changes()
cluster_cores = defaultdict(lambda: {"times": [], "counts": []})
for iv in core_intervals:
series = cluster_cores[iv.cluster_id]
series["times"].extend([iv.start, iv.stop])
series["counts"].extend([iv.active_cores, iv.active_cores])
fig, ax = plt.subplots(figsize=(10, 3))
for cluster_id, series in sorted(cluster_cores.items()):
ax.step(series["times"], series["counts"],
where="post", label=f"Cluster {cluster_id}")
ax.set_xlabel("Time (s)")
ax.set_ylabel("Active cores")
ax.set_title("Active Core Count Over Time")
ax.yaxis.get_major_locator().set_params(integer=True)
ax.legend(loc="upper right")
ax.set_ylim(0)
plt.tight_layout()
plt.savefig("core_trace.png", dpi=150)
plt.show()
Energy Breakdown¶
metrics.get_energy_per_processor() returns a dict mapping processor ID
to energy in millijoules. A horizontal bar chart makes per-core differences
easy to compare.
energy_map = metrics.get_energy_per_processor()
proc_ids = sorted(energy_map.keys())
energies = [energy_map[p] for p in proc_ids]
labels = [f"Proc {p}" for p in proc_ids]
fig, ax = plt.subplots(figsize=(6, 3))
bars = ax.barh(labels, energies, color="steelblue", edgecolor="white")
ax.bar_label(bars, fmt="%.1f mJ", padding=4)
ax.set_xlabel("Energy (mJ)")
ax.set_title("Per-Processor Energy Consumption")
ax.set_xlim(0, max(energies) * 1.2)
plt.tight_layout()
plt.savefig("energy_breakdown.png", dpi=150)
plt.show()
Combined Dashboard¶
Assemble all four charts into a single 2×2 figure for papers or reports.
fig, axes = plt.subplots(2, 2, figsize=(12, 8))
fig.suptitle("Simulation Dashboard", fontsize=14, fontweight="bold")
# ── Top-left: response times (task 1 only for brevity) ───────────────────
ax = axes[0, 0]
first_task_id = sorted(rt_map.keys())[0]
times_ms = [t * 1e3 for t in rt_map[first_task_id]]
ax.hist(times_ms, bins=20, color="steelblue", edgecolor="white")
ax.set_title(f"Response Times — Task {first_task_id}")
ax.set_xlabel("Response time (ms)")
ax.set_ylabel("Count")
# ── Top-right: frequency trace ────────────────────────────────────────────
ax = axes[0, 1]
for cluster_id, series in sorted(cluster_freqs.items()):
ax.step(series["times"], series["freqs"],
where="post", label=f"Cluster {cluster_id}")
ax.set_title("DVFS Frequency")
ax.set_xlabel("Time (s)")
ax.set_ylabel("Frequency (MHz)")
ax.set_ylim(0)
ax.legend(fontsize=8)
# ── Bottom-left: active core count ────────────────────────────────────────
ax = axes[1, 0]
for cluster_id, series in sorted(cluster_cores.items()):
ax.step(series["times"], series["counts"],
where="post", label=f"Cluster {cluster_id}")
ax.set_title("Active Core Count")
ax.set_xlabel("Time (s)")
ax.set_ylabel("Active cores")
ax.yaxis.get_major_locator().set_params(integer=True)
ax.set_ylim(0)
ax.legend(fontsize=8)
# ── Bottom-right: energy breakdown ────────────────────────────────────────
ax = axes[1, 1]
bars = ax.barh(labels, energies, color="steelblue", edgecolor="white")
ax.bar_label(bars, fmt="%.1f mJ", padding=4)
ax.set_title("Per-Processor Energy")
ax.set_xlabel("Energy (mJ)")
ax.set_xlim(0, max(energies) * 1.2)
plt.tight_layout()
plt.savefig("dashboard.pdf")
plt.savefig("dashboard.png", dpi=150)
plt.show()
Saving Figures¶
# Vector PDF for publication (LaTeX-ready)
plt.savefig("results.pdf")
# Raster PNG for slides or reports
plt.savefig("results.png", dpi=150)
# High-DPI raster for posters
plt.savefig("results.png", dpi=300, bbox_inches="tight")
Use bbox_inches="tight" to prevent axis labels from being clipped at the
figure boundary.
Without matplotlib¶
If matplotlib is not available, the same data can be printed as plain text. This is useful in headless environments or CI pipelines.
# Response time summary (no plotting)
rt_map = metrics.get_response_times_per_task()
print("Response time summary")
print(f"{'Task':>6} {'N':>5} {'min ms':>8} {'mean ms':>8} "
f"{'max ms':>8} {'p95 ms':>8}")
print("-" * 52)
for task_id, times in sorted(rt_map.items()):
stats = sim.compute_response_time_stats(times)
print(f"{task_id:>6} {len(times):>5} "
f"{stats.min*1e3:>8.2f} {stats.mean*1e3:>8.2f} "
f"{stats.max*1e3:>8.2f} {stats.percentile_95*1e3:>8.2f}")
# Frequency intervals as text
print("\nFrequency intervals")
print(f"{'cluster':>8} {'start s':>10} {'stop s':>10} {'freq MHz':>9}")
print("-" * 44)
for iv in writer.track_frequency_changes():
print(f"{iv.cluster_id:>8} {iv.start:>10.6f} "
f"{iv.stop:>10.6f} {iv.frequency:>9.0f}")
# Energy per processor
print("\nPer-processor energy")
for proc_id, energy in sorted(metrics.get_energy_per_processor().items()):
print(f" Processor {proc_id}: {energy:.2f} mJ")
Next Steps¶
Trace Analysis and Metrics — full reference for metrics and time-series analysis
Parameter Sweeps and Policy Comparison — generate the data for multi-line plots from sweeps