Skip to content
Open
Show file tree
Hide file tree
Changes from 25 commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
205af52
hwbench: hide background monitoring messages behind a --verbose mode
ErwanAliasr1 Jul 9, 2026
a5f988a
stressng: emit YAML metrics and add a 0.21.03 parsing test
ErwanAliasr1 Jul 12, 2026
ddb28b7
hwbench: make benchmark output filenames unique per run
ErwanAliasr1 Jul 16, 2026
99c58a2
hwgraph: per-NUMA-domain CPU graphs + NUMA topology collection
ErwanAliasr1 Jul 8, 2026
b34e28b
hwgraph: add NUMA heatmaps (per-host distance, per-metric domain x time)
ErwanAliasr1 Jul 8, 2026
2b030e3
hwgraph: Add IPC reporting in scaling mode
ErwanAliasr1 Jul 8, 2026
dcbed35
hwgraph: add per-NUMA-domain delta heatmaps to scaling
ErwanAliasr1 Jul 8, 2026
39409f4
hwgraph: Update System header for more readbility
ErwanAliasr1 Jul 9, 2026
e1a3f4c
hwgraph: Move component legend a bit away from the Y-AXIS
ErwanAliasr1 Jul 9, 2026
75a5f73
hwgraph: hide "No samples found" messages behind --verbose
ErwanAliasr1 Jul 9, 2026
52224f1
hwgraph: do not print a power max for a metric without data
ErwanAliasr1 Jul 9, 2026
d272321
hwgraph: add a Y midline between ticks on smp_scaling graphs
ErwanAliasr1 Jul 9, 2026
20808b0
hwgraph: keep the smp_scaling legend table aligned across traces
ErwanAliasr1 Jul 9, 2026
8e64633
hwgraph: add per-core steady-state distribution graphs
ErwanAliasr1 Jul 9, 2026
5cf7627
hwgraph: add per-core distribution graphs to smp_scaling
ErwanAliasr1 Jul 9, 2026
62bd4d9
hwgraph: add per-NUMA-domain distribution graphs
ErwanAliasr1 Jul 9, 2026
4c1c2b9
hwgraph: show scaling vs ideal linear performance projection
ErwanAliasr1 Jul 10, 2026
632b051
hwgraph: write a per-host benchmarks summary text table
ErwanAliasr1 Jul 10, 2026
b6fda2d
hwgraph: annotate each scaling line's peak value in the line colour
ErwanAliasr1 Jul 10, 2026
28c9865
hwgraph: rename "SMP scaling" to "Performance scaling"
ErwanAliasr1 Jul 10, 2026
e283468
hwgraph: plot the per-worker performance distribution of stress-ng runs
ErwanAliasr1 Jul 12, 2026
02d6f93
hwgraph: align the violin distribution graphs with the linearity graph
ErwanAliasr1 Jul 12, 2026
7c27549
hwgraph: move linearity/distribution graphs into perf/ subdirectories
ErwanAliasr1 Jul 12, 2026
ef57bd9
hwgraph: keep the "data plotted from" box inside the saved crop
ErwanAliasr1 Jul 12, 2026
eb750b6
hwgraph: add a traces-comparison report and exec-summary scorecard
ErwanAliasr1 Jul 14, 2026
cb58ff6
hwgraph: cycle the performance-scaling line colours so a 6th+ trace i…
ErwanAliasr1 Jul 15, 2026
4d639cc
hwgraph: add a per-scaling-step traces comparison under scaling/summary/
ErwanAliasr1 Jul 15, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
567 changes: 561 additions & 6 deletions graph/graph.py

Large diffs are not rendered by default.

149 changes: 138 additions & 11 deletions graph/hwgraph.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,21 @@
try:
from graph.chassis import graph_chassis
from graph.common import fatal
from graph.graph import generic_graph, init_matplotlib, yerr_graph
from graph.graph import (
cpu_distribution_graph,
generic_graph,
init_matplotlib,
numa_aggregated_components,
numa_distance_heatmap,
numa_distribution_graph,
numa_performance_heatmap,
write_benchmarks_summary,
yerr_graph,
)
from graph.group import graph_group_env
from graph.scaling import smp_scaling_graph
from graph.scaling import performance_scaling_graph
from graph.trace import Event, Trace
from graph.versus import max_versus_graph
from graph.versus import max_versus_graph, render_versus_scorecard, write_scaling_comparison
from hwbench.bench.monitoring_structs import (
PowerCategories,
)
Expand Down Expand Up @@ -66,11 +76,18 @@ def _task_env_bench(trace_idx, bench_name, output_dir_str):
count += graph_monitoring_metrics(args, trace, bench_name, output_dir)
count += graph_fans(args, trace, bench_name, output_dir)
count += graph_cpu(args, trace, bench_name, output_dir)
count += graph_cpu_numa(args, trace, bench_name, output_dir)
count += graph_pdu(args, trace, bench_name, output_dir)
count += graph_thermal(args, trace, bench_name, output_dir)
return count


def _task_numa_distance(trace_idx, output_dir_str):
"""Generate the per-host NUMA distance heatmap (once per trace)."""
global _pool_args
return numa_distance_heatmap(_pool_args, pathlib.Path(output_dir_str), _pool_args.traces[trace_idx])


def _task_chassis(bench_name, output_dir_str):
"""Generate chassis graphs for a single bench."""
global _pool_args
Expand All @@ -84,9 +101,18 @@ def _task_group(bench_name, output_dir_str):


def _task_scaling(job, output_dir_str, traces_name):
"""Generate SMP scaling graphs for a job."""
"""Generate performance scaling graphs for a job."""
global _pool_args
return smp_scaling_graph(_pool_args, pathlib.Path(output_dir_str), job, traces_name)
return performance_scaling_graph(_pool_args, pathlib.Path(output_dir_str), job, traces_name)


def _task_scaling_comparison(output_dir_str):
"""Write the traces-comparison summary + exec-summary radar (reference = first trace)."""
global _pool_args
output_dir = pathlib.Path(output_dir_str)
count = write_scaling_comparison(_pool_args, output_dir)
count += render_versus_scorecard(_pool_args, output_dir)
return count


def _task_versus(job, output_dir_str, traces_name):
Expand Down Expand Up @@ -185,6 +211,8 @@ def valid_traces(args):
print(f"environment: rendering {len(benches)} jobs from {trace.get_filename()} ({trace.get_name()})")
for bench_name in sorted(benches):
tasks.append((_task_env_bench, trace_idx, bench_name, str(host_output_dir)))
# One NUMA distance heatmap per host, not tied to any benchmark.
tasks.append((_task_numa_distance, trace_idx, str(host_output_dir)))

return tasks

Expand All @@ -204,10 +232,10 @@ def _collect_plot_tasks(args, output_dir):
traces_name = [trace.get_name() for trace in args.traces]

if not args.no_scaling:
print("SMP scaling: disabled by user")
print("Performance scaling: disabled by user")
else:
# Let's generate the scaling graphs
print(f"SMP scaling: rendering {len(jobs)} jobs")
print(f"Performance scaling: rendering {len(jobs)} jobs")
for job in jobs:
tasks.append((_task_scaling, job, str(output_dir), traces_name))

Expand All @@ -219,6 +247,9 @@ def _collect_plot_tasks(args, output_dir):
print(f"Max versus: rendering {len(jobs)} jobs")
for job in jobs:
tasks.append((_task_versus, job, str(output_dir), traces_name))
# One text report comparing every trace to the first (reference),
# covering all benchmarks at once (max_versus/benchmarks_summary.txt).
tasks.append((_task_scaling_comparison, str(output_dir)))
else:
print("Max versus: skipped as at least 2 traces are necessary for this mode")

Expand Down Expand Up @@ -332,6 +363,14 @@ def render_traces(args: argparse.Namespace):
compare_traces(args)
generate_stats(args)

# Write the per-host benchmarks summary text table before rendering any graph,
# so the key to what each benchmark tests is available up front. args.no_env is
# True when environmental graphs are enabled (--no-env, store_false, disables).
if args.no_env:
host_output_dir = output_dir.joinpath("environment", "by_host")
for trace in args.traces:
write_benchmarks_summary(args, host_output_dir, trace)

# Collect all graph generation tasks
# (This also performs validation and creates output directories)
tasks = []
Expand Down Expand Up @@ -369,7 +408,10 @@ def generate_stats(args) -> None:
for metric in metrics:
# If a metric has no measure, let's ignore it
if len(metrics[metric].get_samples()) == 0:
print(f"{bench_name}: No samples found in {metric_name}.{metric}, ignoring metric.")
if args.verbose:
print(
f"{bench_name}: No samples found in {metric_name}.{metric}, ignoring metric."
)
continue
else:
max_values = metrics[metric].get_max()
Expand All @@ -384,7 +426,8 @@ def generate_stats(args) -> None:
for metric in [MonitoringContextKeys.PowerConsumption]:
print(f"{str(metric):}")
for metric_name in PowerConsumptionContextKeys:
if max_power[metric_name]:
# Only report a max when data was actually found (a bench was recorded).
if max_power[metric_name][0]:
print(
f" {metric_name} max : {max_power[metric_name][1]:.2f} {bench.get_metric_unit(metric)} in {max_power[metric_name][0]}"
)
Expand Down Expand Up @@ -422,7 +465,8 @@ def graph_monitoring_metrics(args, trace: Trace, bench_name: str, output_dir) ->
for metric in metrics:
# If a metric has no measure, let's ignore it
if len(metrics[metric].get_samples()) == 0:
print(f"{bench_name}: No samples found in {metric_name}.{metric}, ignoring metric.")
if args.verbose:
print(f"{bench_name}: No samples found in {metric_name}.{metric}, ignoring metric.")
else:
try:
rendered_graphs += yerr_graph(
Expand Down Expand Up @@ -495,7 +539,90 @@ def graph_cpu(args, trace: Trace, bench_name: str, output_dir) -> int:
dir_suffix=dir_suffix,
title_note=title_note,
)
# Per-core metrics also get a steady-state distribution (violin + box).
if filter == "Core":
rendered_graphs += cpu_distribution_graph(
args,
output_dir,
bench,
metric,
graph_name,
dir_suffix=dir_suffix,
names=names,
title_note=title_note,
)

return rendered_graphs


def graph_cpu_numa(args, trace: Trace, bench_name: str, output_dir) -> int:
"""Render per-core CPU metrics aggregated by NUMA domain (one line per domain).

Requires the NUMA topology in the trace; older traces without it are skipped.
"""
numa_nodes = trace.get_numa_nodes()
if not numa_nodes:
print(f"{bench_name}: no NUMA metric present in trace file, skipping.")
return 0
rendered_graphs = 0
bench = trace.bench(bench_name)
numa_graphs = {
"Core frequency per NUMA domain": MonitoringContextKeys.Freq,
"Core IPC per NUMA domain": MonitoringContextKeys.IPC,
"CPU Core power consumption per NUMA domain": MonitoringContextKeys.PowerConsumption,
}
# Metrics that also get a per-domain x time heatmap next to their line graph.
heatmap_metrics = {MonitoringContextKeys.Freq, MonitoringContextKeys.IPC}
# Like the per-core graphs: one rendering averaging every core of each domain
# (all_numa), and one restricted to the cores pinned during this job, grouped
# by the domains those pinned cores belong to (pinned_numa).
pinned_core_names = bench.pinned_core_names()
# Each rendering is a dir suffix, the pinned-core filter, and a title note.
renderings = [("all_numa", None, None)] # type: list
if pinned_core_names:
pinned_note = f"View limited to the pinned logical cores {bench.pinned_cpu_range()}"
renderings.append(("pinned_numa", pinned_core_names, pinned_note))

for graph_name, metric in numa_graphs.items():
for dir_suffix, pinned_cores, title_note in renderings:
components = numa_aggregated_components(bench, metric, numa_nodes, pinned_cores)
if not components:
continue
rendered_graphs += generic_graph(
args,
output_dir,
bench,
metric,
graph_name,
dir_suffix=dir_suffix,
components=components,
title_note=title_note,
)
# Companion heatmap: NUMA domain x time, color = per-domain value.
if metric in heatmap_metrics:
rendered_graphs += numa_performance_heatmap(
args,
output_dir,
bench,
metric,
graph_name,
numa_nodes,
dir_suffix=dir_suffix,
pinned_cores=pinned_cores,
title_note=title_note,
)
# Companion violin: one steady-state distribution per NUMA domain.
rendered_graphs += numa_distribution_graph(
args,
output_dir,
bench,
metric,
graph_name,
numa_nodes,
dir_suffix=dir_suffix,
pinned_cores=pinned_cores,
title_note=title_note,
)
return rendered_graphs


Expand Down Expand Up @@ -559,7 +686,7 @@ def main():
required=True,
)
parser_graph.add_argument("--no-env", help="Disable environmental graphs", action="store_false")
parser_graph.add_argument("--no-scaling", help="Disable 'SMP scaling' graphs", action="store_false")
parser_graph.add_argument("--no-scaling", help="Disable 'Performance scaling' graphs", action="store_false")
parser_graph.add_argument("--no-versus", help="Disable 'max versus' graphs", action="store_false")
parser_graph.add_argument("--no-stats", help="Disable stats", action="store_false")
parser_graph.add_argument("--title", help="Title of the graph")
Expand Down
Loading