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