diff --git a/graph/graph.py b/graph/graph.py index 3c8c0a65..96052d77 100644 --- a/graph/graph.py +++ b/graph/graph.py @@ -6,12 +6,14 @@ import matplotlib.pyplot as plt import numpy as np from matplotlib.collections import LineCollection +from matplotlib.lines import Line2D from matplotlib.pylab import Axes from matplotlib.ticker import AutoMinorLocator, FuncFormatter, MultipleLocator from graph.common import fatal from graph.trace import Bench from hwbench.bench.monitoring_structs import MonitoringContextKeys, MonitorMetric +from hwbench.utils.helpers import cpu_list_to_range MEAN = "mean" ERROR = "error" @@ -172,6 +174,9 @@ def prepare_grid(self): def set_title(self, title, show_source_file=None, title_note=None): """Set the graph title""" + # Tracked so render() can hand it to savefig's bbox_extra_artists: the box + # sits below the axes and can otherwise be cropped out by bbox_inches="tight". + self.source_text = None # When a note is present, push the main title up to leave room for the # note, which is rendered just above the axes in bold dark red. self.ax.set_title(title, pad=28 if title_note else None) @@ -190,7 +195,7 @@ def set_title(self, title, show_source_file=None, title_note=None): if show_source_file: # place a text box in upper left in axes coords props = dict(boxstyle="round", facecolor="white", alpha=0.5) - self.ax.text( + self.source_text = self.ax.text( 0, -0.1, f"data plotted from {show_source_file.get_filename()}", @@ -243,11 +248,25 @@ def trace_events(self): color=event_color, ) - def render(self): - """Render the graph to a file.""" + def render(self, extra_legend=None): + """Render the graph to a file. + + extra_legend: one or more additional, manually placed legends that + must be taken into account when computing the tight bounding box. + """ # Retrieve the rendering file format file_format = self.args.format - legends = [] + if extra_legend is None: + legends = [] + elif isinstance(extra_legend, (list, tuple)): + legends = list(extra_legend) + else: + legends = [extra_legend] + + # Keep the "data plotted from" box in the tight bounding box: it sits below + # the axes and can otherwise be cropped out by bbox_inches="tight". + if self.source_text is not None: + legends.append(self.source_text) # Trace the events passed on the command line self.trace_events() @@ -261,7 +280,7 @@ def render(self): # Upper left handles, labels = self.ax.get_legend_handles_labels() if handles: - legends.append(self.ax.legend(bbox_to_anchor=(-0.05, 1), title="component [min; mean; stddev; max]\n")) + legends.append(self.ax.legend(bbox_to_anchor=(-0.1, 1), title="component [min; mean; stddev; max]\n")) if self.ax2: # Anchor the legend by its left edge, just right of the 2nd y-axis # (past its tick labels and title), so it grows outward instead of @@ -309,6 +328,540 @@ def statistics_in_label(label: str, serie: np.ndarray, max_title_length=0, max_v return f"{label:{max_title_length}} [{fp(min(serie), max_value_length)}; {fp(np.mean(serie), max_value_length)}; {fp(np.std(serie), max_value_length)}; {fp(max(serie), max_value_length)}]" +def numa_core_blocks(cpus, width: int = 3) -> str: + """Render a cpu list as aligned, individually bracketed range blocks. + + Reuses cpu_list_to_range() and only reformats its output: each range block + gets its own "[]", the numbers are padded to `width` digits with the dash + centered, so blocks line up in a monospace legend. + e.g. [0..7, 64..71] -> "[0 - 7] [64 - 71]". + """ + blocks = [] + for block in cpu_list_to_range(list(cpus)).split(", "): + low, _, high = block.partition("-") + if high: + # Right-justify both bounds so numbers align and the dash stays centered. + blocks.append(f"[{low:>{width}}-{high:>{width}}]") + else: + # A single core (no range): keep the same block width, right-aligned. + blocks.append(f"[{block:>{2 * width + 1}}]") + return ", ".join(blocks) + + +def numa_aggregated_components( + bench: Bench, component_type: MonitoringContextKeys, numa_nodes, pinned_cores=None +) -> list[MonitorMetric]: + """Aggregate per-core metrics into one synthetic metric per NUMA domain. + + For each NUMA domain, average (per sample) the metric of the cores that + belong to it. When pinned_cores (a set of "Core_N" names) is given, only the + pinned cores of each domain are averaged and domains with no pinned core are + dropped. Domains with no monitored core are skipped. The result is a list of + MonitorMetric objects (one per domain) ready to feed generic_graph. + """ + samples_count = bench.get_samples_count() + components = [] + for node in sorted(numa_nodes): + core_names = {f"Core_{cpu}" for cpu in numa_nodes[node]} + if pinned_cores is not None: + core_names &= pinned_cores + if not core_names: + continue + cores = bench.get_all_metrics(component_type, names=core_names) + if not cores: + continue + means = [] + for sample in range(samples_count): + values = [c.get_mean()[sample] for c in cores if sample < len(c.get_mean())] + means.append(float(np.mean(values)) if values else 0.0) + metric = MonitorMetric(f"NUMA {node}", cores[0].get_unit()) + metric.mean = means + metric.full_name = f"NUMA {node}" + components.append(metric) + return components + + +def numa_cores_legend(ax, node_cores): + """Add a left-side box listing each NUMA domain's cores in condensed form. + + node_cores is an ordered list of (domain, cpus) pairs (top to bottom). The + box is anchored well to the left of the Y axis so it does not collide with + it, even with 3-digit core numbers. + """ + node_width = max((len(str(node)) for node, _ in node_cores), default=1) + labels = [f"NUMA {node:>{node_width}}: {numa_core_blocks(cpus)}" for node, cpus in node_cores] + handles = [Line2D([], [], linestyle="none") for _ in labels] + return ax.legend( + handles, + labels, + loc="upper right", + bbox_to_anchor=(-0.10, 1), + title="NUMA domain [cores]", + handlelength=0, + handletextpad=0, + ) + + +def _render_numa_heatmap(graph, nodes, matrix, diagonal_values=None, extra_legend=None) -> None: + """Draw a NUMA domain x domain distance matrix onto graph. + + Cells are colored/annotated by the inter-domain distance. When + diagonal_values is given, the diagonal (whose self-distance is constant and + uninformative) instead shows the per-domain value in bold. + """ + ax = graph.get_ax() + image = ax.imshow(matrix, cmap="viridis") + graph.fig.colorbar(image, ax=ax, fraction=0.046, pad=0.04, label="NUMA distance") + labels = [f"NUMA {node}" for node in nodes] + ax.set_xticks(range(len(nodes))) + ax.set_yticks(range(len(nodes))) + ax.set_xticklabels(labels) + ax.set_yticklabels(labels) + # Text color threshold so annotations stay readable over the colormap. + threshold = (matrix.max() + matrix.min()) / 2 + for i in range(len(nodes)): + for j in range(len(nodes)): + if i == j and diagonal_values is not None: + text, weight = f"{diagonal_values[i]:.0f}", "bold" + else: + text, weight = f"{matrix[i, j]:.0f}", "normal" + ax.text( + j, + i, + text, + ha="center", + va="center", + fontsize=8, + fontweight=weight, + color="white" if matrix[i, j] < threshold else "black", + ) + graph.needs_legend = False + graph.render(extra_legend=extra_legend) + + +def numa_distance_heatmap(args, output_dir, trace) -> int: + """Render the host's NUMA distance matrix as a heatmap. + + This is a per-host topology figure (one per trace), independent of any + benchmark or performance metric: cell color/value is the distance between + two NUMA domains, so which domains are close/far is visible at a glance. + Needs the distance matrix with at least two domains; otherwise nothing is + rendered. + """ + distances = trace.get_numa_distances() + if not distances or len(distances) < 2: + return 0 + nodes = sorted(distances) + try: + matrix = np.array([[distances[src][dst] for dst in nodes] for src in nodes], dtype=float) + except (KeyError, IndexError): + return 0 + + cpu = trace.get_cpu() + dmi = trace.get_dmi() + title = f"NUMA distances of {trace.get_name()}\n{args.title}\n\n" + title += f"System: {dmi['serial']} {dmi['product']}" + title += f"\nProcessor: {cpu.get('sockets', 1)}x {cpu['model']} - {cpu['numa_domains']} NUMA domains" + graph = Graph( + args, + title, + "NUMA domain", + "NUMA domain", + output_dir.joinpath(trace.get_name()), + "NUMA distances", + square=True, + show_source_file=trace, + ) + numa_nodes = trace.get_numa_nodes() + legend = numa_cores_legend(graph.get_ax(), [(node, numa_nodes.get(node, [])) for node in nodes]) + _render_numa_heatmap(graph, nodes, matrix, extra_legend=legend) + return 1 + + +def _int_ranges(values, width: int = 0) -> str: + """Condense a list of ints into ranges, keeping singletons (e.g. '0, 2-4'). + + Unlike cpu_list_to_range(), a lone value is emitted as-is rather than dropped. + Numbers are right-justified to `width` digits (0 = no padding) so ranges stay + aligned. + """ + ordered = sorted(values) + if not ordered: + return "" + parts = [] + start = prev = ordered[0] + for value in ordered[1:]: + if value == prev + 1: + prev = value + else: + parts.append(f"{start:>{width}}" if start == prev else f"{start:>{width}}-{prev:>{width}}") + start = prev = value + parts.append(f"{start:>{width}}" if start == prev else f"{start:>{width}}-{prev:>{width}}") + return ", ".join(parts) + + +def write_benchmarks_summary(args, output_dir, trace) -> int: + """Write a text table describing every benchmark run of the trace. + + One row per benchmark (e.g. avx_0, avx_1, ...): the job it belongs to, the + engine/module and variant (engine_module_parameter) it exercises, the worker + count, core pinning and per-run duration. It is a per-host, plain-text + companion to the environmental graphs -- a quick key to what each benchmark + actually tested, in benchmark (job_number) order. Written to + benchmarks_summary.txt; returns 1 when written. + """ + benches = trace.bench_list() + if not benches: + return 0 + + # One row per benchmark, ordered by job_number (avx_0, avx_1, ... then cpu_*). + ordered = sorted(benches, key=lambda name: trace.bench(name).get("job_number")) + + # core -> NUMA node lookup, to report which domains a benchmark's pinned cores span. + numa_nodes = trace.get_numa_nodes() + core_to_node = {core: node for node, cores in numa_nodes.items() for core in cores} + + headers = [ + "Benchmark", + "Job", + "Engine / module", + "Variant", + "Workers", + "Pinned cores", + "NUMA nodes", + "Duration (s)", + ] + right_aligned = {"Workers", "NUMA nodes", "Duration (s)"} + rows = [] # type: list + for bench_name in ordered: + bench = trace.bench(bench_name) + # NUMA domains the pinned cores belong to (condensed and bracketed, node + # numbers padded to 2 digits so ranges align, e.g. "[ 0- 3]"). + numa_span = "no" + if bench.cpu_pin(): + nodes = sorted({core_to_node[core] for core in bench.cpu_pin() if core in core_to_node}) + numa_span = f"[{_int_ranges(nodes, width=2)}]" if nodes else "-" + rows.append( + { + "Benchmark": bench_name, + "Job": bench.job_name(), + "Engine / module": f"{bench.engine()} / {bench.engine_module()}", + "Variant": bench.engine_module_parameter(), + "Workers": str(bench.workers()), + # Condensed list of pinned cores with digits padded to 3 columns so + # the ranges stay aligned even with 3-digit core numbers, or "no". + "Pinned cores": numa_core_blocks(bench.cpu_pin()) if bench.cpu_pin() else "no", + "NUMA nodes": numa_span, + "Duration (s)": str(int(round(bench.duration()))), + } + ) + + widths = {header: len(header) for header in headers} + for row in rows: + for header in headers: + widths[header] = max(widths[header], len(row[header])) + + def _cell(header, value) -> str: + return str(value).rjust(widths[header]) if header in right_aligned else str(value).ljust(widths[header]) + + def _line(cells) -> str: + return " ".join(_cell(header, cells[header]) for header in headers) + + # Header: the same host description as the graphs, with System / Bios / Kernel + # split onto their own (label-aligned) lines, plus a NUMA-nodes summary. + dmi = trace.get_dmi() + cpu = trace.get_cpu() + kernel = trace.get_kernel() + info = [ + ("System", f"{dmi['serial']} {dmi['product']}"), + ("Bios", f"v{dmi['bios']['version']}"), + ("Kernel", kernel["release"]), + ( + "Processor", + f"{cpu.get('sockets', 1)}x {cpu['model']} - " + f"{cpu['physical_cores']} physical cores and {cpu['numa_domains']} NUMA domains", + ), + ] + label_width = max(len(label) for label, _ in info) + lines = [f"Benchmarks summary - {trace.get_name()}", args.title, ""] + lines += [f"{label:<{label_width}} : {value}" for label, value in info] + + if numa_nodes: + node_width = max(len(str(node)) for node in numa_nodes) + lines.append("") + lines.append(f"NUMA nodes ({len(numa_nodes)}):") + lines += [f" NUMA {node:>{node_width}} : {numa_core_blocks(numa_nodes[node])}" for node in sorted(numa_nodes)] + + lines += ["", _line({header: header for header in headers}), " ".join("-" * widths[header] for header in headers)] + lines += [_line(row) for row in rows] + text = "\n".join(line.rstrip() for line in lines) + "\n" + + trace_dir = output_dir.joinpath(trace.get_name()) + trace_dir.mkdir(parents=True, exist_ok=True) + summary_file = trace_dir.joinpath("benchmarks_summary.txt") + summary_file.write_text(text) + print(f"benchmarks: {trace.get_name()} - wrote {len(rows)} entries to {summary_file}") + return 1 + + +def numa_distribution_graph( + args, + output_dir, + bench: Bench, + component_type: MonitoringContextKeys, + item_title: str, + numa_nodes, + dir_suffix=None, + pinned_cores=None, + title_note=None, +) -> int: + """Render the per-NUMA-domain distribution of a metric at steady state (one violin + box per domain). + + Each domain gets its own violin built from its cores' steady-state (mean + over the run) values, so core-to-core uniformity can be compared across + domains at a glance -- same idea as cpu_distribution_graph, split by NUMA + domain instead of blended into a single body. When pinned_cores is given, + only the pinned cores of each domain feed its violin and domains with no + pinned core are skipped. The Y axis autoscales to the data. + """ + domains = [] + for node in sorted(numa_nodes): + core_names = {f"Core_{cpu}" for cpu in numa_nodes[node]} + if pinned_cores is not None: + core_names &= pinned_cores + if not core_names: + continue + cores = bench.get_all_metrics(component_type, names=core_names) + if not cores: + continue + values = [float(np.mean(core.get_mean())) for core in cores] + domains.append((node, values)) + if not domains: + return 0 + unit = bench.get_metric_unit(component_type) + + trace = bench.get_trace() + title = f'{item_title} per-NUMA-domain distribution during "{bench.get_bench_name()}" benchmark job\n{args.title}\n\n Stressor: ' + title += f"{bench.workers()} x {bench.get_title_engine_name()} for {bench.duration()} seconds" + title += f"\n{bench.get_system_title()}" + graph_dir = output_dir.joinpath(f"{trace.get_name()}/{bench.get_bench_name()}/{component_type!s}") + if dir_suffix: + graph_dir = graph_dir.joinpath(dir_suffix) + graph = Graph( + args, + title, + "", + unit, + graph_dir, + f"{item_title} - distribution", + show_source_file=trace, + title_note=title_note, + ) + ax = graph.get_ax() + positions = list(range(1, len(domains) + 1)) + domain_values = [values for _, values in domains] + parts = ax.violinplot(domain_values, positions=positions, widths=0.7, showextrema=False) + for body in parts["bodies"]: + body.set_facecolor("tab:blue") + body.set_alpha(0.25) + ax.boxplot( + domain_values, + positions=positions, + widths=0.15, + showmeans=True, + meanline=True, + medianprops=dict(color="tab:red"), + meanprops=dict(color="tab:green", linestyle="--"), + flierprops=dict(marker=".", markersize=3, alpha=0.4), + ) + ax.set_xticks(positions) + ax.set_xticklabels([f"NUMA {node}" for node, _ in domains]) + # Same two-tier Y grid as the linearity-deviation graph (solid major lines + # plus a fainter dashed midline between the major Y ticks), but no X grid: + # vertical lines would cut through the violins and hide their shape. + ax.yaxis.set_minor_locator(AutoMinorLocator(2)) + ax.grid(which="major", axis="y", linewidth=0.7, linestyle="solid", color="0.6") + ax.grid(which="minor", axis="y", linewidth=0.6, linestyle="dashed", color="0.75") + + # Left-side box listing each domain's cores in condensed form, like the + # heatmap's legend -- useful to see exactly which cores fed each violin, + # especially on the pinned view where a domain may keep only a few. + pinned_cpus = None + if pinned_cores is not None: + pinned_cpus = {int(name.split("_")[1]) for name in pinned_cores} + node_cores = [] + for node, _ in domains: + cpus = numa_nodes[node] + if pinned_cpus is not None: + cpus = [cpu for cpu in cpus if cpu in pinned_cpus] + node_cores.append((node, cpus)) + cores_legend = numa_cores_legend(ax, node_cores) + stats_legend = ax.legend( + handles=[ + Line2D([], [], color="tab:red", label="median"), + Line2D([], [], color="tab:green", linestyle="--", label="mean"), + ], + loc="upper right", + fontsize=8, + ) + ax.add_artist(cores_legend) + graph.needs_legend = False + graph.render(extra_legend=[cores_legend, stats_legend]) + return 1 + + +def numa_performance_heatmap( + args, + output_dir, + bench: Bench, + component_type: MonitoringContextKeys, + item_title: str, + numa_nodes, + dir_suffix=None, + pinned_cores=None, + title_note=None, +) -> int: + """Render a NUMA-domain x time heatmap of a per-core metric. + + Y axis is the NUMA domains, X axis is time (as in the line graphs of the + same directory) and the color is the domain's average metric value at each + monitoring step. It is the same data as the per-domain line graph, shown as + a heatmap; it lives next to it (all_numa/pinned_numa). + """ + components = numa_aggregated_components(bench, component_type, numa_nodes, pinned_cores) + if not components: + return 0 + unit = bench.get_metric_unit(component_type) + interval = bench.get_time_interval() + matrix = np.array([component.get_mean() for component in components], dtype=float) + domains = len(components) + samples = matrix.shape[1] + + trace = bench.get_trace() + title = f'{item_title} during "{bench.get_bench_name()}" benchmark job\n{args.title}\n\n Stressor: ' + title += f"{bench.workers()} x {bench.get_title_engine_name()} for {bench.duration()} seconds" + title += f"\n{bench.get_system_title()}" + graph_dir = output_dir.joinpath(f"{trace.get_name()}/{bench.get_bench_name()}/{component_type!s}") + if dir_suffix: + graph_dir = graph_dir.joinpath(dir_suffix) + graph = Graph( + args, + title, + "Time [seconds]", + "NUMA domain", + graph_dir, + f"{item_title} - heatmap", + show_source_file=trace, + title_note=title_note, + ) + ax = graph.get_ax() + image = ax.imshow( + matrix, + aspect="auto", + cmap="viridis", + interpolation="nearest", + extent=(0, samples * interval, domains - 0.5, -0.5), + ) + graph.fig.colorbar(image, ax=ax, fraction=0.046, pad=0.04, label=unit) + ax.set_yticks(range(domains)) + ax.set_yticklabels([component.get_full_name() for component in components]) + + # Separate box on the left listing each domain's cores in condensed form, + # like the component legend of the line graphs. + pinned_cpus = None + if pinned_cores is not None: + pinned_cpus = {int(name.split("_")[1]) for name in pinned_cores} + node_cores = [] + for component in components: + node = int(component.get_full_name().split()[1]) + cpus = numa_nodes[node] + if pinned_cpus is not None: + cpus = [cpu for cpu in cpus if cpu in pinned_cpus] + node_cores.append((node, cpus)) + legend = numa_cores_legend(ax, node_cores) + graph.needs_legend = False + graph.render(extra_legend=legend) + return 1 + + +def cpu_distribution_graph( + args, + output_dir, + bench: Bench, + component_type: MonitoringContextKeys, + item_title: str, + dir_suffix=None, + names=None, + title_note=None, +) -> int: + """Render the per-core distribution of a metric at steady state (violin + box). + + Each core's steady-state value (mean over the run) becomes one point; the + violin shows their density and the box shows median/mean/quartiles/outliers, + so the core-to-core uniformity is visible at a glance. names restricts the + cores (pinned view). The Y axis autoscales to the data (a distribution is + unreadable squished against a zero baseline). + """ + components = bench.get_all_metrics(component_type, "Core", names) + if not components: + return 0 + unit = bench.get_metric_unit(component_type) + values = [float(np.mean(component.get_mean())) for component in components] + + trace = bench.get_trace() + title = f'{item_title} per-core distribution during "{bench.get_bench_name()}" benchmark job\n{args.title}\n\n Stressor: ' + title += f"{bench.workers()} x {bench.get_title_engine_name()} for {bench.duration()} seconds" + title += f"\n{bench.get_system_title()}" + graph_dir = output_dir.joinpath(f"{trace.get_name()}/{bench.get_bench_name()}/{component_type!s}") + if dir_suffix: + graph_dir = graph_dir.joinpath(dir_suffix) + graph = Graph( + args, + title, + "", + unit, + graph_dir, + f"{item_title} - distribution", + square=True, + show_source_file=trace, + title_note=title_note, + ) + ax = graph.get_ax() + parts = ax.violinplot([values], positions=[1], widths=0.7, showextrema=False) + for body in parts["bodies"]: + body.set_facecolor("tab:blue") + body.set_alpha(0.25) + ax.boxplot( + [values], + positions=[1], + widths=0.15, + showmeans=True, + meanline=True, + medianprops=dict(color="tab:red"), + meanprops=dict(color="tab:green", linestyle="--"), + flierprops=dict(marker=".", markersize=3, alpha=0.4), + ) + ax.set_xticks([1]) + ax.set_xticklabels([f"{len(values)} cores"]) + # Same two-tier Y grid as the linearity-deviation graph (solid major lines + # plus a fainter dashed midline between the major Y ticks), but no X grid: + # vertical lines would cut through the violins and hide their shape. + ax.yaxis.set_minor_locator(AutoMinorLocator(2)) + ax.grid(which="major", axis="y", linewidth=0.7, linestyle="solid", color="0.6") + ax.grid(which="minor", axis="y", linewidth=0.6, linestyle="dashed", color="0.75") + legend = ax.legend( + handles=[ + Line2D([], [], color="tab:red", label="median"), + Line2D([], [], color="tab:green", linestyle="--", label="mean"), + ], + loc="upper right", + fontsize=8, + ) + graph.needs_legend = False + graph.render(extra_legend=legend) + return 1 + + def generic_graph( args, output_dir, @@ -320,11 +873,13 @@ def generic_graph( names=None, dir_suffix=None, title_note=None, + components=None, ) -> int: outfile = f"{item_title}" trace = bench.get_trace() - components = bench.get_all_metrics(component_type, filter, names) + if components is None: + components = bench.get_all_metrics(component_type, filter, names) if not len(components): title = f"{item_title}: no {component_type!s} metric found" if filter: diff --git a/graph/hwgraph.py b/graph/hwgraph.py index 345bd77b..780cd3f3 100755 --- a/graph/hwgraph.py +++ b/graph/hwgraph.py @@ -19,11 +19,26 @@ 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, + write_scaling_step_comparisons, + ) from hwbench.bench.monitoring_structs import ( PowerCategories, ) @@ -66,11 +81,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 @@ -84,9 +106,20 @@ 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 summaries + exec-summary scorecard (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) + # Same comparison, but one file per scaling step under scaling/. + count += write_scaling_step_comparisons(_pool_args, output_dir) + return count def _task_versus(job, output_dir_str, traces_name): @@ -185,6 +218,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 @@ -204,10 +239,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)) @@ -219,6 +254,10 @@ 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)) + # Text reports comparing every trace to the first (reference): one at + # full load covering all benchmarks (max_versus/benchmarks_summary.txt) + # plus one per scaling step under scaling/. + tasks.append((_task_scaling_comparison, str(output_dir))) else: print("Max versus: skipped as at least 2 traces are necessary for this mode") @@ -332,6 +371,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 = [] @@ -369,7 +416,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() @@ -384,7 +434,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]}" ) @@ -422,7 +473,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( @@ -495,7 +547,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 @@ -559,7 +694,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") diff --git a/graph/scaling.py b/graph/scaling.py index f8c646b9..7ba5dedf 100644 --- a/graph/scaling.py +++ b/graph/scaling.py @@ -1,23 +1,922 @@ +from __future__ import annotations + from itertools import cycle from statistics import stdev from typing import Any # noqa: F401 +import matplotlib +import matplotlib.pyplot as plt import numpy as np +from matplotlib.colors import LinearSegmentedColormap, Normalize +from matplotlib.lines import Line2D +from matplotlib.offsetbox import AnchoredOffsetbox, HPacker, TextArea +from matplotlib.patches import Patch +from matplotlib.ticker import AutoMinorLocator, MultipleLocator + +from graph.graph import ( + GRAPH_TYPES, + Graph, + get_max_value_string_length, + numa_aggregated_components, + numa_core_blocks, + statistics_in_label, +) +from hwbench.bench.monitoring_structs import MonitoringContextKeys + + +def _bench_has_ipc(bench) -> bool: + try: + return bool(bench.get_monitoring_metric(MonitoringContextKeys.IPC)) + except KeyError: + return False + + +def render_numa_delta_heatmaps(args, temp_outdir, job: str, emp: str) -> int: + """Render per-NUMA-domain delta heatmaps comparing traces for one emp. + + The first trace is the reference; one heatmap is produced for each other + trace (reference vs that trace), the compared trace name being part of the + filename. Y = NUMA domains, X = scaling step (worker count), color = signed + delta value(reference) - value(other) of the per-domain metric (white ~ no + difference, red = reference higher, green = reference lower). Like the other + scaling graphs, each metric is rendered for all_cores and, when the sweep + pins cores, pinned_cores. Needs at least two traces exposing the NUMA + topology. + """ + if len(args.traces) < 2: + return 0 + reference = args.traces[0] + numa_nodes = reference.get_numa_nodes() + if not numa_nodes: + return 0 + reference_benches = reference.get_benches_by_job_per_emp(job) + if emp not in reference_benches: + return 0 + benches_ref = reference_benches[emp]["bench"] + if not benches_ref: + return 0 + + engine = benches_ref[0].get_title_engine_name().replace(" ", "_") + # all_cores always; pinned_cores when at least one run of the sweep pins cores. + variants = [("all_cores", False, None)] # type: list + if any(bench.cpu_pin() for bench in benches_ref): + variants.append(("pinned_cores", True, "View limited to the pinned cores of each scaling step")) + + rendered = 0 + # Compare the reference against every other trace, one heatmap set each. + for other in args.traces[1:]: + other_benches = other.get_benches_by_job_per_emp(job) + if emp not in other_benches or not other_benches[emp]["bench"]: + continue + benches_other = other_benches[emp]["bench"] + + metrics = [("cpu_clock", MonitoringContextKeys.Freq, "MHz")] + if _bench_has_ipc(benches_ref[0]) and _bench_has_ipc(benches_other[0]): + metrics.append(("cpu_ipc", MonitoringContextKeys.IPC, "IPC")) + + pair = f"{reference.get_name()}_vs_{other.get_name()}".replace(" ", "_").replace("/", "_") + for dirname, context, unit in metrics: + for dir_suffix, use_pinned, note in variants: + # Per trace: worker count -> {domain: mean metric value over the run} + per_trace = [] + for benches in (benches_ref, benches_other): + values = {} # type: dict[int, dict[int, float]] + for bench in benches: + pinned = bench.pinned_core_names() if use_pinned else None + domain_values = { + int(component.get_full_name().split()[1]): float(np.mean(component.get_mean())) + for component in numa_aggregated_components(bench, context, numa_nodes, pinned) + } + if domain_values: + values[bench.workers()] = domain_values + per_trace.append(values) + values_ref, values_other = per_trace + + workers = sorted(set(values_ref) & set(values_other)) + domains = sorted({d for w in workers for d in set(values_ref[w]) & set(values_other[w])}) + if not workers or not domains: + continue + + matrix = np.full((len(domains), len(workers)), np.nan) + for row, domain in enumerate(domains): + for col, worker in enumerate(workers): + ref = values_ref[worker].get(domain) + oth = values_other[worker].get(domain) + if ref is not None and oth is not None: + # Signed delta: reference minus other. + matrix[row, col] = ref - oth + + title = f'{args.title}\n\nPerformance scaling NUMA {dirname} delta via "{job}" benchmark job\n' + title += f"{unit} performance delta = ({reference.get_name()} (reference) - {other.get_name()})" + # Colour legend rendered as a caption below the graph (see below), + # with the "red" and "green" words drawn in their respective colour. + # No segment starts/ends with a space (TextArea drops edge spaces, + # which made the coloured words collide); word gaps come from the + # HPacker sep below instead. + caption_tail = f"{other.get_name()} is higher than reference, white: no difference" + if use_pinned: + caption_tail += ", black: unused NUMA node during benchmark" + caption_segments = [ + ("red:", "red"), + (f"{other.get_name()} is lower than reference, ", "black"), + (" green:", "green"), + (caption_tail, "black"), + ] + graph = Graph( + args, + title, + "Workers (scaling step)", + "NUMA domain", + temp_outdir.joinpath(dirname, dir_suffix), + f"scaling_{dirname}_numa_delta_{pair}_{engine}", + title_note=note, + ) + ax = graph.get_ax() + # Diverging map centered on 0: positive delta -> red, negative -> green. + finite = matrix[np.isfinite(matrix)] + bound = float(np.abs(finite).max()) if finite.size else 1.0 + bound = bound or 1.0 + cmap = LinearSegmentedColormap.from_list("numa_delta", ["green", "white", "red"]) + # Missing data: black on the pinned view (domain not pinned at that + # step), light grey otherwise, so it is not mistaken for a zero delta. + cmap.set_bad("black" if use_pinned else "0.85") + image = ax.imshow( + np.ma.masked_invalid(matrix), + aspect="auto", + cmap=cmap, + norm=Normalize(-bound, bound), + interpolation="nearest", + ) + graph.fig.colorbar( + image, + ax=ax, + fraction=0.046, + pad=0.04, + label=f"{reference.get_name()} - {other.get_name()} ({unit})", + ) + ax.set_xticks(range(len(workers))) + ax.set_xticklabels(workers) + ax.set_yticks(range(len(domains))) + ax.set_yticklabels([f"NUMA {domain}" for domain in domains]) + + # Left box listing each domain's cores (condensed). For the pinned + # variant, the union of cores pinned across the sweep per domain. + if use_pinned: + pinned_union = set() # type: set + for bench in benches_ref: + pinned_union |= {int(name.split("_")[1]) for name in (bench.pinned_core_names() or set())} + cores_by_domain = {d: [c for c in numa_nodes[d] if c in pinned_union] for d in domains} + else: + cores_by_domain = {d: numa_nodes[d] for d in domains} + node_width = max((len(str(domain)) for domain in domains), default=1) + legend_labels = [ + f"NUMA {domain:>{node_width}}; {numa_core_blocks(cores_by_domain[domain])}" for domain in domains + ] + handles = [Line2D([], [], linestyle="none") for _ in legend_labels] + legend = ax.legend( + handles, + legend_labels, + loc="upper right", + # Enough room from the Y-axis so 3-digit core numbers don't collide with it. + bbox_to_anchor=(-0.1, 1), + title="NUMA domain [cores]", + handlelength=0, + handletextpad=0, + ) + # Colour legend caption below the graph: one TextArea per segment + # so "red"/"green" can be drawn in their own colour. A proportional + # font is used because the cairo backend mismeasures monospace text + # width, which would make the HPacker segments overlap. + caption = HPacker( + children=[ + TextArea(text, textprops=dict(color=color, fontfamily="sans-serif")) + for text, color in caption_segments + ], + align="baseline", + pad=0, + # A gap the width of a space between segments (edge spaces are dropped). + sep=3, + ) + ax.add_artist( + AnchoredOffsetbox( + loc="upper center", + child=caption, + pad=0, + frameon=False, + bbox_to_anchor=(0.5, -0.16), + bbox_transform=ax.transAxes, + ) + ) + graph.needs_legend = False + graph.render(extra_legend=legend) + rendered += 1 + return rendered + + +def render_scaling_distributions(args, temp_outdir, job: str, emp: str, has_ipc: bool) -> int: + """Render the per-core metric distribution across the scaling steps. + + For each trace and each per-core metric (frequency, IPC and per-core power), + one graph shows how the core-to-core distribution evolves as the sweep grows: + X = worker count (one violin + box per scaling step, evenly spaced), Y = the + metric. The violin shows the density across cores, the box the median (red), + mean (green dashed), quartiles and outliers. + + Where the scaling line graphs plot a single averaged value per step, these + expose the spread hidden behind that average -- e.g. cores starting to + throttle or diverge only past a given worker count. Like the other per-core + scaling graphs they are rendered for all_cores and, when the sweep pins + cores, pinned_cores, and the Y axis is autoscaled (a distribution is + unreadable squished against a zero baseline). + """ + metrics = [ + ("cpu_clock", MonitoringContextKeys.Freq, "Mhz", "Core frequency"), + ] # type: list + if has_ipc: + metrics.append(("cpu_ipc", MonitoringContextKeys.IPC, "IPC", "Core IPC")) + metrics.append(("cpu_core_power", MonitoringContextKeys.PowerConsumption, None, "CPU Core power consumption")) + + rendered = 0 + for trace in args.traces: + trace_benches = trace.get_benches_by_job_per_emp(job) + if emp not in trace_benches or not trace_benches[emp]["bench"]: + continue + benches = trace_benches[emp]["bench"] + engine = benches[0].get_title_engine_name().replace(" ", "_") + trace_slug = trace.get_name().replace(" ", "_").replace("/", "_") + # all_cores always; pinned_cores when at least one run of the sweep pins cores. + variants = [("all_cores", False, None)] # type: list + if any(bench.cpu_pin() for bench in benches): + variants.append(("pinned_cores", True, "View limited to the pinned cores of each scaling step")) + + for dirname, context, unit, item_title in metrics: + for dir_suffix, use_pinned, note in variants: + data = [] # type: list + labels = [] # type: list + metric_unit = "" + for bench in sorted(benches, key=lambda b: b.workers()): + names = bench.pinned_core_names() if use_pinned else None + components = bench.get_all_metrics(context, "Core", names) + if not components: + continue + data.append([float(np.mean(component.get_mean())) for component in components]) + labels.append(bench.workers()) + metric_unit = bench.get_metric_unit(context) + if not data: + continue + + title = f'{args.title}\n\n{item_title} per-core distribution scaling via "{job}" benchmark job\n\n Stressor: ' + title += f"{benches[0].get_title_engine_name()} for {benches[0].duration()} seconds" + graph = Graph( + args, + title, + "Workers (scaling step)", + unit or metric_unit, + temp_outdir.joinpath(dirname, dir_suffix), + f"scaling_{dirname}_distribution_{trace_slug}_{engine}", + square=True, + show_source_file=trace, + title_note=note, + ) + ax = graph.get_ax() + positions = list(range(1, len(data) + 1)) + parts = ax.violinplot(data, positions=positions, widths=0.7, showextrema=False) + for body in parts["bodies"]: + body.set_facecolor("tab:blue") + body.set_alpha(0.25) + ax.boxplot( + data, + positions=positions, + widths=0.15, + showmeans=True, + meanline=True, + medianprops=dict(color="tab:red"), + meanprops=dict(color="tab:green", linestyle="--"), + flierprops=dict(marker=".", markersize=3, alpha=0.4), + ) + ax.set_xticks(positions) + ax.set_xticklabels(labels) + # Same two-tier Y grid as the linearity-deviation graph (solid + # major lines plus a fainter dashed midline between the major Y + # ticks), but no X grid: vertical lines would cut through the + # violins and hide their shape. + ax.yaxis.set_minor_locator(AutoMinorLocator(2)) + ax.grid(which="major", axis="y", linewidth=0.7, linestyle="solid", color="0.6") + ax.grid(which="minor", axis="y", linewidth=0.6, linestyle="dashed", color="0.75") + # Same legend as the linearity-deviation graph (lower left, trace + # name), on top of the median/mean key: the blue violins are this + # trace's data. + legend = ax.legend( + handles=[ + Patch(facecolor="tab:blue", alpha=0.25, label=trace.get_name()), + Line2D([], [], color="tab:red", label="median"), + Line2D([], [], color="tab:green", linestyle="--", label="mean"), + ], + loc="lower left", + fontsize=8, + ) + graph.needs_legend = False + graph.render(extra_legend=legend) + rendered += 1 + return rendered + + +def _bench_detail_metrics(bench) -> dict: + """Return the per-instance stress-ng detail metrics of a bench, if any. + + stress-ng exposes, through its YAML output, one value per individual worker + instance under the bench "detail" key (e.g. per-instance "bogo op/s"). + Returns a mapping metric-name -> list of per-instance values, keeping only + non-empty numeric lists; an empty dict when the bench carries no such detail + (older runs, non-stress-ng engines, or a skipped job). + """ + if bench.skipped(): + return {} + detail = bench.get("detail") + if not isinstance(detail, dict): + return {} + metrics = {} # type: dict[str, list] + for name, values in detail.items(): + if isinstance(values, list) and values and all(isinstance(v, (int, float)) for v in values): + metrics[name] = [float(v) for v in values] + return metrics + + +def render_scaling_perf_distributions(args, temp_outdir, job: str, emp: str) -> int: + """Render the per-instance performance distribution across the scaling steps. + + When stress-ng exposes its individual per-worker results (the bench "detail" + metrics, e.g. per-instance "bogo op/s" extracted from its YAML output), one + graph per trace and per detail metric shows how the instance-to-instance + spread evolves as the sweep grows: X = worker count (one violin + box per + scaling step, evenly spaced), Y = the metric. The violin shows the density + across the individual stressor instances, the box the median (red), mean + (green dashed), quartiles and outliers. + + This mirrors the per-core distribution graphs rendered under all_cores / + pinned_cores, but is built from the stressor's own individual results rather + than the monitoring, and lands in the perf/per_worker_distribution directory. + The Y axis is autoscaled (a distribution is unreadable squished against a zero + baseline). Nothing is rendered when no trace exposes detail. + """ + rendered = 0 + for trace in args.traces: + trace_benches = trace.get_benches_by_job_per_emp(job) + if emp not in trace_benches or not trace_benches[emp]["bench"]: + continue + benches = sorted(trace_benches[emp]["bench"], key=lambda b: b.workers()) + engine = benches[0].get_title_engine_name().replace(" ", "_") + trace_slug = trace.get_name().replace(" ", "_").replace("/", "_") + + # Collect, per detail metric, one per-instance distribution per scaling step. + per_metric = {} # type: dict[str, dict[str, list]] + for bench in benches: + for name, values in _bench_detail_metrics(bench).items(): + series = per_metric.setdefault(name, {"data": [], "labels": []}) + series["data"].append(values) + series["labels"].append(bench.workers()) + + for metric_name, series in per_metric.items(): + data = series["data"] + labels = series["labels"] + clean_metric = metric_name.replace(" ", "_").replace("/", "") + title = ( + f'{args.title}\n\nPerformance per worker distribution scaling via "{job}" benchmark job\n\n Stressor: ' + ) + title += f"{benches[0].get_title_engine_name()} for {benches[0].duration()} seconds" + # Rendered one trace at a time, so add the host info like the other + # per-trace graphs (serial/product/bios/kernel + processor). + title += f"\n{benches[0].get_system_title()}" + graph = Graph( + args, + title, + "Workers (scaling step)", + metric_name, + temp_outdir.joinpath("perf", "per_worker_distribution"), + f"scaling_perf_distribution_{clean_metric}_{trace_slug}_{engine}", + square=True, + show_source_file=trace, + ) + ax = graph.get_ax() + positions = list(range(1, len(data) + 1)) + parts = ax.violinplot(data, positions=positions, widths=0.7, showextrema=False) + for body in parts["bodies"]: + body.set_facecolor("tab:blue") + body.set_alpha(0.25) + ax.boxplot( + data, + positions=positions, + widths=0.15, + showmeans=True, + meanline=True, + medianprops=dict(color="tab:red"), + meanprops=dict(color="tab:green", linestyle="--"), + flierprops=dict(marker=".", markersize=3, alpha=0.4), + ) + ax.set_xticks(positions) + ax.set_xticklabels(labels) + # Same two-tier Y grid as the linearity-deviation graph (solid major + # lines plus a fainter dashed midline between the major Y ticks), but + # no X grid: vertical lines would cut through the violins and hide + # their shape. + ax.yaxis.set_minor_locator(AutoMinorLocator(2)) + ax.grid(which="major", axis="y", linewidth=0.7, linestyle="solid", color="0.6") + ax.grid(which="minor", axis="y", linewidth=0.6, linestyle="dashed", color="0.75") + # Same legend as the linearity-deviation graph (lower left, trace + # name), on top of the median/mean key: the blue violins are this + # trace's data. + legend = ax.legend( + handles=[ + Patch(facecolor="tab:blue", alpha=0.25, label=trace.get_name()), + Line2D([], [], color="tab:red", label="median"), + Line2D([], [], color="tab:green", linestyle="--", label="mean"), + ], + loc="lower left", + fontsize=8, + ) + graph.needs_legend = False + graph.render(extra_legend=legend) + rendered += 1 + return rendered + + +def _scaling_perf_value(bench, perf: str) -> float: + """Per-step performance value, mirroring add_perf's memrate special-case.""" + if bench.engine_module() in ["memrate"]: + return float(bench.get(perf)["sum_speed"]) + return float(bench.get(perf)) + + +def render_scaling_linearity_deviation(args, temp_outdir, job: str, emp: str) -> int: + """Render, per trace, how far measured performance deviates from linear scaling. + + For each trace one graph plots, per scaling step, the signed deviation of the + measured performance from the ideal linear projection anchored on the first + step (slope = perf0 / workers0, the same anchor as the perf-graph projection + lines): deviation = (measured / ideal - 1) * 100. So 0% is perfectly linear, a + step running at 80% of the ideal reads -20%, and a superlinear step goes + positive. The signed area to the 0% baseline is filled red below (scaling + loss) and green above (superlinear), and the worst and final steps are + annotated -- summarising the whole sweep's scaling quality at a glance. + + Only rendered when the sweep has more than 3 steps, below which the trend + carries little information. Lands in the perf/linearity_deviation directory. + """ + # First pass: compute every trace's deviation series, then derive a single Y + # range shared by all the per-trace graphs so they can be compared visually. + series = [] # type: list + for trace in args.traces: + trace_benches = trace.get_benches_by_job_per_emp(job) + if emp not in trace_benches or not trace_benches[emp]["bench"]: + continue + perf = trace_benches[emp]["metrics"][0][0] + rows = sorted(trace_benches[emp]["bench"], key=lambda b: b.workers()) + if len(rows) <= 3: + continue + workers = np.array([bench.workers() for bench in rows], dtype=float) + measured = np.array([_scaling_perf_value(bench, perf) for bench in rows], dtype=float) + # No valid anchor for the linear projection when the first step has no + # throughput (e.g. an incomplete run reporting effective_runtime=0, hence + # 0 perf): ideal would be all-zeros and measured/ideal would be NaN/Inf. + if workers[0] == 0 or measured[0] <= 0: + continue + ideal = (measured[0] / workers[0]) * workers + deviation = (measured / ideal - 1.0) * 100.0 + # Skip if any step still produced a non-finite deviation, so the shared + # Y range below can't become NaN/Inf (which crashes set_ylim). + if not np.all(np.isfinite(deviation)): + continue + series.append((trace, rows, workers, deviation)) + + if not series: + return 0 + + # Common Y limits across all traces (always including the 0% baseline). + all_dev = np.concatenate([dev for _, _, _, dev in series]) + dev_min = min(float(all_dev.min()), 0.0) + dev_max = max(float(all_dev.max()), 0.0) + pad = (dev_max - dev_min) * 0.08 or 1 + ylim = (dev_min - pad, dev_max + pad) + + rendered = 0 + for trace, rows, workers, deviation in series: + engine = rows[0].get_title_engine_name().replace(" ", "_") + trace_slug = trace.get_name().replace(" ", "_").replace("/", "_") + title = f'{args.title}\n\nPerformance scaling linearity deviation via "{job}" benchmark job\n\n Stressor: ' + title += f"{rows[0].get_title_engine_name()} for {rows[0].duration()} seconds" + # As the graphs are rendered one trace at a time, add the host info + # (serial/product/bios/kernel + processor) like the other per-trace graphs. + title += f"\n{rows[0].get_system_title()}" + # Same X label as the perf scaling graph: report the logical-cores-per-worker + # ratio under "Workers" when it is constant across the sweep. + ratios = [bench.workers() / (len(bench.cpu_pin()) or bench.workers()) for bench in rows] + xlabel = "Workers" + if stdev(ratios) == 0: + cores = "core" if ratios[0] == 1 else "cores" + xlabel += f"\n({int(ratios[0])} logical {cores} per worker)" + graph = Graph( + args, + title, + xlabel, + "Deviation from linear scaling [%] (0 = ideal)", + temp_outdir.joinpath("perf", "linearity_deviation"), + f"scaling_linearity_deviation_{trace_slug}_{engine}", + square=True, + show_source_file=trace, + ) + ax = graph.get_ax() + # 0% baseline = perfect linear scaling; depth below it is the scaling loss. + ax.axhline(0, color="0.3", linewidth=1.2, label="linear performance projection") + ax.fill_between(workers, deviation, 0, where=(deviation <= 0), color="tab:red", alpha=0.15, interpolate=True) + ax.fill_between(workers, deviation, 0, where=(deviation >= 0), color="tab:green", alpha=0.15, interpolate=True) + ax.plot(workers, deviation, color="tab:blue", marker="o", markersize=3, label=trace.get_name()) + worst = int(np.argmin(deviation)) + ax.annotate( + f"{deviation[worst]:+.0f}%", + xy=(workers[worst], deviation[worst]), + xytext=(0, -12), + textcoords="offset points", + ha="center", + color="tab:red", + fontweight="bold", + ) + # Shared Y range so every trace's graph is directly comparable. + ax.set_ylim(*ylim) + # Reuse the perf scaling graph's X axis (its prepare_axes(8, 4)): start at 0 + # with a major tick every 8 workers and a minor every 4, so the two graphs + # share the same worker scale and line up when read together. + ax.set_xlim(xmin=0) + ax.xaxis.set_major_locator(MultipleLocator(8)) + ax.xaxis.set_minor_locator(MultipleLocator(4)) + # Grid to ease reading the depth: solid major lines plus a fainter dashed + # midline between ticks (Y midline kept between the major Y ticks). + ax.yaxis.set_minor_locator(AutoMinorLocator(2)) + ax.grid(which="major", linewidth=0.7, linestyle="solid", color="0.6") + ax.grid(which="minor", linewidth=0.6, linestyle="dashed", color="0.75") + legend = ax.legend(loc="lower left", fontsize=8) + graph.needs_legend = False + graph.render(extra_legend=legend) + rendered += 1 + return rendered + + +# Colours cycled across NUMA domains in the render_numa_scaling_* graphs below, so +# each domain keeps the same colour everywhere it appears. tab20 gives 20 distinct +# colours, enough for every domain count seen on real systems so far; it only wraps +# (and two domains start sharing a colour) past that. +_NUMA_DOMAIN_COLORS = [matplotlib.colormaps["tab20"](i) for i in range(20)] + +# Pale background washes used by render_numa_scaling_ridgelines to mark which CPU +# package each NUMA domain belongs to, cycled per package. Kept very light so they +# stay subordinate to the (much more saturated) per-domain ridge colours. +_NUMA_PACKAGE_BAND_COLORS = ["#eaf1fb", "#fbf1e2", "#eaf7ee", "#f6eef8"] + + +# hwbench does not record which package a NUMA domain sits on, but same-package +# domains are always much closer to each other than cross-package ones (see the +# sample matrices in hwbench/environment/numa.py: ~10-12 within a package, 32+ +# across). 20 sits comfortably between the two tiers. +_NUMA_SAME_PACKAGE_DISTANCE = 20 + + +def _numa_domains_by_package(numa_nodes: dict, numa_distances: dict) -> dict | None: + """Group NUMA domains into CPU packages, from the distance matrix. + + Connects domains whose mutual distance is below _NUMA_SAME_PACKAGE_DISTANCE + (union-find) and returns {domain: package index}. Returns None when there is + nothing to distinguish (everything lands in one group, e.g. a single socket) + or the matrix is missing/incomplete. + """ + if len(numa_nodes) <= 1 or not numa_distances: + return None + domains = sorted(numa_nodes) + if not all(d in numa_distances and len(numa_distances[d]) > max(domains) for d in domains): + return None + + parent = {d: d for d in domains} + + def find(node): + while parent[node] != node: + parent[node] = parent[parent[node]] + node = parent[node] + return node -from graph.graph import GRAPH_TYPES, Graph, statistics_in_label + for d in domains: + for o in domains: + if d != o and numa_distances[d][o] < _NUMA_SAME_PACKAGE_DISTANCE: + root_d, root_o = find(d), find(o) + if root_d != root_o: + parent[root_d] = root_o + groups: dict = {} + for d in domains: + groups.setdefault(find(d), []).append(d) -def smp_scaling_graph(args, output_dir, job: str, traces_name: list) -> int: + if len(groups) <= 1: + return None + ordered_groups = sorted(groups.values(), key=min) + return {d: package for package, group in enumerate(ordered_groups) for d in group} + + +def _numa_scaling_metrics(has_ipc: bool) -> list: + """Metrics rendered by the render_numa_scaling_* graphs below.""" + metrics = [ + ("cpu_clock", MonitoringContextKeys.Freq, "Mhz", "Core frequency per NUMA domain"), + ] # type: list + if has_ipc: + metrics.append(("cpu_ipc", MonitoringContextKeys.IPC, "IPC", "Core IPC per NUMA domain")) + metrics.append( + ( + "cpu_core_power", + MonitoringContextKeys.PowerConsumption, + None, + "CPU Core power consumption per NUMA domain", + ) + ) + return metrics + + +def _numa_scaling_variants(benches: list) -> list: + """all_numa always; pinned_numa when at least one run of the sweep pins cores.""" + variants = [("all_numa", False, None)] # type: list + if any(bench.cpu_pin() for bench in benches): + variants.append(("pinned_numa", True, "View limited to the pinned logical cores of each scaling step")) + return variants + + +def _collect_numa_step_domain_values(benches: list, context, numa_nodes, use_pinned: bool) -> dict: + """Return {worker count: {domain: [per-core steady-state values]}} for one metric/variant.""" + per_step = {} # type: dict[int, dict[int, list]] + for bench in benches: + pinned = bench.pinned_core_names() if use_pinned else None + step_domains = {} + for node in sorted(numa_nodes): + core_names = {f"Core_{cpu}" for cpu in numa_nodes[node]} + if pinned is not None: + core_names &= pinned + if not core_names: + continue + cores = bench.get_all_metrics(context, names=core_names) + if not cores: + continue + step_domains[node] = [float(np.mean(core.get_mean())) for core in cores] + if step_domains: + per_step[bench.workers()] = step_domains + return per_step + + +def _numa_scaling_cores_by_domain(numa_nodes, domains: list, benches: list, use_pinned: bool) -> dict: + """{domain: [cpus]}, restricted to the union of cores pinned across the sweep on the pinned view.""" + pinned_union = None # type: set | None + if use_pinned: + pinned_union = set() + for bench in benches: + pinned_union |= {int(name.split("_")[1]) for name in (bench.pinned_core_names() or set())} + cores_by_domain = {} + for node in domains: + cpus = numa_nodes[node] + if pinned_union is not None: + cpus = [cpu for cpu in cpus if cpu in pinned_union] + cores_by_domain[node] = cpus + return cores_by_domain + + +def _save_standalone_figure(args, fig, output_dir, filename: str) -> None: + """Save a figure built without the Graph wrapper (used by grid/multi-axes graphs). + + Mirrors Graph.render()'s save call so standalone figures match the rest of + hwgraph's output (same directory creation, format, dpi and tight bbox). + """ + output_dir.mkdir(parents=True, exist_ok=True) + fig.savefig( + f"{output_dir}/{filename}.{args.format}", + format=args.format, + dpi=args.dpi, + bbox_inches="tight", + pad_inches=1, + ) + fig.clear() + plt.close(fig) + + +def _gaussian_kde_curve(values: list, sample_points: np.ndarray) -> np.ndarray: + """Scipy-free Gaussian KDE (Silverman's rule-of-thumb bandwidth), unnormalized.""" + values_arr = np.asarray(values, dtype=float) + std = float(np.std(values_arr)) + bandwidth = 1.06 * std * len(values_arr) ** (-1 / 5) if std > 0 else 1.0 + bandwidth = max(bandwidth, 1e-3) + diffs = (sample_points[:, None] - values_arr[None, :]) / bandwidth + return np.exp(-0.5 * diffs**2).sum(axis=1) + + +# render_numa_scaling_ridgelines lays out one panel per step, _RIDGELINE_MAX_COLS +# per row, so the grid (and the figure) grows taller as a sweep has more steps. +# The header/footer are reserved as a constant number of INCHES rather than a +# fraction of the figure, so they keep the same size and never shrink to +# nothing (or balloon) as the number of rows changes. +_RIDGELINE_PANEL_WIDTH = 3.0 +_RIDGELINE_PANEL_HEIGHT = 3.3 +_RIDGELINE_MAX_COLS = 5 +_RIDGELINE_SUPTITLE_INSET = 0.05 # inches from the top edge to the suptitle anchor +_RIDGELINE_NOTE_INSET = 1.3 # inches from the top edge to the note (when present) +_RIDGELINE_HEADER_INCHES = 1.6 # reserved header height: title only +_RIDGELINE_HEADER_INCHES_NOTE = 2.0 # reserved header height: title + note +_RIDGELINE_FOOTER_INCHES = 0.5 # reserved footer height: source caption +_RIDGELINE_FOOTER_INCHES_PACKAGE = 0.85 # reserved footer height: source caption + package legend +_RIDGELINE_CAPTION_INSET = 0.08 # inches from the bottom edge to the source caption +_RIDGELINE_PACKAGE_LEGEND_INSET = 0.4 # inches from the bottom edge to the package legend + + +def render_numa_scaling_ridgelines(args, temp_outdir, job: str, emp: str, has_ipc: bool) -> int: + """Render the per-NUMA-domain metric distribution at every scaling step. + + For each trace and each per-NUMA metric (frequency, IPC and per-core + power, aggregated by domain), one graph lays out one ridgeline panel per + scaling step -- every step, not a sample -- in a grid: each panel is a + stacked density (ridgeline) per NUMA domain, preserving the full + distribution shape (skew, bimodality) that a single averaged value would + flatten. + + Like the other per-domain scaling graphs, rendered for all_numa (every + core of each domain) and, when the sweep pins cores, pinned_numa. Needs + the NUMA topology in each trace; older traces without it are skipped. + """ + rendered = 0 + for trace in args.traces: + # Topology is per-host: read it from this trace, not a shared reference, + # so hosts with different NUMA-node counts each render their own domains. + numa_nodes = trace.get_numa_nodes() + if not numa_nodes: + continue + # Best-effort: which package each domain sits on, so multi-socket systems get a + # background wash marking the boundary; None on a single socket or when the + # distance matrix doesn't cleanly cluster (older trace, unexpected encoding). + domain_package = _numa_domains_by_package(numa_nodes, trace.get_numa_distances()) + + trace_benches = trace.get_benches_by_job_per_emp(job) + if emp not in trace_benches or not trace_benches[emp]["bench"]: + continue + benches = sorted(trace_benches[emp]["bench"], key=lambda b: b.workers()) + engine = benches[0].get_title_engine_name().replace(" ", "_") + trace_slug = trace.get_name().replace(" ", "_").replace("/", "_") + variants = _numa_scaling_variants(benches) + + for dirname, context, unit, item_title in _numa_scaling_metrics(has_ipc): + for dir_suffix, use_pinned, note in variants: + per_step = _collect_numa_step_domain_values(benches, context, numa_nodes, use_pinned) + if not per_step: + continue + steps = sorted(per_step) + domains = sorted({node for step_domains in per_step.values() for node in step_domains}) + metric_unit = unit or benches[0].get_metric_unit(context) + cores_by_domain = _numa_scaling_cores_by_domain(numa_nodes, domains, benches, use_pinned) + domain_color = { + node: _NUMA_DOMAIN_COLORS[i % len(_NUMA_DOMAIN_COLORS)] for i, node in enumerate(domains) + } + + all_values = [ + v for step_domains in per_step.values() for values in step_domains.values() for v in values + ] + pad = (max(all_values) - min(all_values)) * 0.05 or 1 + yy = np.linspace(min(all_values) - pad, max(all_values) + pad, 200) + + # Every step gets a panel -- no sampling -- laid out as a grid so the + # figure grows in rows rather than becoming arbitrarily wide. + cols = min(_RIDGELINE_MAX_COLS, len(steps)) + rows = -(-len(steps) // cols) # ceil division + header_inches = _RIDGELINE_HEADER_INCHES_NOTE if note else _RIDGELINE_HEADER_INCHES + footer_inches = ( + _RIDGELINE_FOOTER_INCHES_PACKAGE if domain_package is not None else _RIDGELINE_FOOTER_INCHES + ) + fig_width = _RIDGELINE_PANEL_WIDTH * cols + fig_height = _RIDGELINE_PANEL_HEIGHT * rows + header_inches + footer_inches + fig, axes = plt.subplots(rows, cols, figsize=(fig_width, fig_height), sharey=True, squeeze=False) + flat_axes = axes.flatten() + + for ax, step in zip(flat_axes, steps): + step_domains = per_step[step] + present = [node for node in domains if node in step_domains] + offsets = np.arange(len(present)) * 1.1 + + # Background wash per CPU package, drawn first so the ridges sit on top. + if domain_package is not None: + package_offsets: dict = {} + for offset, node in zip(offsets, present): + package_offsets.setdefault(domain_package[node], []).append(offset) + for package, offs in package_offsets.items(): + ax.axvspan( + min(offs) - 0.55, + max(offs) + 0.55, + color=_NUMA_PACKAGE_BAND_COLORS[package % len(_NUMA_PACKAGE_BAND_COLORS)], + zorder=0, + ) + + for offset, node in zip(offsets, present): + density = _gaussian_kde_curve(step_domains[node], yy) + peak = density.max() + if peak > 0: + density = density / peak + ax.fill_betweenx(yy, offset, offset + density, color=domain_color[node], alpha=0.8, zorder=2) + ax.plot(offset + density, yy, color="black", linewidth=0.5, zorder=2) + ax.set_xticks(offsets) + ax.set_xticklabels([f"N{node}" for node in present], rotation=90, fontsize=6) + ax.set_title(f"{step} workers", fontsize=9) + ax.set_xlim(-0.3, (offsets[-1] if len(offsets) else 0) + 1.3) + ax.grid(which="major", axis="y", linewidth=0.4, linestyle="dashed", color="0.85", zorder=1) + for ax in flat_axes[len(steps) :]: + ax.axis("off") + # Y label on the leftmost panel of every row (sharey makes the scale common). + for row in range(rows): + flat_axes[row * cols].set_ylabel(metric_unit) + + node_width = max((len(str(node)) for node in domains), default=1) + handles = [Line2D([], [], color=domain_color[node], linewidth=6) for node in domains] + labels = [ + f"NUMA {node:>{node_width}}" + + (f" (pkg {domain_package[node]})" if domain_package is not None else "") + + f": {numa_core_blocks(cores_by_domain[node])}" + for node in domains + ] + fig.legend( + handles, + labels, + loc="center left", + bbox_to_anchor=(1.0, 0.5), + title="NUMA domain [cores]", + fontsize=8, + ) + # Separate legend explaining the background wash colour, centered below the + # grid (fig.legend keeps every call as its own legend, unlike ax.legend which + # replaces the last one). + if domain_package is not None: + package_count = len(set(domain_package.values())) + package_handles = [ + Patch( + facecolor=_NUMA_PACKAGE_BAND_COLORS[package % len(_NUMA_PACKAGE_BAND_COLORS)], + edgecolor="0.6", + ) + for package in range(package_count) + ] + package_labels = [f"CPU package {package}" for package in range(package_count)] + fig.legend( + package_handles, + package_labels, + loc="lower center", + bbox_to_anchor=(0.5, _RIDGELINE_PACKAGE_LEGEND_INSET / fig_height), + ncol=package_count, + fontsize=8, + ) + + title = f'{args.title}\n\n{item_title} distribution scaling via "{job}" benchmark job\n\n Stressor: ' + title += f"{benches[0].get_title_engine_name()} for {benches[0].duration()} seconds" + fig.suptitle(title, fontsize=11, y=1 - _RIDGELINE_SUPTITLE_INSET / fig_height) + if note: + fig.text( + 0.5, + 1 - _RIDGELINE_NOTE_INSET / fig_height, + note, + ha="center", + color="darkred", + fontweight="bold", + fontsize=10, + ) + fig.text( + 0.01, + _RIDGELINE_CAPTION_INSET / fig_height, + f"data plotted from {trace.get_filename()}", + fontsize=8, + bbox=dict(boxstyle="round", facecolor="white", alpha=0.6), + ) + fig.tight_layout( + rect=( + 0.02, + footer_inches / fig_height, + 1, + 1 - header_inches / fig_height, + ) + ) + + _save_standalone_figure( + args, + fig, + temp_outdir.joinpath(dirname, dir_suffix), + f"scaling_{dirname}_numa_ridgeline_{trace_slug}_{engine}", + ) + rendered += 1 + return rendered + + +def performance_scaling_graph(args, output_dir, job: str, traces_name: list) -> int: """Render line graphs to compare performance scaling.""" rendered_graphs = 0 - temp_outdir = output_dir.joinpath("smp_scaling") + temp_outdir = output_dir.joinpath("scaling") # We extract the skeleton from the first trace # This will give us the name of the engine module parameters and # the metrics we need to plot benches = args.traces[0].get_benches_by_job_per_emp(job) if args.verbose: - print(f"SMP scaling: working on job '{job}' : {len(benches.keys())} engine_module_parameter to render") + print(f"Performance scaling: working on job '{job}' : {len(benches.keys())} engine_module_parameter to render") # For all subjobs sharing the same engine module parameter # i.e int128 for emp in benches: @@ -30,6 +929,11 @@ def smp_scaling_graph(args, output_dir, job: str, traces_name: list) -> int: # Same as above but restricted to the cores pinned during each benchmark aggregated_cpu_clock_pinned = {} # type: dict[str, dict[str, Any]] aggregated_cpu_clock_pinned_err = {} # type: dict[str, dict[str, Any]] + # IPC, same handling as cpu_clock (only rendered when the trace has IPC) + aggregated_ipc = {} # type: dict[str, dict[str, Any]] + aggregated_ipc_err = {} # type: dict[str, dict[str, Any]] + aggregated_ipc_pinned = {} # type: dict[str, dict[str, Any]] + aggregated_ipc_pinned_err = {} # type: dict[str, dict[str, Any]] # Whether at least one run of this sweep pinned cores (each run may pin a # different set), used to decide if the pinned-cores graph is relevant. any_pinned = False @@ -39,9 +943,15 @@ def smp_scaling_graph(args, output_dir, job: str, traces_name: list) -> int: # If we can't detect several bench on the same emp, it means there was no scaling if len(args.traces[0].get_benches_by_job_per_emp(job)[emp]["bench"]) == 1: - print(f"SMP scaling: No scaling detected on job '{job}', skipping graph") + print(f"Performance scaling: No scaling detected on job '{job}', skipping graph") continue + # IPC is not always collected; only aggregate/render it when present. + try: + has_ipc = bool(benches[emp]["bench"][0].get_monitoring_metric(MonitoringContextKeys.IPC)) + except KeyError: + has_ipc = False + # For each metric we need to plot for perf in perf_list: if perf not in aggregated_perfs: @@ -53,6 +963,10 @@ def smp_scaling_graph(args, output_dir, job: str, traces_name: list) -> int: aggregated_cpu_clock_err[perf] = {} aggregated_cpu_clock_pinned[perf] = {} aggregated_cpu_clock_pinned_err[perf] = {} + aggregated_ipc[perf] = {} + aggregated_ipc_err[perf] = {} + aggregated_ipc_pinned[perf] = {} + aggregated_ipc_pinned_err[perf] = {} # For every trace file given at the command line for trace in args.traces: workers[trace.get_name()] = [] @@ -79,6 +993,10 @@ def smp_scaling_graph(args, output_dir, job: str, traces_name: list) -> int: aggregated_cpu_clock_err[perf][trace.get_name()] = [] aggregated_cpu_clock_pinned[perf][trace.get_name()] = [] aggregated_cpu_clock_pinned_err[perf][trace.get_name()] = [] + aggregated_ipc[perf][trace.get_name()] = [] + aggregated_ipc_err[perf][trace.get_name()] = [] + aggregated_ipc_pinned[perf][trace.get_name()] = [] + aggregated_ipc_pinned_err[perf][trace.get_name()] = [] bench.add_perf( perf, @@ -98,11 +1016,21 @@ def smp_scaling_graph(args, output_dir, job: str, traces_name: list) -> int: cpu_clock_err=aggregated_cpu_clock_pinned_err[perf][trace.get_name()], cpu_clock_cores=bench.pinned_core_names(), ) + if has_ipc: + bench.add_perf( + ipc=aggregated_ipc[perf][trace.get_name()], + ipc_err=aggregated_ipc_err[perf][trace.get_name()], + ) + bench.add_perf( + ipc=aggregated_ipc_pinned[perf][trace.get_name()], + ipc_err=aggregated_ipc_pinned_err[perf][trace.get_name()], + ipc_cores=bench.pinned_core_names(), + ) if bench.cpu_pin(): any_pinned = True - # Let's render all graphs types - for graph_type in GRAPH_TYPES: + # Let's render all graphs types (IPC only when the trace collected it) + for graph_type in GRAPH_TYPES + (["cpu_ipc"] if has_ipc else []): # Let's render each performance graph graph_type_title = "" @@ -112,35 +1040,51 @@ def smp_scaling_graph(args, output_dir, job: str, traces_name: list) -> int: y_label = unit # err_source is only set for graphs plotted with error bars. err_source = None + # pinned_* are only set for per-core metrics (cpu_clock, ipc). + pinned_y_source = None + pinned_err_source = None + # The raw performance graph gets an ideal linear-scaling overlay. + is_perf_graph = False if "perf_watt" in graph_type: - graph_type_title = f"SMP scaling {graph_type}: '{bench.get_title_engine_name()} / {args.traces[0].get_metric_name()}'" + graph_type_title = f"Performance scaling {graph_type}: '{bench.get_title_engine_name()} / {args.traces[0].get_metric_name()}'" y_label = f"{unit} per Watt" outfile = f"scaling_watt_{clean_perf}_{bench.get_title_engine_name().replace(' ', '_')}" y_source = aggregated_perfs_watt elif "watts" in graph_type: - graph_type_title = f"SMP scaling {graph_type}: {args.traces[0].get_metric_name()}" + graph_type_title = f"Performance scaling {graph_type}: {args.traces[0].get_metric_name()}" outfile = f"scaling_watt_{clean_perf}_{bench.get_title_engine_name().replace(' ', '_')}" y_label = "Watts" y_source = aggregated_watt err_source = aggregated_watt_err elif "cpu_clock" in graph_type: - graph_type_title = f"SMP scaling {graph_type}: {args.traces[0].get_metric_name()}" + graph_type_title = f"Performance scaling {graph_type}: {args.traces[0].get_metric_name()}" outfile = f"scaling_cpu_clock_{clean_perf}_{bench.get_title_engine_name().replace(' ', '_')}" y_label = "Mhz" y_source = aggregated_cpu_clock err_source = aggregated_cpu_clock_err + pinned_y_source = aggregated_cpu_clock_pinned + pinned_err_source = aggregated_cpu_clock_pinned_err + elif "ipc" in graph_type: + graph_type_title = f"Performance scaling {graph_type}: {bench.get_title_engine_name()}" + outfile = f"scaling_cpu_ipc_{clean_perf}_{bench.get_title_engine_name().replace(' ', '_')}" + y_label = "IPC" + y_source = aggregated_ipc + err_source = aggregated_ipc_err + pinned_y_source = aggregated_ipc_pinned + pinned_err_source = aggregated_ipc_pinned_err else: - graph_type_title = f"SMP scaling {graph_type}: {bench.get_title_engine_name()}" + graph_type_title = f"Performance scaling: {bench.get_title_engine_name()}" outfile = f"scaling_{clean_perf}_{bench.get_title_engine_name().replace(' ', '_')}" y_source = aggregated_perfs + is_perf_graph = True - # The cpu clock scaling graph is rendered twice: once averaging - # every system core, once averaging only the cores pinned during - # each benchmark. Each variant is stored in its own subdirectory - # so the two renderings never collide. - if "cpu_clock" in graph_type: + # The per-core metrics (cpu_clock, ipc) are rendered twice: once + # averaging every system core, once averaging only the cores + # pinned during each benchmark. Each variant is stored in its own + # subdirectory so the two renderings never collide. + if pinned_y_source is not None: variants = [ - ("all_cores", aggregated_cpu_clock, aggregated_cpu_clock_err, None), + ("all_cores", y_source, err_source, None), ] # type: list # Only add the pinned-cores variant when at least one run of # the sweep actually pinned cores (otherwise it duplicates @@ -148,9 +1092,7 @@ def smp_scaling_graph(args, output_dir, job: str, traces_name: list) -> int: # cores, so we don't list a single global range here. if any_pinned: pinned_note = "View limited to the pinned cores of each scaling step" - variants.append( - ("pinned_cores", aggregated_cpu_clock_pinned, aggregated_cpu_clock_pinned_err, pinned_note) - ) + variants.append(("pinned_cores", pinned_y_source, pinned_err_source, pinned_note)) else: variants = [(None, y_source, err_source, None)] @@ -196,16 +1138,32 @@ def smp_scaling_graph(args, output_dir, job: str, traces_name: list) -> int: "tab:brown", "tab:grey", ] + # Pad the trace name and the values to a common width so the + # per-trace legend table stays aligned even when trace names + # (or values) have different lengths. + max_title_length = max((len(trace_name) for trace_name in v_y_source[perf]), default=0) + max_value_length = max( + ( + get_max_value_string_length(np.array(v_y_source[perf][trace_name])) + for trace_name in v_y_source[perf] + ), + default=0, + ) + # Ideal linear-scaling projection lines, collected here as + # (colour, trace name) and appended to the end of the per-trace + # statistics legend (see below) under their own sub-header, so + # they stay in a single, naturally aligned legend box. + projection_specs = [] # type: list # Traces are not ordered by growing cpu cores count # We need to prepare the x_serie to be sorted this way # The y_serie depends on the graph type - for trace_name, color_name, e_color in zip(aggregated_perfs[perf], colors, cycle(e_colors)): + for trace_name, color_name, e_color in zip(aggregated_perfs[perf], cycle(colors), cycle(e_colors)): # Each trace can have different numbers of workers based on the hardware setup # So let's consider the list of x values per trace. order = np.argsort(workers[trace_name]) x_serie = np.array(workers[trace_name])[order] y_serie = np.array(v_y_source[perf][trace_name])[order] - series_label = statistics_in_label(trace_name, y_serie) + series_label = statistics_in_label(trace_name, y_serie, max_title_length, max_value_length) # If we have an error distribution, let's use errorbars if v_err_source is not None: graph.get_ax().errorbar( @@ -227,8 +1185,101 @@ def smp_scaling_graph(args, output_dir, job: str, traces_name: list) -> int: marker="o", ) + # Annotate the peak (top) value of this trace's line so it + # can be read straight off the graph, drawn in the line's + # own colour and using the same human-readable formatting as + # the Y axis. + if len(y_serie): + peak = int(np.argmax(y_serie)) + graph.get_ax().annotate( + graph.human_format(y_serie[peak]), + xy=(x_serie[peak], y_serie[peak]), + xytext=(0, 8), + textcoords="offset points", + ha="center", + color=color_name, + fontweight="bold", + fontsize=8, + ) + + # On the raw performance graph, overlay the ideal + # linear-scaling projection for this trace: the line the + # performance would follow if every added worker kept the + # per-worker throughput measured at the first scaling + # step (slope = y0 / x0, i.e. a line through the origin + # and the first point). Same colour as the trace, dotted + # so it reads as a reference, not a measured curve; the + # gap to the measured line is the scaling loss. Only drawn + # when the sweep has more than 3 steps, below which the + # comparison carries little information. + if is_perf_graph and len(x_serie) > 3 and x_serie[0] != 0: + slope = y_serie[0] / x_serie[0] + graph.get_ax().plot( + x_serie, + slope * x_serie, + linestyle=":", + color=color_name, + linewidth=1.5, + alpha=0.9, + zorder=1, + ) + projection_specs.append((color_name, trace_name)) + graph.prepare_axes(8, 4) - graph.render() + # Add a midline between Y ticks to ease value reading (a bit more + # visible than the default minor grid, but still lighter than the major one). + graph.get_ax().yaxis.set_minor_locator(AutoMinorLocator(2)) + graph.get_ax().grid(which="minor", axis="y", linewidth=0.6, linestyle="dashed", color="0.6") + extra_legend = None + if projection_specs: + # Append the projection lines to the END of the per-trace stats + # legend (rather than a second box, which never aligns cleanly + # with the stats table): keeping everything in one legend means + # one width and one set of edges. A blank row separates the two + # blocks, then a centered sub-header introduces the dotted + # projection entries, each labelled with its trace name and + # drawn in that trace's colour. + handles, labels = graph.get_ax().get_legend_handles_labels() + # Centre the sub-header over the label column with leading + # spaces (monospace font, so char count maps to width). + header = "linear performance projection" + label_width = max((len(name) for name in labels + [t for _, t in projection_specs]), default=0) + header_pad = "\x20" * max(0, (label_width - len(header)) // 2) + # Blank separator row between the stats and projection blocks. + handles.append(Line2D([], [], linestyle="none")) + labels.append("\x20") + handles.append(Line2D([], [], linestyle="none")) + labels.append(f"{header_pad}{header}") + for color_name, trace_name in projection_specs: + handles.append(Line2D([], [], linestyle=":", color=color_name, linewidth=1.5, alpha=0.9)) + labels.append(trace_name) + extra_legend = graph.get_ax().legend( + handles, + labels, + bbox_to_anchor=(-0.1, 1), + title="component [min; mean; stddev; max]\n", + ) + graph.get_ax().add_artist(extra_legend) + # We built the legend ourselves; stop Graph.render() rebuilding one. + graph.needs_legend = False + graph.render(extra_legend=extra_legend) rendered_graphs += 1 + # Per-NUMA-domain delta heatmap comparing the two traces (reference vs other). + rendered_graphs += render_numa_delta_heatmaps(args, temp_outdir, job, emp) + + # Per-core distribution (violin + box) across the scaling steps. + rendered_graphs += render_scaling_distributions(args, temp_outdir, job, emp, has_ipc) + + # Per-trace deviation of measured performance from ideal linear scaling. + rendered_graphs += render_scaling_linearity_deviation(args, temp_outdir, job, emp) + + # Per-instance distribution (violin + box) of the stressor's individual + # results across the scaling steps, when the engine exposes them. + rendered_graphs += render_scaling_perf_distributions(args, temp_outdir, job, emp) + + # Per-NUMA-domain distribution across the scaling steps: a ridgeline grid, + # one panel per step, one density per domain within each panel. + rendered_graphs += render_numa_scaling_ridgelines(args, temp_outdir, job, emp, has_ipc) + return rendered_graphs diff --git a/graph/trace.py b/graph/trace.py index 38a30dc4..4a09de3f 100644 --- a/graph/trace.py +++ b/graph/trace.py @@ -227,7 +227,7 @@ def get_system_title(self): d = self.get_trace().get_dmi() c = self.get_trace().get_cpu() k = self.get_trace().get_kernel() - title = f"System: {d['serial']} {d['product']} Bios v{d['bios']['version']} Linux Kernel {k['release']}" + title = f"System:{d['serial']} {d['product']} Bios:v{d['bios']['version']} Linux Kernel:{k['release']}" title += ( f"\nProcessor: {c.get('sockets', 1)}x {c['model']} - " f"{c['physical_cores']} physical cores and {c['numa_domains']} NUMA domains" @@ -299,6 +299,9 @@ def add_perf( cpu_clock=None, cpu_clock_err=None, cpu_clock_cores=None, + ipc=None, + ipc_err=None, + ipc_cores=None, index=None, ) -> None: """Extract performance and power efficiency""" @@ -423,6 +426,40 @@ def add_perf( else: cpu_clock_err[index] = metric + # Same approach as cpu_clock but for the CPU IPC (may be absent). + if ipc is not None: + try: + mm = self.get_monitoring_metric(MonitoringContextKeys.IPC) + except KeyError: + mm = {} + mean_values = [] + min_values = [] + max_values = [] + for ipc_metric in mm: + if ipc_metric != "CPU": + continue + # All cores by default, or only the pinned ones when ipc_cores is set. + for core in mm[ipc_metric]: + if ipc_cores is not None and core not in ipc_cores: + continue + min_values.append(min(mm[ipc_metric][core].get_min())) + mean_values.append(mean(mm[ipc_metric][core].get_mean())) + max_values.append(max(mm[ipc_metric][core].get_max())) + if mean_values: + min_value = min(min_values) + mean_value = mean(mean_values) + max_value = max(max_values) + if index is None: + ipc.append(mean_value) + else: + ipc[index] = mean_value + if ipc_err is not None: + metric = (mean_value - min_value, max_value - mean_value) + if index is None: + ipc_err.append(metric) + else: + ipc_err[index] = metric + except ValueError: fatal(f"No {perf} found in {self.get_bench_name()}") @@ -580,6 +617,27 @@ def get_sanitized_cpu_model(self): def get_sockets_count(self): return self.get_cpu()["sockets"] + def get_numa_nodes(self) -> dict[int, list[int]]: + """Return the {numa domain: [logical cores]} mapping. + + Returns an empty dict for older traces that did not record it. + """ + numa_nodes = self.get_cpu().get("numa_nodes") + if not numa_nodes: + return {} + # JSON object keys are strings; expose them as ints. + return {int(node): cores for node, cores in numa_nodes.items()} + + def get_numa_distances(self) -> dict[int, list[int]]: + """Return the NUMA distance matrix ({source node: [latency to each node]}). + + Returns an empty dict for older traces that did not record it. + """ + distances = self.get_cpu().get("numa_distances") + if not distances: + return {} + return {int(node): latencies for node, latencies in distances.items()} + def get_physical_cores(self): return self.get_cpu()["physical_cores"] diff --git a/graph/versus.py b/graph/versus.py index 31f070b7..e586af90 100644 --- a/graph/versus.py +++ b/graph/versus.py @@ -1,8 +1,13 @@ +from __future__ import annotations + from typing import Any # noqa: F401 +import matplotlib.pyplot as plt import numpy as np from graph.graph import GRAPH_TYPES, Graph +from graph.scaling import _scaling_perf_value +from hwbench.bench.monitoring_structs import MonitoringContextKeys def max_versus_graph(args, output_dir, job: str, traces_name: list) -> int: @@ -191,3 +196,704 @@ def max_versus_graph(args, output_dir, job: str, traces_name: list) -> int: rendered_graphs += 1 return rendered_graphs + + +def _bench_power(bench, metric_name: str) -> float | None: + """Mean power (Watts) of a bench for the trace's selected power metric. + + metric_name is the "component.measure" power metric chosen on the command line + (e.g. CPU.package, BMC.ServerInChassis). None when not collected. + """ + try: + metric = bench.get_monitoring_metric_by_name(MonitoringContextKeys.PowerConsumption, metric_name) + except (KeyError, ValueError): + return None + means = metric.get_mean() + return float(np.mean(means)) if len(means) else None + + +def _bench_avg_core_clock(bench) -> float | None: + """Mean core frequency (MHz) averaged over all cores of a bench, or None.""" + cores = bench.get_all_metrics(MonitoringContextKeys.Freq, "Core") + if not cores: + return None + per_core = [float(np.mean(c.get_mean())) for c in cores if len(c.get_mean())] + return float(np.mean(per_core)) if per_core else None + + +def _bench_avg_ipc(bench) -> float | None: + """Mean IPC (instructions per cycle) averaged over all cores of a bench, or None.""" + cores = bench.get_all_metrics(MonitoringContextKeys.IPC, "Core") + if not cores: + return None + per_core = [float(np.mean(c.get_mean())) for c in cores if len(c.get_mean())] + return float(np.mean(per_core)) if per_core else None + + +def _bench_perf_cov(bench) -> float | None: + """Per-worker performance spread of a bench (coefficient of variation, %). + + Computed from the per-instance "bogo op/s" distribution stress-ng records in + the bench "detail" (extracted from its YAML output); lower means the workers + performed more homogeneously. None when no per-worker detail is available. + """ + detail = bench.get("detail") + if not isinstance(detail, dict): + return None + values = detail.get("bogo op/s") + if not isinstance(values, list) or len(values) < 2: + return None + arr = np.asarray(values, dtype=float) + mean = float(arr.mean()) + if mean == 0: + return None + return float(arr.std() / mean * 100.0) + + +def _trace_job_step_metrics(trace, job: str) -> dict: + """Per engine-module-parameter, per scaling-step metrics for one trace's job. + + For each emp (e.g. float128) return {worker count: metrics} covering every + scaling step, where metrics holds: performance, CPU package power, average + core clock, IPC, worker count, the per-worker performance spread (CoV%) and + the scaling linearity efficiency at that step (step performance vs the ideal + linear projection anchored on the first step -- so the first step reads 100%). + """ + metric_name = trace.get_metric_name() + out = {} # type: dict + for emp, info in trace.get_benches_by_job_per_emp(job).items(): + benches = sorted((b for b in info["bench"] if not b.skipped()), key=lambda b: b.workers()) + if not benches: + continue + perf_key = info["metrics"][0][0] + unit = info["metrics"][1] + workers = [b.workers() for b in benches] + perf = [_scaling_perf_value(b, perf_key) for b in benches] + # First-step slope for the linear projection; None when the first step has + # no throughput (an anchor of 0 would make every linearity undefined). + slope = (perf[0] / workers[0]) if (workers[0] > 0 and perf[0] > 0) else None + per_step = {} # type: dict + for i, bench in enumerate(benches): + lin_full = perf[i] / (slope * workers[i]) * 100 if slope else None + per_step[bench.workers()] = { + "engine_module": bench.engine_module(), + "unit": unit, + "workers": workers[i], + "perf": perf[i], + "power": _bench_power(bench, metric_name), + "clock": _bench_avg_core_clock(bench), + "ipc": _bench_avg_ipc(bench), + "cov": _bench_perf_cov(bench), + "lin_full": lin_full, + } + out[emp] = per_step + return out + + +def _trace_job_metrics(trace, job: str) -> dict: + """Per engine-module-parameter full-load metrics for one trace's job. + + The full-load (max workers) step of _trace_job_step_metrics, kept as the + reference point for the single-report comparison and the scorecard. + """ + out = {} # type: dict + for emp, per_step in _trace_job_step_metrics(trace, job).items(): + if per_step: + out[emp] = per_step[max(per_step)] + return out + + +def _hf(n: float) -> str: + """Human-readable number with a K/M/G suffix (mirrors Graph.human_format).""" + for unit, div in (("G", 1e9), ("M", 1e6), ("K", 1e3)): + if abs(n) >= div: + return f"{n / div:.2f}{unit}" + return f"{n:.1f}" + + +def _ratio(value, ref) -> str: + """Ratio string 'x.xxx' against the reference, or '-' when not computable.""" + if value is None or ref in (None, 0): + return "-" + return f"{value / ref:.2f}x" + + +def _geomean(values) -> float | None: + vals = [v for v in values if v and v > 0] + return float(np.exp(np.mean(np.log(vals)))) if vals else None + + +def _render_traces_comparison( + args, + traces, + metrics, + cores, + sweep, + *, + scope_title: str, + columns_scope: str, + aggregate_scope: str, + ref_missing_reason: str, +) -> str: + """Build the traces-comparison report text shared by the full-load and the + per-step reports. + + metrics maps {trace name: {(job, emp): metrics}} already reduced to the scope + being reported (full load, or one scaling step); a benchmark absent from a + trace's mapping is rendered as N/A. cores/sweep are per-trace. The scope_* + strings tailor the wording (heading suffix, the "Columns" note and the + aggregate header) and ref_missing_reason explains, in a per-section note, why + a benchmark the reference lacks is excluded from the aggregate. Returns the + whole report as text. + """ + ref_name = traces[0].get_name() + # Benchmark keys: those of the reference first (in job order), then any extra + # benchmark other traces have but the reference does not (e.g. AVX-512 on a CPU + # that lacks it). The latter are reported as N/A and excluded from the aggregate. + ordered = list(metrics[ref_name].keys()) + for tm in metrics.values(): + for key in tm: + if key not in ordered: + ordered.append(key) + + # IPC columns are only shown when at least one trace collected IPC. + has_ipc = any(m.get("ipc") is not None for tm in metrics.values() for m in tm.values()) + + headers = [ + "Trace", + "Wrk", + "Perf", + "Δperf", + "Perf/core", + "Δperf/core", + "Power", + "Δpower", + "Perf/W", + "Clock", + "Δclock", + ] + if has_ipc: + headers += ["IPC", "ΔIPC"] + headers += ["CoV", "Linearity"] + left = {"Trace"} + + def _bench_row(tname, i, m, ref_m): + """One table row. ref_m is None when the reference has no value for this + benchmark in this scope -- then every ratio is reported as N/A.""" + ct, cr = cores[tname], cores[ref_name] + pc = m["perf"] / ct if ct else None + + def R(value, ref_value): + return "N/A" if ref_m is None else _ratio(value, ref_value) + + ref_pc = ref_m["perf"] / cr if (ref_m and cr) else None + ref_ppw = ref_m["perf"] / ref_m["power"] if (ref_m and ref_m["power"]) else None + return { + "Trace": tname + ("*" if i == 0 else ""), + "Wrk": str(m["workers"]), + "Perf": _hf(m["perf"]), + "Δperf": R(m["perf"], ref_m["perf"] if ref_m else None), + "Perf/core": _hf(pc) if pc is not None else "-", + "Δperf/core": R(pc, ref_pc), + "Power": f"{m['power']:.0f}W" if m["power"] is not None else "n/a", + "Δpower": R(m["power"], ref_m["power"] if ref_m else None), + "Perf/W": R(m["perf"] / m["power"] if m["power"] else None, ref_ppw), + "Clock": f"{m['clock']:.0f}M" if m["clock"] is not None else "n/a", + "Δclock": R(m["clock"], ref_m["clock"] if ref_m else None), + "IPC": f"{m['ipc']:.2f}" if m["ipc"] is not None else "n/a", + "ΔIPC": R(m["ipc"], ref_m["ipc"] if ref_m else None), + "CoV": f"{m['cov']:.1f}%" if m["cov"] is not None else "-", + # Deviation from perfect linear scaling (like the linearity graphs): + # 0% = perfect, negative = scaling loss. + "Linearity": f"{m['lin_full'] - 100:+.0f}%" if m["lin_full"] is not None else "-", + } + + # Build every row first so column widths are shared across all blocks. + blocks = [] # type: list + for key in ordered: + ref_m = metrics[ref_name].get(key) # None when the reference has no value here + # Engine/unit come from the reference, or any trace that has the benchmark. + sample = ref_m or next(m for tm in metrics.values() if (m := tm.get(key))) + rows = [] + for i, trace in enumerate(traces): + tname = trace.get_name() + m = metrics[tname].get(key) + if m is None: + # Trace has no value for this benchmark in this scope. + rows.append({h: ("N/A" if h != "Trace" else tname + ("*" if i == 0 else "")) for h in headers}) + continue + rows.append(_bench_row(tname, i, m, ref_m)) + note = None if ref_m else f"reference {ref_name} {ref_missing_reason}; excluded from the aggregate" + blocks.append((f"{sample['engine_module']}/{key[1]}", sample["unit"], rows, note)) + + widths = {h: len(h) for h in headers} + for _, _, rows, _ in blocks: + for row in rows: + for h in headers: + widths[h] = max(widths[h], len(row[h])) + + def _cell(h, v): + return str(v).ljust(widths[h]) if h in left else str(v).rjust(widths[h]) + + def _line(cells): + return " ".join(_cell(h, cells[h]) for h in headers) + + sep = " ".join("-" * widths[h] for h in headers) + + lines = [f"Traces comparison (scaling){scope_title}", args.title, "", f"Reference : {ref_name}", ""] + for i, trace in enumerate(traces): + cpu = trace.get_cpu() + tname = trace.get_name() + smin, smax = sweep[tname] + tag = "(ref) " if i == 0 else " " + lines.append( + f" {tag}{tname:<22} {cpu.get('sockets', 1)}x {cpu['model']} - " + f"{cores[tname]:>3} cores / {cpu['numa_domains']:>2} NUMA scaling {smin}->{smax} workers" + ) + # Name the selected power metric; note it if it varies across traces. + power_metrics = sorted({trace.get_metric_name() for trace in traces}) + power_label = power_metrics[0] if len(power_metrics) == 1 else "the per-trace selected power metric" + + lines.append("") + lines.append(f"Columns ({columns_scope}):") + lines.append(" Trace = trace logical name; '*' marks the reference (first trace).") + lines.append(" Wrk = worker count for this section.") + lines.append(" Perf = benchmark performance (unit shown per section); Δperf = ratio to the reference.") + lines.append(" Perf/core = performance per physical CPU core (physical cores, not SMT threads or workers);") + lines.append(" Δperf/core = ratio to the reference.") + lines.append(f" Power = mean {power_label} power in Watts; Δpower = ratio to the reference.") + lines.append(" Perf/W = performance per watt, as a ratio to the reference (Δperf / Δpower).") + lines.append(" Clock = mean CPU core frequency in MHz; Δclock = ratio to the reference.") + if has_ipc: + lines.append(" IPC = mean instructions per cycle across cores; ΔIPC = ratio to the reference.") + lines.append(" CoV = Coefficient of Variation (std-dev / mean, %) of the per-worker performance:") + lines.append(" how evenly the workers performed (lower = more homogeneous, 0% = identical);") + lines.append(" from the stress-ng per-worker detail, '-' when absent.") + lines.append(" Linearity = deviation from perfect linear scaling, as in the scaling graphs:") + lines.append(" (perf / ideal linear projection from the first step) - 100%.") + lines.append(" 0% = perfectly linear, negative = scaling loss, positive = superlinear.") + lines.append("") + + for title, unit, rows, note in blocks: + lines.append(f"### {title} [{unit}]") + if note: + lines.append(f"(note: {note})") + lines.append(_line({h: h for h in headers})) + lines.append(sep) + lines += [_line(row) for row in rows] + lines.append("") + + # Aggregate: geometric mean of the ratios across every benchmark in scope, + # plus the mean per-worker homogeneity. + # Same column order as the per-benchmark tables (minus the absolute values, + # which don't aggregate): Δperf, Δperf/core, Δpower, Perf/W, Δclock, ΔIPC, CoV, Linearity. + agg_headers = ["Trace", "Δperf", "Δperf/core", "Δpower", "Perf/W", "Δclock"] + if has_ipc: + agg_headers += ["ΔIPC"] + agg_headers += ["CoV", "Linearity"] + agg_widths = {h: len(h) for h in agg_headers} + agg_rows = [] + + def _fmt_ratio(g): + return f"{g:.2f}x" if g else "-" + + for i, trace in enumerate(traces): + tname = trace.get_name() + pr, pcr, wr, cr, ipcr, lins, covs = [], [], [], [], [], [], [] + for key in ordered: + m = metrics[tname].get(key) + ref_m = metrics[ref_name].get(key) + # Skip benchmarks the reference has no value for (reported as N/A above + # and excluded from the aggregate). + if not m or not ref_m: + continue + if ref_m["perf"]: + pr.append(m["perf"] / ref_m["perf"]) + if cores[tname] and cores[ref_name] and ref_m["perf"]: + pcr.append((m["perf"] / cores[tname]) / (ref_m["perf"] / cores[ref_name])) + if m["power"] and ref_m["power"]: + wr.append(m["power"] / ref_m["power"]) + if m["clock"] and ref_m["clock"]: + cr.append(m["clock"] / ref_m["clock"]) + if m["ipc"] and ref_m["ipc"]: + ipcr.append(m["ipc"] / ref_m["ipc"]) + if m["lin_full"] is not None: + lins.append(m["lin_full"]) + if m["cov"] is not None: + covs.append(m["cov"]) + gperf, gpow = _geomean(pr), _geomean(wr) + row = { + "Trace": tname + ("*" if i == 0 else ""), + "Δperf": _fmt_ratio(gperf), + "Δperf/core": _fmt_ratio(_geomean(pcr)), + "Δpower": _fmt_ratio(gpow), + "Δclock": _fmt_ratio(_geomean(cr)), + "Perf/W": f"{gperf / gpow:.2f}x" if gperf and gpow else "-", + "Linearity": f"{np.mean(lins) - 100:+.0f}%" if lins else "-", + "CoV": f"{np.mean(covs):.1f}%" if covs else "-", + } + if has_ipc: + row["ΔIPC"] = _fmt_ratio(_geomean(ipcr)) + agg_rows.append(row) + for row in agg_rows: + for h in agg_headers: + agg_widths[h] = max(agg_widths[h], len(row[h])) + + def _agg_cell(h, v): + return str(v).ljust(agg_widths[h]) if h == "Trace" else str(v).rjust(agg_widths[h]) + + def _agg_line(cells): + return " ".join(_agg_cell(h, cells[h]) for h in agg_headers) + + lines.append(f"### Aggregate (geometric mean of {aggregate_scope} across all benchmarks)") + lines.append(_agg_line({h: h for h in agg_headers})) + lines.append(" ".join("-" * agg_widths[h] for h in agg_headers)) + lines += [_agg_line(row) for row in agg_rows] + + return "\n".join(line.rstrip() for line in lines) + "\n" + + +def _comparison_jobs(ref) -> list: + """Benchmark job names in the reference's order (job, then emp encounter order).""" + jobs = [] # type: list[str] + for name in sorted(ref.bench_list()): + job = ref.bench(name).job_name() + if job not in jobs: + jobs.append(job) + return jobs + + +def _trace_cores_and_sweep(trace) -> tuple: + """(physical core count, (min workers, max workers)) for one trace.""" + allw = [trace.bench(n).workers() for n in trace.bench_list()] + return trace.get_cpu()["physical_cores"], ((min(allw), max(allw)) if allw else (0, 0)) + + +def write_scaling_comparison(args, output_dir) -> int: + """Write max_versus/benchmarks_summary.txt comparing every trace to the first one. + + In scaling mode the first trace on the command line is the reference. For each + benchmark type (engine module / variant) the report lists, at the full-load + (max workers) step: total performance and its ratio to the reference, + performance per physical core and its ratio, CPU package power and clock with + their ratios, and -- when the per-worker detail is available -- the + performance homogeneity (CoV%, lower is more uniform). Each trace also carries + its scaling linearity efficiency (full-load performance vs the ideal linear + projection from the first step). A closing aggregate section gives the + geometric mean of the ratios across all benchmarks. Returns 1 when written. + """ + traces = args.traces + if not traces: + return 0 + jobs = _comparison_jobs(traces[0]) + + # Per trace: {(job, emp): full-load metrics}, physical-core count and sweep. + metrics = {} # type: dict + cores = {} # type: dict + sweep = {} # type: dict + for trace in traces: + tm = {} # type: dict + for job in jobs: + for emp, m in _trace_job_metrics(trace, job).items(): + tm[(job, emp)] = m + metrics[trace.get_name()] = tm + cores[trace.get_name()], sweep[trace.get_name()] = _trace_cores_and_sweep(trace) + + text = _render_traces_comparison( + args, + traces, + metrics, + cores, + sweep, + scope_title="", + columns_scope="all values taken at full load = the max workers each trace scaled to", + aggregate_scope="full-load ratios", + ref_missing_reason="did not run this benchmark", + ) + versus_dir = output_dir.joinpath("max_versus") + versus_dir.mkdir(parents=True, exist_ok=True) + summary_file = versus_dir.joinpath("benchmarks_summary.txt") + summary_file.write_text(text) + print(f"Performance scaling: wrote traces comparison to {summary_file}") + return 1 + + +def write_scaling_step_comparisons(args, output_dir) -> int: + """Write one traces comparison per scaling step under scaling/. + + Same format and columns as max_versus/benchmarks_summary.txt, but instead of a + single report at full load, one file per scaling step (worker count) -- + scaling/summary/benchmarks_summary__workers.txt -- each comparing every + trace to the first (reference) using the values measured at that step. A trace + with no run at a given worker count is reported as N/A in that step's file. + Returns the number of files written (0 with fewer than two traces). + """ + traces = args.traces + if len(traces) < 2: + return 0 + jobs = _comparison_jobs(traces[0]) + + # Per trace: {(job, emp): {workers: metrics}}, physical-core count and sweep, + # plus the union of every worker count seen across traces/benchmarks. + step_metrics = {} # type: dict + cores = {} # type: dict + sweep = {} # type: dict + all_steps = set() # type: set + for trace in traces: + tm = {} # type: dict + for job in jobs: + for emp, per_step in _trace_job_step_metrics(trace, job).items(): + tm[(job, emp)] = per_step + all_steps.update(per_step) + step_metrics[trace.get_name()] = tm + cores[trace.get_name()], sweep[trace.get_name()] = _trace_cores_and_sweep(trace) + + if not all_steps: + return 0 + + summary_dir = output_dir.joinpath("scaling", "summary") + summary_dir.mkdir(parents=True, exist_ok=True) + steps = sorted(all_steps) + # Zero-pad the worker count in the filename so the files list in sweep order. + width = len(str(steps[-1])) + written = 0 + for n in steps: + # This step's metrics: {trace name: {(job, emp): metrics}}, keeping only the + # benchmarks each trace actually ran at n workers. + metrics = { + tname: {key: per_step[n] for key, per_step in tm.items() if n in per_step} + for tname, tm in step_metrics.items() + } + text = _render_traces_comparison( + args, + traces, + metrics, + cores, + sweep, + scope_title=f" - {n} workers", + columns_scope=f"all values taken at this scaling step = {n} workers", + aggregate_scope=f"ratios at {n} workers", + ref_missing_reason=f"has no result at {n} workers", + ) + summary_file = summary_dir.joinpath(f"benchmarks_summary_{n:0{width}d}_workers.txt") + summary_file.write_text(text) + written += 1 + print(f"Performance scaling: wrote {written} per-step traces comparison(s) to {summary_dir}") + return written + + +def _aggregate_metrics(traces) -> tuple[list, str, dict]: + """Per-trace aggregate metrics vs the first trace (the reference). + + Returns (ordered trace names, reference name, {name: metrics}) where metrics + holds the geometric-mean ratios to the reference across all comparable + benchmarks (dperf, dcore, dpow, dipc, dclk, ppw) plus the mean full-load + linearity (lin, deviation %) and per-worker homogeneity (cov, %). Mirrors the + aggregate section of write_scaling_comparison. + """ + ref = traces[0] + ref_name = ref.get_name() + jobs = [] # type: list[str] + for name in sorted(ref.bench_list()): + job = ref.bench(name).job_name() + if job not in jobs: + jobs.append(job) + + metrics = {} # type: dict + cores = {} # type: dict + for trace in traces: + tm = {} # type: dict + for job in jobs: + for emp, m in _trace_job_metrics(trace, job).items(): + tm[(job, emp)] = m + metrics[trace.get_name()] = tm + cores[trace.get_name()] = trace.get_physical_cores() + + out = {} # type: dict + for trace in traces: + n = trace.get_name() + pr, pcr, wr, ipc, clk, lins, covs = [], [], [], [], [], [], [] + for key, m in metrics[n].items(): + rm = metrics[ref_name].get(key) + if not m or not rm: + continue + if rm["perf"]: + pr.append(m["perf"] / rm["perf"]) + if cores[n] and cores[ref_name] and rm["perf"]: + pcr.append((m["perf"] / cores[n]) / (rm["perf"] / cores[ref_name])) + if m["power"] and rm["power"]: + wr.append(m["power"] / rm["power"]) + if m["ipc"] and rm["ipc"]: + ipc.append(m["ipc"] / rm["ipc"]) + if m["clock"] and rm["clock"]: + clk.append(m["clock"] / rm["clock"]) + if m["lin_full"] is not None: + lins.append(m["lin_full"]) + if m["cov"] is not None: + covs.append(m["cov"]) + gperf, gpow = _geomean(pr), _geomean(wr) + out[n] = { + "dperf": gperf, + "dcore": _geomean(pcr), + "dpow": gpow, + "dipc": _geomean(ipc), + "dclk": _geomean(clk), + "ppw": (gperf / gpow) if gperf and gpow else None, + # Deviation from perfect linear scaling (0% = linear), like the text + # report: lin_full is the ratio-percent, so subtract 100. + "lin": float(np.mean(lins)) - 100 if lins else None, + "cov": float(np.mean(covs)) if covs else None, + } + return [trace.get_name() for trace in traces], ref_name, out + + +def _traces_caption(traces) -> str: + """Full per-trace system description block, rendered below the exec graphs. + + System / Bios / Kernel / Processor each on their own line. + """ + parts = [] + for i, trace in enumerate(traces): + tag = trace.get_name() + (" (ref)" if i == 0 else "") + dmi = trace.get_dmi() + cpu = trace.get_cpu() + kernel = trace.get_kernel() + parts.append( + f"{tag}:\n" + f" System : {dmi['serial']} {dmi['product']}\n" + f" Bios : v{dmi['bios']['version']}\n" + f" Kernel : {kernel['release']}\n" + f" Processor: {cpu.get('sockets', 1)}x {cpu['model']}\n" + f" {cpu['physical_cores']} physical cores, {cpu['numa_domains']} NUMA domains" + ) + return "\n\n".join(parts) + + +# Column meanings, rendered below the scorecard next to the system description. +_METRICS_CAPTION = ( + "Metrics (geometric mean across benchmarks, at full load, vs reference):\n" + " Δperf : total throughput ratio\n" + " Δperf/core : throughput per physical core ratio\n" + " ΔIPC : instructions-per-cycle ratio\n" + " Perf/W : performance-per-watt ratio (Δperf / Δpower)\n" + " Δpower : CPU package power ratio (lower is better)\n" + " Δclock : mean core-frequency ratio\n" + " Linearity : deviation from perfect linear scaling (0% = linear)\n" + " CoV : per-worker performance spread (lower = more homogeneous)\n" + "\n" + "Color: green = better than the reference, red = worse, neutral = same." +) + + +def render_versus_scorecard(args, output_dir) -> int: + """Render an executive-summary scorecard heatmap of the aggregate. + + One row per trace, one column per aggregate metric, each cell annotated with + the real value and colored vs the reference (green = better than the + reference, red = worse, ~neutral = same). Direction is folded per metric so + green always means "better": power and CoV are inverted (lower is greener), + and linearity uses its signed deviation. Written to + max_versus/performance_summary_scorecard. + """ + traces = args.traces + if len(traces) < 2: + return 0 + names, ref_name, agg = _aggregate_metrics(traces) + ref = agg[ref_name] + + def rx(v): + return f"{v:.2f}x" if v is not None else "n/a" + + def pc(v): + return f"{v:+.0f}%" if v is not None else "n/a" + + def cx(v): + return f"{v:.1f}%" if v is not None else "n/a" + + # (label, value key, display fn, goodness-vs-reference fn -> signed float or + # None). Positive goodness = better than the reference. None fn = neutral. + cols = [ + ("Δperf", "dperf", rx, lambda a: float(np.log2(a["dperf"])) if a["dperf"] else None), + ("Δperf/core", "dcore", rx, lambda a: float(np.log2(a["dcore"])) if a["dcore"] else None), + ("ΔIPC", "dipc", rx, lambda a: float(np.log2(a["dipc"])) if a["dipc"] else None), + ("Perf/W", "ppw", rx, lambda a: float(np.log2(a["ppw"])) if a["ppw"] else None), + ("Δpower", "dpow", rx, lambda a: -float(np.log2(a["dpow"])) if a["dpow"] else None), + ("Δclock", "dclk", rx, lambda a: float(np.log2(a["dclk"])) if a["dclk"] else None), + ( + "Linearity", + "lin", + pc, + lambda a: (a["lin"] - ref["lin"]) if (a["lin"] is not None and ref["lin"] is not None) else None, + ), + ( + "CoV", + "cov", + cx, + lambda a: (ref["cov"] - a["cov"]) if (a["cov"] is not None and ref["cov"] is not None) else None, + ), + ] + + # Per column: normalize the signed goodness to [-1, 1] (0 = reference) so a + # single diverging colormap works across metrics with different units. + grid = np.full((len(names), len(cols)), np.nan) + for j, (_, _, _, gfn) in enumerate(cols): + if gfn is None: + continue + vals = np.array([gfn(agg[n]) if gfn(agg[n]) is not None else np.nan for n in names]) + scale = np.nanmax(np.abs(vals)) if np.any(np.isfinite(vals)) else 0.0 + if scale > 0: + grid[:, j] = vals / scale + + fig, ax = plt.subplots(figsize=(1.15 * len(cols) + 2.5, 0.62 * len(names) + 1.8)) + cmap = plt.get_cmap("RdYlGn").copy() + cmap.set_bad("0.9") # neutral / missing cells + ax.imshow(np.ma.masked_invalid(grid), cmap=cmap, vmin=-1, vmax=1, aspect="auto") + ax.set_xticks(range(len(cols))) + ax.set_xticklabels([c for c, _, _, _ in cols]) + ax.set_yticks(range(len(names))) + ax.set_yticklabels([n + (" *" if n == ref_name else "") for n in names]) + for i, n in enumerate(names): + for j, (_, key, dfn, _) in enumerate(cols): + ax.text(j, i, dfn(agg[n][key]), ha="center", va="center", fontsize=9) + + title = f"{args.title}\n\nPerformance executive summary (vs {ref_name})\n" + title += "green = better than reference, red = worse, neutral = same; * = reference" + ax.set_title(title, fontsize=10) + # Two side-by-side blocks below the graph: system descriptions on the left + # half, the meaning of each metric column on the right half. + sys_caption = ax.text( + 0.0, + -0.18, + _traces_caption(traces), + transform=ax.transAxes, + va="top", + ha="left", + fontsize=7, + family="monospace", + ) + metrics_caption = ax.text( + 0.5, + -0.18, + _METRICS_CAPTION, + transform=ax.transAxes, + va="top", + ha="left", + fontsize=7, + family="monospace", + ) + + outdir = output_dir.joinpath("max_versus") + outdir.mkdir(parents=True, exist_ok=True) + summary_file = outdir.joinpath(f"performance_summary_scorecard.{args.format}") + fig.savefig( + str(summary_file), + format=args.format, + dpi=args.dpi, + bbox_inches="tight", + pad_inches=0.3, + bbox_extra_artists=[sys_caption, metrics_caption], + ) + fig.clear() + plt.close(fig) + print(f"Max versus: wrote executive summary scorecard to {summary_file}") + return 1 diff --git a/hwbench/bench/benchmark.py b/hwbench/bench/benchmark.py index 03b8b479..ab19e961 100644 --- a/hwbench/bench/benchmark.py +++ b/hwbench/bench/benchmark.py @@ -80,6 +80,16 @@ def __init__(self, engine_module: EngineModuleBase, parameters: BenchmarkParamet self.engine_module = engine_module self.skip = False + @property + def output_basename(self) -> str: + # Prefix the output files with the per-benchmark id used as the + # results.json key (get_name_with_position()). A single job expands via + # the scaling matrix (hosting_cpu_cores_scaling / stressor_range) into + # many runs that share the same engine `name`; without this prefix they + # all write into the same out_dir under the same filenames and only the + # last iteration survives. + return f"{self.parameters.get_name_with_position()}_{self.name}" + def get_taskset(self, args): # Let's pin the CPU if needed if self.parameters.get_pinned_cpu(): diff --git a/hwbench/bench/benchmarks.py b/hwbench/bench/benchmarks.py index 4f66ffc4..0bdeacfc 100644 --- a/hwbench/bench/benchmarks.py +++ b/hwbench/bench/benchmarks.py @@ -16,9 +16,10 @@ class Benchmarks: """A class to list and execute benchmarks to run.""" - def __init__(self, out_dir, jobs_config) -> None: + def __init__(self, out_dir, jobs_config, verbose: bool = False) -> None: self.jobs_config = jobs_config self.out_dir = out_dir + self.verbose = verbose self.benchs: list[Benchmark] = [] self.monitoring: Monitoring = None # type: ignore[assignment] self.hardware: BaseHardware | None = None @@ -175,7 +176,7 @@ def __schedule_benchmark(self, job, pinned_cpu, engine_module_parameter, validat for pdu in self.get_hardware().vendor.get_pdus(): pdu.connect_redfish() pdu.detect() - self.monitoring = Monitoring(self.out_dir, self.jobs_config, self.get_hardware()) + self.monitoring = Monitoring(self.out_dir, self.jobs_config, self.get_hardware(), verbose=self.verbose) # For each stressor, add a benchmark object to the list for stressor_count in self.jobs_config.get_stressor_range(job): diff --git a/hwbench/bench/monitoring.py b/hwbench/bench/monitoring.py index c0d4d846..93ce6f98 100644 --- a/hwbench/bench/monitoring.py +++ b/hwbench/bench/monitoring.py @@ -35,9 +35,10 @@ def join(self): class Monitoring: """A class to perform monitoring.""" - def __init__(self, out_dir, config, hardware: BaseHardware): + def __init__(self, out_dir, config, hardware: BaseHardware, verbose: bool = False): self.config = config self.out_dir = out_dir + self.verbose = verbose self.hardware = hardware self.vendor = hardware.get_vendor() self.metrics = MonitoringData() @@ -114,7 +115,8 @@ def preup(self, precision_s: int): if self.turbostat: # Reinitialize turbostat metrics after reset (fast, doesn't run turbostat) self.turbostat.reinitialize_metrics() - print("Monitoring/turbostat: starting background monitoring with EOL-triggered sampling") + if self.verbose: + print("Monitoring/turbostat: starting background monitoring with EOL-triggered sampling") self.turbostat.start_background() def predown(self): @@ -124,7 +126,8 @@ def predown(self): the background turbostat process. """ if self.turbostat: - print("Monitoring/turbostat: stopping background monitoring") + if self.verbose: + print("Monitoring/turbostat: stopping background monitoring") self.turbostat.stop_background() def __monitor_bmc(self): diff --git a/hwbench/bench/parameters.py b/hwbench/bench/parameters.py index 3213bc98..79f68dbe 100644 --- a/hwbench/bench/parameters.py +++ b/hwbench/bench/parameters.py @@ -1,9 +1,15 @@ +from __future__ import annotations + import pathlib +from typing import TYPE_CHECKING from hwbench.environment.hardware import BaseHardware from .monitoring import Monitoring +if TYPE_CHECKING: + from .benchmark import Benchmark + class BenchmarkParameters: """A class to host parameters attached to a benchmark.""" @@ -38,6 +44,9 @@ def __init__( self.skip_method = skip_method self.sync_start = sync_start self.custom_parameters: dict[str, str] = kwargs + # Set once the owning Benchmark is built (see set_benchmark); until then + # get_name_with_position() falls back to the bare job name. + self.benchmark: Benchmark | None = None def get_benchmark(self): return self.benchmark diff --git a/hwbench/bench/test_numa.py b/hwbench/bench/test_numa.py index da49f556..d3241220 100644 --- a/hwbench/bench/test_numa.py +++ b/hwbench/bench/test_numa.py @@ -57,6 +57,23 @@ def test_numa_simple(self): for index, numa_node in enumerate(cumulative_numa_nodes): assert self.get_bench_parameters(9 + index).get_pinned_cpu() == numa_node + def test_numa_dump(self): + """The CPU dump exposes the NUMA topology: node->cores and distance matrix.""" + dump = self.hw.get_cpu().dump() + # NUMA node -> logical cores mapping + assert dump["numa_domains"] == 8 + assert set(dump["numa_nodes"].keys()) == set(range(8)) + assert dump["numa_nodes"][0] == self.NUMA0 + assert dump["numa_nodes"][1] == self.NUMA1 + assert dump["numa_nodes"][7] == self.NUMA7 + # NUMA distance matrix: 8x8, local node is 10, node 0<->1 is 11, rest 12 + distances = dump["numa_distances"] + assert len(distances) == 8 + assert distances[0] == [10, 11, 12, 12, 12, 12, 12, 12] + for domain in range(8): + assert len(distances[domain]) == 8 + assert distances[domain][domain] == 10 + def test_numa(self): """Check numa syntax""" assert self.hw.logical_core_count() == 128 diff --git a/hwbench/engines/stressng.py b/hwbench/engines/stressng.py index c006d02f..56158860 100644 --- a/hwbench/engines/stressng.py +++ b/hwbench/engines/stressng.py @@ -1,7 +1,10 @@ from __future__ import annotations +import pathlib import re +import yaml + from hwbench.bench.benchmark import ExternalBench from hwbench.bench.engine import EngineBase, EngineModuleBase from hwbench.bench.parameters import BenchmarkParameters @@ -94,6 +97,8 @@ def run_cmd(self) -> list[str]: "--timeout", str(self.parameters.get_runtime()), "--metrics", + "--yaml", + f"{self.output_basename}.yaml", ] return self.get_taskset(args) @@ -128,16 +133,62 @@ def stats_parse(self) -> re.Pattern: r"\s+(?P[0-9\.]+)" ) + def yaml_output_file(self) -> pathlib.Path: + """Path of the YAML metrics file emitted by stress-ng (see run_cmd).""" + return self.out_dir / f"{self.output_basename}.yaml" + + def parse_yaml_per_instance(self) -> dict: + """Extract the per-instance bogo-ops from the stress-ng YAML output. + + stress-ng reports, under each stressor of the "metrics:" section, an + "instances:" list holding a per-second bogo-ops rate per instance: + + metrics: + - stressor: cpu + bogo-ops: 3257280 + ... + instances: + - instance: 0 + bogo-ops: 5120 + bogo-ops-per-second-real-time: 341.098366 + ... + + We keep the per-instance metric so it can be inspected/plotted later. + The YAML file is optional (older stress-ng, or a skipped run); when it + is missing we simply return an empty dict and change nothing. + """ + yaml_file = self.yaml_output_file() + if not yaml_file.exists(): + return {} + + with yaml_file.open() as f: + data = yaml.safe_load(f) + + bogo_ops: list[float] = [] + for stressor in (data or {}).get("metrics", []): + for instance in stressor.get("instances", []): + if "bogo-ops-per-second-real-time" in instance: + bogo_ops.append(float(instance["bogo-ops-per-second-real-time"])) + + if not bogo_ops: + return {} + + return {"detail": {"bogo op/s": bogo_ops}} + def parse_cmd(self, stdout: bytes, stderr: bytes): """Generic stress-ng output parsing to extract performance metrics.""" for line in (stdout or stderr).splitlines(): stats = self.stats_parse().search(str(line)) if stats: s = stats.groupdict() - return self.parameters.get_result_format() | { - "bogo ops/s": float(s["bogo_ops_sec"]), - "effective_runtime": float(s["real_time"]), - } + return ( + self.parameters.get_result_format() + | { + "bogo ops/s": float(s["bogo_ops_sec"]), + "effective_runtime": float(s["real_time"]), + } + | self.parse_yaml_per_instance() + ) h.fatal("Unable to detect stress-ng reporting metrics") diff --git a/hwbench/engines/stressng_stream.py b/hwbench/engines/stressng_stream.py index 0da274fd..e211fbef 100644 --- a/hwbench/engines/stressng_stream.py +++ b/hwbench/engines/stressng_stream.py @@ -22,6 +22,8 @@ def run_cmd(self) -> list[str]: "--timeout", str(self.parameters.get_runtime()), "--metrics", + "--yaml", + f"{self.name}.yaml", "--stream", str(self.parameters.get_engine_instances_count()), ] diff --git a/hwbench/engines/test_parse_stressng.py b/hwbench/engines/test_parse_stressng.py index 730c6d2b..43b4570b 100644 --- a/hwbench/engines/test_parse_stressng.py +++ b/hwbench/engines/test_parse_stressng.py @@ -9,6 +9,7 @@ from hwbench.environment.mock import MockHardware from .stressng import Engine as StressNG +from .stressng_cpu import EngineModuleCpu, StressNGCPU from .stressng_memrate import EngineModuleMemrate, StressNGMemrate from .stressng_qsort import EngineModuleQsort, StressNGQsort from .stressng_stream import EngineModuleStream, StressNGStream @@ -90,6 +91,81 @@ def test_module_parsing_output(self): output.pop(key, None) assert output == json.loads((d / "output").read_bytes()) + def test_yaml_per_instance_parsing(self): + """When stress-ng emits a YAML file, the per-instance bogo-ops are kept.""" + # The YAML file is named "_.yaml" in out_dir (the + # job prefix keeps each scaling iteration's files distinct), so we point + # out_dir at the fixture directory and use the matching job/stressor names. + d = pathlib.Path("./hwbench/tests/parsing/stressng/v02103b") + engine = mock_engine("v17") + params = BenchmarkParameters( + d, + "cpu10", + 0, + "", + 5, + "matrixprod", + "", + MockHardware(), + "none", + None, + "bypass", + "none", + ) + module = EngineModuleCpu(engine, "cpu") + test_target = StressNGCPU(module, params) + + assert test_target.name == "stressngmatrixprod" + assert test_target.yaml_output_file() == d / "cpu10_stressngmatrixprod.yaml" + + stdout = (d / "stdout").read_bytes() + stderr = (d / "stderr").read_bytes() + output = test_target.parse_cmd(stdout, stderr) + + # The generic metrics are still parsed from stdout. + assert output["bogo ops/s"] == 217929.51 + assert output["effective_runtime"] == 14.95 + + # The per-instance bogo-ops/s (real time) are extracted from the YAML file. + bogo_ops = output["detail"]["bogo op/s"] + assert len(bogo_ops) == 640 + assert bogo_ops[:6] == [ + 341.098366, + 339.750707, + 341.169751, + 340.387195, + 340.689242, + 340.715147, + ] + # The per-instance rates sum up to roughly the aggregated bogo ops/s. + assert sum(bogo_ops) == pytest.approx(217929.51, abs=2) + + def test_yaml_absent_keeps_result(self): + """Without a YAML file (e.g. older stress-ng), the result is unchanged.""" + d = pathlib.Path("./hwbench/tests/parsing/stressng/v17") + engine = mock_engine("v17") + params = BenchmarkParameters( + d, + "stressng", + 0, + "", + 5, + "", + "", + MockHardware(), + "none", + None, + "bypass", + "none", + ) + module = EngineModuleCpu(engine, "cpu") + test_target = StressNGCPU(module, params) + + assert not test_target.yaml_output_file().exists() + + output = test_target.parse_cmd((d / "stdout").read_bytes(), (d / "stderr").read_bytes()) + assert "detail" not in output + def test_stressng_methods(self): test_dir = pathlib.Path("./hwbench/tests/parsing/stressngmethods") for d in test_dir.iterdir(): diff --git a/hwbench/environment/cpu.py b/hwbench/environment/cpu.py index 18d43ee7..797f8c6d 100644 --- a/hwbench/environment/cpu.py +++ b/hwbench/environment/cpu.py @@ -74,6 +74,10 @@ def get_logical_cores_in_numa_domain(self, numa_domain) -> list[int]: """Return logical cores in a numa domain.""" return self.numa.get_cores(numa_domain) + def get_numa_distances(self) -> dict[int, list[int]]: + """Return the NUMA distance matrix ({source node: [latency to each node]}).""" + return self.numa.get_distances() + def get_quadrants_count(self) -> int: """Return the number of quadrants.""" return self.numa.quadrants_count() @@ -93,6 +97,13 @@ def dump(self): "logical_cores": self.get_logical_cores_count(), "physical_cores": self.get_physical_cores_count(), "numa_domains": self.get_numa_domains_count(), + # Mapping of each NUMA domain to its logical cores, so consumers + # (e.g. hwgraph) can aggregate per-core metrics by NUMA domain. + "numa_nodes": { + node: self.get_logical_cores_in_numa_domain(node) for node in range(self.get_numa_domains_count()) + }, + # NUMA distance matrix between domains, for topology-aware features. + "numa_distances": self.get_numa_distances(), "sockets": self.get_sockets_count(), } diff --git a/hwbench/environment/numa.py b/hwbench/environment/numa.py index d3ee3d5f..957a49df 100644 --- a/hwbench/environment/numa.py +++ b/hwbench/environment/numa.py @@ -40,6 +40,9 @@ def parse_cmd(self, stdout: bytes, _stderr: bytes): numa_distance = re.findall(r"(\d+): (.*)", line) if numa_distance: for source, latencies in numa_distance: + # Keep the full distance matrix (source node -> latency to each + # node); useful to reason about node locality/topology. + self.distances[int(source)] = [int(latency) for latency in latencies.split()] quadrant = self.__is_numa_node_in_quadrant(source) numa_dest = -1 for latency in latencies.split(): @@ -65,6 +68,7 @@ def __init__(self, out_dir: pathlib.Path): super().__init__(out_dir) self.numa_domains: dict[int, list[int]] = {} self.quadrants: list[list[int]] = [] + self.distances: dict[int, list[int]] = {} def count(self) -> int: return len(self.numa_domains) @@ -72,6 +76,10 @@ def count(self) -> int: def get_cores(self, numa_domain) -> list[int]: return self.numa_domains.get(numa_domain, []) + def get_distances(self) -> dict[int, list[int]]: + """Return the NUMA distance matrix: {source node: [latency to each node]}.""" + return self.distances + def quadrants_count(self) -> int: return len(self.quadrants) diff --git a/hwbench/environment/test_parse.py b/hwbench/environment/test_parse.py index 49731158..9450ffb9 100644 --- a/hwbench/environment/test_parse.py +++ b/hwbench/environment/test_parse.py @@ -141,6 +141,13 @@ def test_parsing_numa_8_domains_with_llc(self): assert test_target.count() == 8 for domain in range(0, test_target.count()): assert len(test_target.get_cores(domain)) == 16 + # Full 8x8 distance matrix: local node is 10, node 0<->1 is 11, rest 12 + distances = test_target.get_distances() + assert len(distances) == 8 + assert distances[0] == [10, 11, 12, 12, 12, 12, 12, 12] + for domain in range(0, test_target.count()): + assert len(distances[domain]) == 8 + assert distances[domain][domain] == 10 def test_parsing_numa_20_domains_with_llc(self): d = pathlib.Path("./hwbench/tests/parsing/numa/20domainsllc") @@ -160,6 +167,12 @@ def test_parsing_numa_20_domains_with_llc(self): assert test_target.get_numa_nodes_in_quadrant(1) == [5, 6, 7, 8, 9] assert test_target.get_numa_nodes_in_quadrant(2) == [10, 11, 12, 13, 14] assert test_target.get_numa_nodes_in_quadrant(3) == [15, 16, 17, 18, 19] + # Full 20x20 distance matrix with a local distance of 10 on the diagonal + distances = test_target.get_distances() + assert len(distances) == 20 + for domain in range(0, test_target.count()): + assert len(distances[domain]) == 20 + assert distances[domain][domain] == 10 class TestParseIpmitool: diff --git a/hwbench/hwbench.py b/hwbench/hwbench.py index 8bfda3eb..96db455d 100755 --- a/hwbench/hwbench.py +++ b/hwbench/hwbench.py @@ -37,7 +37,7 @@ def main(): init_logging(tuning_out_dir / "hwbench-tuning.log") hwbench_config = config.Config(args.jobs_config) - benches = benchmarks.Benchmarks(out_dir, hwbench_config) + benches = benchmarks.Benchmarks(out_dir, hwbench_config, verbose=args.verbose) problems = env_hw.check_requirements() + benches.check_requirements() @@ -109,6 +109,13 @@ def parse_options(): default=True, help="Enable or disable tuning: this is useful when you want to test the system as-is.", ) + parser.add_argument( + "-v", + "--verbose", + action="store_true", + default=False, + help="Enable verbose output", + ) return parser.parse_args() diff --git a/hwbench/tests/parsing/stressng/v02103b/cpu10_stressngmatrixprod.yaml b/hwbench/tests/parsing/stressng/v02103b/cpu10_stressngmatrixprod.yaml new file mode 100644 index 00000000..9c2ae845 --- /dev/null +++ b/hwbench/tests/parsing/stressng/v02103b/cpu10_stressngmatrixprod.yaml @@ -0,0 +1,2605 @@ +--- +build-info: + compiler: 'gcc 14.3.1' + stdc-version: '199901L' + stdc-hosted: '1' + +system-info: + stress-ng-version: '0.21.03' + run-by: 'root' + date-yyyy-mm-dd: '2026:07:12' + time-hh-mm-ss: '10:54:21' + epoch-secs: 1783853661 + hostname: 'hwtest-linux88-fr3' + sysname: 'Linux' + nodename: 'hwtest-linux88-fr3' + release: '7.2.0-rc2' + version: '#1 SMP PREEMPT Mon Jul 6 12:44:38 UTC 2026' + machine: 'x86_64' + compiler: 'gcc 14.3.1' + libc: 'glibc 2.39' + uptime: 155613 + totalram: 2434403393536 + freeram: 2423057178624 + sharedram: 6860800 + bufferram: 0 + totalswap: 0 + freeswap: 0 + pagesize: 4096 + cpus: 640 + cpus-online: 640 + ticks-per-second: 100 + +metrics: + - stressor: cpu + bogo-ops: 3257280 + bogo-ops-per-second-usr-sys-time: 341.256408 + bogo-ops-per-second-real-time: 217929.506101 + wall-clock-time: 14.946485 + user-time: 9544.586766 + system-time: 0.376845 + cpu-usage-per-instance: 99.782699 + max-rss: 0 + instances: + - instance: 0 + bogo-ops: 5120 + bogo-ops-per-second-usr-sys-time: 341.149045 + bogo-ops-per-second-real-time: 341.098366 + - instance: 1 + bogo-ops: 5097 + bogo-ops-per-second-usr-sys-time: 339.761131 + bogo-ops-per-second-real-time: 339.750707 + - instance: 2 + bogo-ops: 5120 + bogo-ops-per-second-usr-sys-time: 341.173619 + bogo-ops-per-second-real-time: 341.169751 + - instance: 3 + bogo-ops: 5108 + bogo-ops-per-second-usr-sys-time: 340.385651 + bogo-ops-per-second-real-time: 340.387195 + - instance: 4 + bogo-ops: 5114 + bogo-ops-per-second-usr-sys-time: 340.687652 + bogo-ops-per-second-real-time: 340.689242 + - instance: 5 + bogo-ops: 5114 + bogo-ops-per-second-usr-sys-time: 340.713800 + bogo-ops-per-second-real-time: 340.715147 + - instance: 6 + bogo-ops: 5114 + bogo-ops-per-second-usr-sys-time: 340.694234 + bogo-ops-per-second-real-time: 340.695719 + - instance: 7 + bogo-ops: 5103 + bogo-ops-per-second-usr-sys-time: 340.164464 + bogo-ops-per-second-real-time: 340.163369 + - instance: 8 + bogo-ops: 5108 + bogo-ops-per-second-usr-sys-time: 340.558262 + bogo-ops-per-second-real-time: 340.557010 + - instance: 9 + bogo-ops: 5131 + bogo-ops-per-second-usr-sys-time: 341.936822 + bogo-ops-per-second-real-time: 341.923005 + - instance: 10 + bogo-ops: 5097 + bogo-ops-per-second-usr-sys-time: 339.725193 + bogo-ops-per-second-real-time: 339.716252 + - instance: 11 + bogo-ops: 5125 + bogo-ops-per-second-usr-sys-time: 341.721821 + bogo-ops-per-second-real-time: 341.685811 + - instance: 12 + bogo-ops: 5108 + bogo-ops-per-second-usr-sys-time: 340.417478 + bogo-ops-per-second-real-time: 340.364976 + - instance: 13 + bogo-ops: 5108 + bogo-ops-per-second-usr-sys-time: 340.464469 + bogo-ops-per-second-real-time: 340.451736 + - instance: 14 + bogo-ops: 5137 + bogo-ops-per-second-usr-sys-time: 342.537092 + bogo-ops-per-second-real-time: 342.513170 + - instance: 15 + bogo-ops: 5148 + bogo-ops-per-second-usr-sys-time: 342.964315 + bogo-ops-per-second-real-time: 342.963971 + - instance: 16 + bogo-ops: 5148 + bogo-ops-per-second-usr-sys-time: 343.011549 + bogo-ops-per-second-real-time: 342.994905 + - instance: 17 + bogo-ops: 5108 + bogo-ops-per-second-usr-sys-time: 340.458251 + bogo-ops-per-second-real-time: 340.452526 + - instance: 18 + bogo-ops: 5108 + bogo-ops-per-second-usr-sys-time: 340.494018 + bogo-ops-per-second-real-time: 340.484383 + - instance: 19 + bogo-ops: 5148 + bogo-ops-per-second-usr-sys-time: 343.089319 + bogo-ops-per-second-real-time: 343.082152 + - instance: 20 + bogo-ops: 5148 + bogo-ops-per-second-usr-sys-time: 343.269730 + bogo-ops-per-second-real-time: 343.263874 + - instance: 21 + bogo-ops: 5137 + bogo-ops-per-second-usr-sys-time: 342.486554 + bogo-ops-per-second-real-time: 342.491033 + - instance: 22 + bogo-ops: 5108 + bogo-ops-per-second-usr-sys-time: 340.555742 + bogo-ops-per-second-real-time: 340.559760 + - instance: 23 + bogo-ops: 5148 + bogo-ops-per-second-usr-sys-time: 343.095653 + bogo-ops-per-second-real-time: 343.096435 + - instance: 24 + bogo-ops: 5148 + bogo-ops-per-second-usr-sys-time: 343.041012 + bogo-ops-per-second-real-time: 343.034809 + - instance: 25 + bogo-ops: 5114 + bogo-ops-per-second-usr-sys-time: 340.977047 + bogo-ops-per-second-real-time: 340.970669 + - instance: 26 + bogo-ops: 5148 + bogo-ops-per-second-usr-sys-time: 343.002271 + bogo-ops-per-second-real-time: 343.000266 + - instance: 27 + bogo-ops: 5120 + bogo-ops-per-second-usr-sys-time: 341.271017 + bogo-ops-per-second-real-time: 341.275710 + - instance: 28 + bogo-ops: 5148 + bogo-ops-per-second-usr-sys-time: 343.185129 + bogo-ops-per-second-real-time: 343.183581 + - instance: 29 + bogo-ops: 5103 + bogo-ops-per-second-usr-sys-time: 340.119097 + bogo-ops-per-second-real-time: 340.123709 + - instance: 30 + bogo-ops: 5108 + bogo-ops-per-second-usr-sys-time: 340.401484 + bogo-ops-per-second-real-time: 340.397909 + - instance: 31 + bogo-ops: 5125 + bogo-ops-per-second-usr-sys-time: 341.528530 + bogo-ops-per-second-real-time: 341.526011 + - instance: 32 + bogo-ops: 5137 + bogo-ops-per-second-usr-sys-time: 342.389903 + bogo-ops-per-second-real-time: 342.386575 + - instance: 33 + bogo-ops: 5103 + bogo-ops-per-second-usr-sys-time: 340.238606 + bogo-ops-per-second-real-time: 340.234886 + - instance: 34 + bogo-ops: 5120 + bogo-ops-per-second-usr-sys-time: 341.175756 + bogo-ops-per-second-real-time: 341.162591 + - instance: 35 + bogo-ops: 5137 + bogo-ops-per-second-usr-sys-time: 342.336556 + bogo-ops-per-second-real-time: 342.331926 + - instance: 36 + bogo-ops: 5148 + bogo-ops-per-second-usr-sys-time: 343.020715 + bogo-ops-per-second-real-time: 343.006265 + - instance: 37 + bogo-ops: 5097 + bogo-ops-per-second-usr-sys-time: 339.834731 + bogo-ops-per-second-real-time: 339.832776 + - instance: 38 + bogo-ops: 5103 + bogo-ops-per-second-usr-sys-time: 340.241396 + bogo-ops-per-second-real-time: 340.241555 + - instance: 39 + bogo-ops: 5125 + bogo-ops-per-second-usr-sys-time: 341.436925 + bogo-ops-per-second-real-time: 341.436427 + - instance: 40 + bogo-ops: 5131 + bogo-ops-per-second-usr-sys-time: 341.997036 + bogo-ops-per-second-real-time: 341.995299 + - instance: 41 + bogo-ops: 5097 + bogo-ops-per-second-usr-sys-time: 339.892496 + bogo-ops-per-second-real-time: 339.891539 + - instance: 42 + bogo-ops: 5108 + bogo-ops-per-second-usr-sys-time: 340.481989 + bogo-ops-per-second-real-time: 340.476846 + - instance: 43 + bogo-ops: 5125 + bogo-ops-per-second-usr-sys-time: 341.580884 + bogo-ops-per-second-real-time: 341.585297 + - instance: 44 + bogo-ops: 5142 + bogo-ops-per-second-usr-sys-time: 342.732527 + bogo-ops-per-second-real-time: 342.645959 + - instance: 45 + bogo-ops: 3240 + bogo-ops-per-second-usr-sys-time: 339.813493 + bogo-ops-per-second-real-time: 215.903346 + - instance: 46 + bogo-ops: 5103 + bogo-ops-per-second-usr-sys-time: 340.163784 + bogo-ops-per-second-real-time: 340.159969 + - instance: 47 + bogo-ops: 5103 + bogo-ops-per-second-usr-sys-time: 340.155848 + bogo-ops-per-second-real-time: 340.156758 + - instance: 48 + bogo-ops: 5125 + bogo-ops-per-second-usr-sys-time: 341.775351 + bogo-ops-per-second-real-time: 341.772494 + - instance: 49 + bogo-ops: 5125 + bogo-ops-per-second-usr-sys-time: 341.818480 + bogo-ops-per-second-real-time: 341.819108 + - instance: 50 + bogo-ops: 4613 + bogo-ops-per-second-usr-sys-time: 340.237331 + bogo-ops-per-second-real-time: 307.627360 + - instance: 51 + bogo-ops: 5137 + bogo-ops-per-second-usr-sys-time: 342.497925 + bogo-ops-per-second-real-time: 342.498317 + - instance: 52 + bogo-ops: 5125 + bogo-ops-per-second-usr-sys-time: 341.847504 + bogo-ops-per-second-real-time: 341.846434 + - instance: 53 + bogo-ops: 5131 + bogo-ops-per-second-usr-sys-time: 342.036887 + bogo-ops-per-second-real-time: 342.040250 + - instance: 54 + bogo-ops: 5125 + bogo-ops-per-second-usr-sys-time: 341.605474 + bogo-ops-per-second-real-time: 341.609226 + - instance: 55 + bogo-ops: 5125 + bogo-ops-per-second-usr-sys-time: 341.566497 + bogo-ops-per-second-real-time: 341.571000 + - instance: 56 + bogo-ops: 4990 + bogo-ops-per-second-usr-sys-time: 335.800469 + bogo-ops-per-second-real-time: 332.595326 + - instance: 57 + bogo-ops: 5131 + bogo-ops-per-second-usr-sys-time: 342.144653 + bogo-ops-per-second-real-time: 342.135899 + - instance: 58 + bogo-ops: 5137 + bogo-ops-per-second-usr-sys-time: 342.351796 + bogo-ops-per-second-real-time: 342.349832 + - instance: 59 + bogo-ops: 5131 + bogo-ops-per-second-usr-sys-time: 342.079734 + bogo-ops-per-second-real-time: 342.076731 + - instance: 60 + bogo-ops: 5125 + bogo-ops-per-second-usr-sys-time: 341.736062 + bogo-ops-per-second-real-time: 341.737307 + - instance: 61 + bogo-ops: 5131 + bogo-ops-per-second-usr-sys-time: 342.115110 + bogo-ops-per-second-real-time: 342.113562 + - instance: 62 + bogo-ops: 5688 + bogo-ops-per-second-usr-sys-time: 379.263792 + bogo-ops-per-second-real-time: 379.261717 + - instance: 63 + bogo-ops: 5120 + bogo-ops-per-second-usr-sys-time: 341.445305 + bogo-ops-per-second-real-time: 341.445854 + - instance: 64 + bogo-ops: 5148 + bogo-ops-per-second-usr-sys-time: 343.303243 + bogo-ops-per-second-real-time: 343.300860 + - instance: 65 + bogo-ops: 5125 + bogo-ops-per-second-usr-sys-time: 341.869600 + bogo-ops-per-second-real-time: 341.864565 + - instance: 66 + bogo-ops: 5103 + bogo-ops-per-second-usr-sys-time: 340.091375 + bogo-ops-per-second-real-time: 340.090137 + - instance: 67 + bogo-ops: 5131 + bogo-ops-per-second-usr-sys-time: 342.184378 + bogo-ops-per-second-real-time: 342.184794 + - instance: 68 + bogo-ops: 5142 + bogo-ops-per-second-usr-sys-time: 342.897497 + bogo-ops-per-second-real-time: 342.888465 + - instance: 69 + bogo-ops: 6667 + bogo-ops-per-second-usr-sys-time: 444.572030 + bogo-ops-per-second-real-time: 444.565565 + - instance: 70 + bogo-ops: 5114 + bogo-ops-per-second-usr-sys-time: 340.974137 + bogo-ops-per-second-real-time: 340.975926 + - instance: 71 + bogo-ops: 5148 + bogo-ops-per-second-usr-sys-time: 343.268219 + bogo-ops-per-second-real-time: 343.269598 + - instance: 72 + bogo-ops: 5137 + bogo-ops-per-second-usr-sys-time: 342.558655 + bogo-ops-per-second-real-time: 342.559866 + - instance: 73 + bogo-ops: 5165 + bogo-ops-per-second-usr-sys-time: 344.362283 + bogo-ops-per-second-real-time: 344.363142 + - instance: 74 + bogo-ops: 5120 + bogo-ops-per-second-usr-sys-time: 341.402570 + bogo-ops-per-second-real-time: 341.400746 + - instance: 75 + bogo-ops: 5137 + bogo-ops-per-second-usr-sys-time: 342.671310 + bogo-ops-per-second-real-time: 342.509974 + - instance: 76 + bogo-ops: 5091 + bogo-ops-per-second-usr-sys-time: 339.655126 + bogo-ops-per-second-real-time: 339.658372 + - instance: 77 + bogo-ops: 5114 + bogo-ops-per-second-usr-sys-time: 341.080225 + bogo-ops-per-second-real-time: 341.080918 + - instance: 78 + bogo-ops: 5103 + bogo-ops-per-second-usr-sys-time: 340.417981 + bogo-ops-per-second-real-time: 340.419322 + - instance: 79 + bogo-ops: 5080 + bogo-ops-per-second-usr-sys-time: 338.821440 + bogo-ops-per-second-real-time: 338.819655 + - instance: 80 + bogo-ops: 5114 + bogo-ops-per-second-usr-sys-time: 341.184787 + bogo-ops-per-second-real-time: 341.182838 + - instance: 81 + bogo-ops: 5097 + bogo-ops-per-second-usr-sys-time: 340.011714 + bogo-ops-per-second-real-time: 340.010049 + - instance: 82 + bogo-ops: 5120 + bogo-ops-per-second-usr-sys-time: 341.606892 + bogo-ops-per-second-real-time: 341.605039 + - instance: 83 + bogo-ops: 5103 + bogo-ops-per-second-usr-sys-time: 340.358630 + bogo-ops-per-second-real-time: 340.356544 + - instance: 84 + bogo-ops: 5080 + bogo-ops-per-second-usr-sys-time: 338.782033 + bogo-ops-per-second-real-time: 338.782510 + - instance: 85 + bogo-ops: 5125 + bogo-ops-per-second-usr-sys-time: 341.792104 + bogo-ops-per-second-real-time: 341.793318 + - instance: 86 + bogo-ops: 5125 + bogo-ops-per-second-usr-sys-time: 341.912775 + bogo-ops-per-second-real-time: 341.914065 + - instance: 87 + bogo-ops: 5170 + bogo-ops-per-second-usr-sys-time: 344.773523 + bogo-ops-per-second-real-time: 344.774558 + - instance: 88 + bogo-ops: 5125 + bogo-ops-per-second-usr-sys-time: 341.901393 + bogo-ops-per-second-real-time: 341.902579 + - instance: 89 + bogo-ops: 5615 + bogo-ops-per-second-usr-sys-time: 374.455431 + bogo-ops-per-second-real-time: 374.457804 + - instance: 90 + bogo-ops: 5091 + bogo-ops-per-second-usr-sys-time: 339.446436 + bogo-ops-per-second-real-time: 339.414420 + - instance: 91 + bogo-ops: 5103 + bogo-ops-per-second-usr-sys-time: 340.345305 + bogo-ops-per-second-real-time: 340.345644 + - instance: 92 + bogo-ops: 5688 + bogo-ops-per-second-usr-sys-time: 379.263286 + bogo-ops-per-second-real-time: 379.264321 + - instance: 93 + bogo-ops: 5080 + bogo-ops-per-second-usr-sys-time: 338.770443 + bogo-ops-per-second-real-time: 338.772200 + - instance: 94 + bogo-ops: 5120 + bogo-ops-per-second-usr-sys-time: 341.648288 + bogo-ops-per-second-real-time: 341.645364 + - instance: 95 + bogo-ops: 5137 + bogo-ops-per-second-usr-sys-time: 342.704069 + bogo-ops-per-second-real-time: 342.704380 + - instance: 96 + bogo-ops: 5137 + bogo-ops-per-second-usr-sys-time: 342.482284 + bogo-ops-per-second-real-time: 342.483215 + - instance: 97 + bogo-ops: 5080 + bogo-ops-per-second-usr-sys-time: 338.779887 + bogo-ops-per-second-real-time: 338.778626 + - instance: 98 + bogo-ops: 5091 + bogo-ops-per-second-usr-sys-time: 339.443268 + bogo-ops-per-second-real-time: 339.431319 + - instance: 99 + bogo-ops: 5148 + bogo-ops-per-second-usr-sys-time: 343.478172 + bogo-ops-per-second-real-time: 343.478519 + - instance: 100 + bogo-ops: 5103 + bogo-ops-per-second-usr-sys-time: 340.310056 + bogo-ops-per-second-real-time: 340.308219 + - instance: 101 + bogo-ops: 5108 + bogo-ops-per-second-usr-sys-time: 340.544276 + bogo-ops-per-second-real-time: 340.546286 + - instance: 102 + bogo-ops: 5125 + bogo-ops-per-second-usr-sys-time: 341.839296 + bogo-ops-per-second-real-time: 341.840976 + - instance: 103 + bogo-ops: 5125 + bogo-ops-per-second-usr-sys-time: 341.917064 + bogo-ops-per-second-real-time: 341.918971 + - instance: 104 + bogo-ops: 5137 + bogo-ops-per-second-usr-sys-time: 342.857616 + bogo-ops-per-second-real-time: 342.846970 + - instance: 105 + bogo-ops: 5108 + bogo-ops-per-second-usr-sys-time: 340.768145 + bogo-ops-per-second-real-time: 340.768675 + - instance: 106 + bogo-ops: 5142 + bogo-ops-per-second-usr-sys-time: 342.873101 + bogo-ops-per-second-real-time: 342.869534 + - instance: 107 + bogo-ops: 5114 + bogo-ops-per-second-usr-sys-time: 341.269097 + bogo-ops-per-second-real-time: 341.269626 + - instance: 108 + bogo-ops: 5148 + bogo-ops-per-second-usr-sys-time: 343.520894 + bogo-ops-per-second-real-time: 343.518951 + - instance: 109 + bogo-ops: 5131 + bogo-ops-per-second-usr-sys-time: 342.288401 + bogo-ops-per-second-real-time: 342.288983 + - instance: 110 + bogo-ops: 5131 + bogo-ops-per-second-usr-sys-time: 342.191772 + bogo-ops-per-second-real-time: 342.193048 + - instance: 111 + bogo-ops: 5108 + bogo-ops-per-second-usr-sys-time: 340.843114 + bogo-ops-per-second-real-time: 340.845137 + - instance: 112 + bogo-ops: 5131 + bogo-ops-per-second-usr-sys-time: 342.204781 + bogo-ops-per-second-real-time: 342.205138 + - instance: 113 + bogo-ops: 5114 + bogo-ops-per-second-usr-sys-time: 341.149827 + bogo-ops-per-second-real-time: 341.151311 + - instance: 114 + bogo-ops: 5137 + bogo-ops-per-second-usr-sys-time: 342.645779 + bogo-ops-per-second-real-time: 342.646501 + - instance: 115 + bogo-ops: 5142 + bogo-ops-per-second-usr-sys-time: 343.041318 + bogo-ops-per-second-real-time: 343.041060 + - instance: 116 + bogo-ops: 5131 + bogo-ops-per-second-usr-sys-time: 342.511772 + bogo-ops-per-second-real-time: 342.512374 + - instance: 117 + bogo-ops: 5142 + bogo-ops-per-second-usr-sys-time: 343.128465 + bogo-ops-per-second-real-time: 343.122029 + - instance: 118 + bogo-ops: 5142 + bogo-ops-per-second-usr-sys-time: 343.079312 + bogo-ops-per-second-real-time: 343.080470 + - instance: 119 + bogo-ops: 5120 + bogo-ops-per-second-usr-sys-time: 341.597593 + bogo-ops-per-second-real-time: 341.599785 + - instance: 120 + bogo-ops: 5142 + bogo-ops-per-second-usr-sys-time: 343.038663 + bogo-ops-per-second-real-time: 343.037252 + - instance: 121 + bogo-ops: 5148 + bogo-ops-per-second-usr-sys-time: 343.681222 + bogo-ops-per-second-real-time: 343.459244 + - instance: 122 + bogo-ops: 5137 + bogo-ops-per-second-usr-sys-time: 342.771985 + bogo-ops-per-second-real-time: 342.770114 + - instance: 123 + bogo-ops: 5114 + bogo-ops-per-second-usr-sys-time: 341.237604 + bogo-ops-per-second-real-time: 341.237507 + - instance: 124 + bogo-ops: 5137 + bogo-ops-per-second-usr-sys-time: 342.777497 + bogo-ops-per-second-real-time: 342.777645 + - instance: 125 + bogo-ops: 5114 + bogo-ops-per-second-usr-sys-time: 341.235281 + bogo-ops-per-second-real-time: 341.236785 + - instance: 126 + bogo-ops: 5131 + bogo-ops-per-second-usr-sys-time: 342.304066 + bogo-ops-per-second-real-time: 342.306215 + - instance: 127 + bogo-ops: 5108 + bogo-ops-per-second-usr-sys-time: 341.000550 + bogo-ops-per-second-real-time: 341.001943 + - instance: 128 + bogo-ops: 5120 + bogo-ops-per-second-usr-sys-time: 341.596750 + bogo-ops-per-second-real-time: 341.598269 + - instance: 129 + bogo-ops: 5148 + bogo-ops-per-second-usr-sys-time: 343.676450 + bogo-ops-per-second-real-time: 343.676622 + - instance: 130 + bogo-ops: 5125 + bogo-ops-per-second-usr-sys-time: 341.888986 + bogo-ops-per-second-real-time: 341.891480 + - instance: 131 + bogo-ops: 5137 + bogo-ops-per-second-usr-sys-time: 342.846655 + bogo-ops-per-second-real-time: 342.847750 + - instance: 132 + bogo-ops: 5125 + bogo-ops-per-second-usr-sys-time: 341.854208 + bogo-ops-per-second-real-time: 341.855480 + - instance: 133 + bogo-ops: 5120 + bogo-ops-per-second-usr-sys-time: 341.809428 + bogo-ops-per-second-real-time: 341.810100 + - instance: 134 + bogo-ops: 5131 + bogo-ops-per-second-usr-sys-time: 342.399205 + bogo-ops-per-second-real-time: 342.399817 + - instance: 135 + bogo-ops: 5120 + bogo-ops-per-second-usr-sys-time: 341.556803 + bogo-ops-per-second-real-time: 341.557118 + - instance: 136 + bogo-ops: 5131 + bogo-ops-per-second-usr-sys-time: 342.525239 + bogo-ops-per-second-real-time: 342.525507 + - instance: 137 + bogo-ops: 5137 + bogo-ops-per-second-usr-sys-time: 342.935528 + bogo-ops-per-second-real-time: 342.936894 + - instance: 138 + bogo-ops: 5131 + bogo-ops-per-second-usr-sys-time: 342.379670 + bogo-ops-per-second-real-time: 342.379994 + - instance: 139 + bogo-ops: 5142 + bogo-ops-per-second-usr-sys-time: 343.235108 + bogo-ops-per-second-real-time: 343.236923 + - instance: 140 + bogo-ops: 5137 + bogo-ops-per-second-usr-sys-time: 342.856838 + bogo-ops-per-second-real-time: 342.857696 + - instance: 141 + bogo-ops: 5137 + bogo-ops-per-second-usr-sys-time: 342.960461 + bogo-ops-per-second-real-time: 342.960470 + - instance: 142 + bogo-ops: 5125 + bogo-ops-per-second-usr-sys-time: 342.041088 + bogo-ops-per-second-real-time: 342.043039 + - instance: 143 + bogo-ops: 5142 + bogo-ops-per-second-usr-sys-time: 343.183885 + bogo-ops-per-second-real-time: 343.184682 + - instance: 144 + bogo-ops: 5125 + bogo-ops-per-second-usr-sys-time: 342.009155 + bogo-ops-per-second-real-time: 342.009995 + - instance: 145 + bogo-ops: 5142 + bogo-ops-per-second-usr-sys-time: 342.988781 + bogo-ops-per-second-real-time: 342.990040 + - instance: 146 + bogo-ops: 5142 + bogo-ops-per-second-usr-sys-time: 343.170785 + bogo-ops-per-second-real-time: 343.170582 + - instance: 147 + bogo-ops: 5120 + bogo-ops-per-second-usr-sys-time: 341.918333 + bogo-ops-per-second-real-time: 341.888575 + - instance: 148 + bogo-ops: 5170 + bogo-ops-per-second-usr-sys-time: 345.033069 + bogo-ops-per-second-real-time: 344.998743 + - instance: 149 + bogo-ops: 5131 + bogo-ops-per-second-usr-sys-time: 342.793114 + bogo-ops-per-second-real-time: 342.545630 + - instance: 150 + bogo-ops: 5131 + bogo-ops-per-second-usr-sys-time: 342.566768 + bogo-ops-per-second-real-time: 342.568727 + - instance: 151 + bogo-ops: 5114 + bogo-ops-per-second-usr-sys-time: 341.325721 + bogo-ops-per-second-real-time: 341.326522 + - instance: 152 + bogo-ops: 5114 + bogo-ops-per-second-usr-sys-time: 341.361651 + bogo-ops-per-second-real-time: 341.340607 + - instance: 153 + bogo-ops: 5137 + bogo-ops-per-second-usr-sys-time: 342.914605 + bogo-ops-per-second-real-time: 342.894210 + - instance: 154 + bogo-ops: 5108 + bogo-ops-per-second-usr-sys-time: 341.037910 + bogo-ops-per-second-real-time: 341.038517 + - instance: 155 + bogo-ops: 5125 + bogo-ops-per-second-usr-sys-time: 342.237793 + bogo-ops-per-second-real-time: 342.239347 + - instance: 156 + bogo-ops: 5108 + bogo-ops-per-second-usr-sys-time: 340.984387 + bogo-ops-per-second-real-time: 340.984950 + - instance: 157 + bogo-ops: 5125 + bogo-ops-per-second-usr-sys-time: 342.128449 + bogo-ops-per-second-real-time: 342.130083 + - instance: 158 + bogo-ops: 5131 + bogo-ops-per-second-usr-sys-time: 342.452748 + bogo-ops-per-second-real-time: 342.442581 + - instance: 159 + bogo-ops: 5148 + bogo-ops-per-second-usr-sys-time: 343.678309 + bogo-ops-per-second-real-time: 343.680380 + - instance: 160 + bogo-ops: 5137 + bogo-ops-per-second-usr-sys-time: 342.933559 + bogo-ops-per-second-real-time: 342.930339 + - instance: 161 + bogo-ops: 5097 + bogo-ops-per-second-usr-sys-time: 340.241202 + bogo-ops-per-second-real-time: 340.243507 + - instance: 162 + bogo-ops: 5142 + bogo-ops-per-second-usr-sys-time: 343.202416 + bogo-ops-per-second-real-time: 343.203271 + - instance: 163 + bogo-ops: 5097 + bogo-ops-per-second-usr-sys-time: 340.231391 + bogo-ops-per-second-real-time: 340.232255 + - instance: 164 + bogo-ops: 5165 + bogo-ops-per-second-usr-sys-time: 344.676792 + bogo-ops-per-second-real-time: 344.675272 + - instance: 165 + bogo-ops: 5125 + bogo-ops-per-second-usr-sys-time: 342.032071 + bogo-ops-per-second-real-time: 342.032491 + - instance: 166 + bogo-ops: 5137 + bogo-ops-per-second-usr-sys-time: 342.821876 + bogo-ops-per-second-real-time: 342.823033 + - instance: 167 + bogo-ops: 5108 + bogo-ops-per-second-usr-sys-time: 341.057106 + bogo-ops-per-second-real-time: 341.057589 + - instance: 168 + bogo-ops: 5137 + bogo-ops-per-second-usr-sys-time: 342.846312 + bogo-ops-per-second-real-time: 342.847488 + - instance: 169 + bogo-ops: 5131 + bogo-ops-per-second-usr-sys-time: 342.411429 + bogo-ops-per-second-real-time: 342.412848 + - instance: 170 + bogo-ops: 5131 + bogo-ops-per-second-usr-sys-time: 342.487218 + bogo-ops-per-second-real-time: 342.488870 + - instance: 171 + bogo-ops: 5153 + bogo-ops-per-second-usr-sys-time: 343.957341 + bogo-ops-per-second-real-time: 343.958146 + - instance: 172 + bogo-ops: 5120 + bogo-ops-per-second-usr-sys-time: 341.624694 + bogo-ops-per-second-real-time: 341.626510 + - instance: 173 + bogo-ops: 5131 + bogo-ops-per-second-usr-sys-time: 342.615171 + bogo-ops-per-second-real-time: 342.616572 + - instance: 174 + bogo-ops: 5108 + bogo-ops-per-second-usr-sys-time: 341.005535 + bogo-ops-per-second-real-time: 341.005460 + - instance: 175 + bogo-ops: 5114 + bogo-ops-per-second-usr-sys-time: 341.369786 + bogo-ops-per-second-real-time: 341.358598 + - instance: 176 + bogo-ops: 5153 + bogo-ops-per-second-usr-sys-time: 344.110452 + bogo-ops-per-second-real-time: 344.102820 + - instance: 177 + bogo-ops: 5103 + bogo-ops-per-second-usr-sys-time: 340.745670 + bogo-ops-per-second-real-time: 340.747491 + - instance: 178 + bogo-ops: 5131 + bogo-ops-per-second-usr-sys-time: 342.487790 + bogo-ops-per-second-real-time: 342.488080 + - instance: 179 + bogo-ops: 5103 + bogo-ops-per-second-usr-sys-time: 340.722464 + bogo-ops-per-second-real-time: 340.723781 + - instance: 180 + bogo-ops: 5125 + bogo-ops-per-second-usr-sys-time: 342.393385 + bogo-ops-per-second-real-time: 342.300118 + - instance: 181 + bogo-ops: 5108 + bogo-ops-per-second-usr-sys-time: 340.879644 + bogo-ops-per-second-real-time: 340.879156 + - instance: 182 + bogo-ops: 5114 + bogo-ops-per-second-usr-sys-time: 341.507544 + bogo-ops-per-second-real-time: 341.502120 + - instance: 183 + bogo-ops: 5103 + bogo-ops-per-second-usr-sys-time: 340.899184 + bogo-ops-per-second-real-time: 340.900483 + - instance: 184 + bogo-ops: 5103 + bogo-ops-per-second-usr-sys-time: 340.762076 + bogo-ops-per-second-real-time: 340.762328 + - instance: 185 + bogo-ops: 5148 + bogo-ops-per-second-usr-sys-time: 343.773965 + bogo-ops-per-second-real-time: 343.775842 + - instance: 186 + bogo-ops: 5131 + bogo-ops-per-second-usr-sys-time: 342.649285 + bogo-ops-per-second-real-time: 342.650072 + - instance: 187 + bogo-ops: 5148 + bogo-ops-per-second-usr-sys-time: 343.803145 + bogo-ops-per-second-real-time: 343.802548 + - instance: 188 + bogo-ops: 5125 + bogo-ops-per-second-usr-sys-time: 342.245038 + bogo-ops-per-second-real-time: 342.246943 + - instance: 189 + bogo-ops: 5148 + bogo-ops-per-second-usr-sys-time: 343.767675 + bogo-ops-per-second-real-time: 343.769257 + - instance: 190 + bogo-ops: 5131 + bogo-ops-per-second-usr-sys-time: 342.566586 + bogo-ops-per-second-real-time: 342.567554 + - instance: 191 + bogo-ops: 5148 + bogo-ops-per-second-usr-sys-time: 343.704306 + bogo-ops-per-second-real-time: 343.705212 + - instance: 192 + bogo-ops: 5131 + bogo-ops-per-second-usr-sys-time: 342.690935 + bogo-ops-per-second-real-time: 342.691796 + - instance: 193 + bogo-ops: 5159 + bogo-ops-per-second-usr-sys-time: 344.683871 + bogo-ops-per-second-real-time: 344.684319 + - instance: 194 + bogo-ops: 5148 + bogo-ops-per-second-usr-sys-time: 343.908957 + bogo-ops-per-second-real-time: 343.910522 + - instance: 195 + bogo-ops: 5137 + bogo-ops-per-second-usr-sys-time: 343.136125 + bogo-ops-per-second-real-time: 343.136501 + - instance: 196 + bogo-ops: 5137 + bogo-ops-per-second-usr-sys-time: 343.029853 + bogo-ops-per-second-real-time: 343.030093 + - instance: 197 + bogo-ops: 5137 + bogo-ops-per-second-usr-sys-time: 343.019087 + bogo-ops-per-second-real-time: 343.019946 + - instance: 198 + bogo-ops: 5120 + bogo-ops-per-second-usr-sys-time: 341.788847 + bogo-ops-per-second-real-time: 341.790352 + - instance: 199 + bogo-ops: 5125 + bogo-ops-per-second-usr-sys-time: 342.258751 + bogo-ops-per-second-real-time: 342.261335 + - instance: 200 + bogo-ops: 5148 + bogo-ops-per-second-usr-sys-time: 343.688886 + bogo-ops-per-second-real-time: 343.680030 + - instance: 201 + bogo-ops: 5108 + bogo-ops-per-second-usr-sys-time: 341.146624 + bogo-ops-per-second-real-time: 341.141955 + - instance: 202 + bogo-ops: 5097 + bogo-ops-per-second-usr-sys-time: 340.454171 + bogo-ops-per-second-real-time: 340.452885 + - instance: 203 + bogo-ops: 5120 + bogo-ops-per-second-usr-sys-time: 342.069627 + bogo-ops-per-second-real-time: 342.069417 + - instance: 204 + bogo-ops: 5137 + bogo-ops-per-second-usr-sys-time: 343.495413 + bogo-ops-per-second-real-time: 343.043381 + - instance: 205 + bogo-ops: 5114 + bogo-ops-per-second-usr-sys-time: 341.631377 + bogo-ops-per-second-real-time: 341.631441 + - instance: 206 + bogo-ops: 5097 + bogo-ops-per-second-usr-sys-time: 340.267641 + bogo-ops-per-second-real-time: 340.268218 + - instance: 207 + bogo-ops: 5114 + bogo-ops-per-second-usr-sys-time: 341.610063 + bogo-ops-per-second-real-time: 341.604874 + - instance: 208 + bogo-ops: 5091 + bogo-ops-per-second-usr-sys-time: 340.175578 + bogo-ops-per-second-real-time: 340.175599 + - instance: 209 + bogo-ops: 5125 + bogo-ops-per-second-usr-sys-time: 342.293269 + bogo-ops-per-second-real-time: 342.294188 + - instance: 210 + bogo-ops: 5108 + bogo-ops-per-second-usr-sys-time: 341.014846 + bogo-ops-per-second-real-time: 341.016310 + - instance: 211 + bogo-ops: 5097 + bogo-ops-per-second-usr-sys-time: 340.420632 + bogo-ops-per-second-real-time: 340.422195 + - instance: 212 + bogo-ops: 5131 + bogo-ops-per-second-usr-sys-time: 342.747569 + bogo-ops-per-second-real-time: 342.749146 + - instance: 213 + bogo-ops: 5086 + bogo-ops-per-second-usr-sys-time: 339.703656 + bogo-ops-per-second-real-time: 339.702741 + - instance: 214 + bogo-ops: 5131 + bogo-ops-per-second-usr-sys-time: 342.790824 + bogo-ops-per-second-real-time: 342.788410 + - instance: 215 + bogo-ops: 5108 + bogo-ops-per-second-usr-sys-time: 341.170731 + bogo-ops-per-second-real-time: 341.172740 + - instance: 216 + bogo-ops: 5125 + bogo-ops-per-second-usr-sys-time: 342.367333 + bogo-ops-per-second-real-time: 342.368768 + - instance: 217 + bogo-ops: 5137 + bogo-ops-per-second-usr-sys-time: 343.297424 + bogo-ops-per-second-real-time: 343.294327 + - instance: 218 + bogo-ops: 5114 + bogo-ops-per-second-usr-sys-time: 341.852301 + bogo-ops-per-second-real-time: 341.675053 + - instance: 219 + bogo-ops: 3612 + bogo-ops-per-second-usr-sys-time: 342.566831 + bogo-ops-per-second-real-time: 241.335430 + - instance: 220 + bogo-ops: 5108 + bogo-ops-per-second-usr-sys-time: 341.175289 + bogo-ops-per-second-real-time: 341.176000 + - instance: 221 + bogo-ops: 5125 + bogo-ops-per-second-usr-sys-time: 342.170844 + bogo-ops-per-second-real-time: 342.170422 + - instance: 222 + bogo-ops: 5103 + bogo-ops-per-second-usr-sys-time: 341.013203 + bogo-ops-per-second-real-time: 341.014484 + - instance: 223 + bogo-ops: 5114 + bogo-ops-per-second-usr-sys-time: 341.802217 + bogo-ops-per-second-real-time: 341.798269 + - instance: 224 + bogo-ops: 5091 + bogo-ops-per-second-usr-sys-time: 340.258813 + bogo-ops-per-second-real-time: 340.258974 + - instance: 225 + bogo-ops: 5114 + bogo-ops-per-second-usr-sys-time: 341.595185 + bogo-ops-per-second-real-time: 341.596986 + - instance: 226 + bogo-ops: 5120 + bogo-ops-per-second-usr-sys-time: 342.060074 + bogo-ops-per-second-real-time: 342.056368 + - instance: 227 + bogo-ops: 5114 + bogo-ops-per-second-usr-sys-time: 341.694538 + bogo-ops-per-second-real-time: 341.696835 + - instance: 228 + bogo-ops: 5120 + bogo-ops-per-second-usr-sys-time: 342.008024 + bogo-ops-per-second-real-time: 342.003331 + - instance: 229 + bogo-ops: 5075 + bogo-ops-per-second-usr-sys-time: 339.127887 + bogo-ops-per-second-real-time: 339.127717 + - instance: 230 + bogo-ops: 5114 + bogo-ops-per-second-usr-sys-time: 341.759229 + bogo-ops-per-second-real-time: 341.760022 + - instance: 231 + bogo-ops: 5069 + bogo-ops-per-second-usr-sys-time: 339.283841 + bogo-ops-per-second-real-time: 338.744878 + - instance: 232 + bogo-ops: 5114 + bogo-ops-per-second-usr-sys-time: 341.688739 + bogo-ops-per-second-real-time: 341.689591 + - instance: 233 + bogo-ops: 5063 + bogo-ops-per-second-usr-sys-time: 338.460648 + bogo-ops-per-second-real-time: 338.389567 + - instance: 234 + bogo-ops: 5097 + bogo-ops-per-second-usr-sys-time: 340.609197 + bogo-ops-per-second-real-time: 340.610205 + - instance: 235 + bogo-ops: 5137 + bogo-ops-per-second-usr-sys-time: 343.085593 + bogo-ops-per-second-real-time: 343.066660 + - instance: 236 + bogo-ops: 5091 + bogo-ops-per-second-usr-sys-time: 340.088861 + bogo-ops-per-second-real-time: 340.090126 + - instance: 237 + bogo-ops: 5091 + bogo-ops-per-second-usr-sys-time: 340.080547 + bogo-ops-per-second-real-time: 340.081915 + - instance: 238 + bogo-ops: 5114 + bogo-ops-per-second-usr-sys-time: 341.579032 + bogo-ops-per-second-real-time: 341.579725 + - instance: 239 + bogo-ops: 5114 + bogo-ops-per-second-usr-sys-time: 341.857854 + bogo-ops-per-second-real-time: 341.857090 + - instance: 240 + bogo-ops: 6301 + bogo-ops-per-second-usr-sys-time: 420.985453 + bogo-ops-per-second-real-time: 420.959007 + - instance: 241 + bogo-ops: 5097 + bogo-ops-per-second-usr-sys-time: 340.687924 + bogo-ops-per-second-real-time: 340.687765 + - instance: 242 + bogo-ops: 5097 + bogo-ops-per-second-usr-sys-time: 340.664493 + bogo-ops-per-second-real-time: 340.667119 + - instance: 243 + bogo-ops: 5125 + bogo-ops-per-second-usr-sys-time: 342.457218 + bogo-ops-per-second-real-time: 342.392125 + - instance: 244 + bogo-ops: 5097 + bogo-ops-per-second-usr-sys-time: 340.577015 + bogo-ops-per-second-real-time: 340.577902 + - instance: 245 + bogo-ops: 5120 + bogo-ops-per-second-usr-sys-time: 342.100597 + bogo-ops-per-second-real-time: 342.079786 + - instance: 246 + bogo-ops: 5091 + bogo-ops-per-second-usr-sys-time: 340.360088 + bogo-ops-per-second-real-time: 340.361171 + - instance: 247 + bogo-ops: 5120 + bogo-ops-per-second-usr-sys-time: 342.000028 + bogo-ops-per-second-real-time: 341.997585 + - instance: 248 + bogo-ops: 5091 + bogo-ops-per-second-usr-sys-time: 340.264635 + bogo-ops-per-second-real-time: 340.264770 + - instance: 249 + bogo-ops: 5114 + bogo-ops-per-second-usr-sys-time: 341.961315 + bogo-ops-per-second-real-time: 341.909719 + - instance: 250 + bogo-ops: 5120 + bogo-ops-per-second-usr-sys-time: 342.391758 + bogo-ops-per-second-real-time: 342.285663 + - instance: 251 + bogo-ops: 5125 + bogo-ops-per-second-usr-sys-time: 342.424727 + bogo-ops-per-second-real-time: 342.426116 + - instance: 252 + bogo-ops: 5114 + bogo-ops-per-second-usr-sys-time: 341.798448 + bogo-ops-per-second-real-time: 341.798819 + - instance: 253 + bogo-ops: 5125 + bogo-ops-per-second-usr-sys-time: 342.458453 + bogo-ops-per-second-real-time: 342.458674 + - instance: 254 + bogo-ops: 5114 + bogo-ops-per-second-usr-sys-time: 341.688054 + bogo-ops-per-second-real-time: 341.688186 + - instance: 255 + bogo-ops: 5103 + bogo-ops-per-second-usr-sys-time: 341.044768 + bogo-ops-per-second-real-time: 341.044962 + - instance: 256 + bogo-ops: 5080 + bogo-ops-per-second-usr-sys-time: 339.658424 + bogo-ops-per-second-real-time: 339.659577 + - instance: 257 + bogo-ops: 5125 + bogo-ops-per-second-usr-sys-time: 342.539892 + bogo-ops-per-second-real-time: 342.505289 + - instance: 258 + bogo-ops: 5086 + bogo-ops-per-second-usr-sys-time: 339.869347 + bogo-ops-per-second-real-time: 339.870883 + - instance: 259 + bogo-ops: 5103 + bogo-ops-per-second-usr-sys-time: 341.167551 + bogo-ops-per-second-real-time: 341.168195 + - instance: 260 + bogo-ops: 5080 + bogo-ops-per-second-usr-sys-time: 339.415258 + bogo-ops-per-second-real-time: 339.416528 + - instance: 261 + bogo-ops: 5108 + bogo-ops-per-second-usr-sys-time: 341.369324 + bogo-ops-per-second-real-time: 341.370539 + - instance: 262 + bogo-ops: 5103 + bogo-ops-per-second-usr-sys-time: 340.973168 + bogo-ops-per-second-real-time: 340.973277 + - instance: 263 + bogo-ops: 5086 + bogo-ops-per-second-usr-sys-time: 339.970012 + bogo-ops-per-second-real-time: 339.970585 + - instance: 264 + bogo-ops: 5125 + bogo-ops-per-second-usr-sys-time: 342.597893 + bogo-ops-per-second-real-time: 342.558315 + - instance: 265 + bogo-ops: 5086 + bogo-ops-per-second-usr-sys-time: 339.884224 + bogo-ops-per-second-real-time: 339.885623 + - instance: 266 + bogo-ops: 5080 + bogo-ops-per-second-usr-sys-time: 339.589172 + bogo-ops-per-second-real-time: 339.589261 + - instance: 267 + bogo-ops: 5103 + bogo-ops-per-second-usr-sys-time: 341.220751 + bogo-ops-per-second-real-time: 341.220991 + - instance: 268 + bogo-ops: 5114 + bogo-ops-per-second-usr-sys-time: 341.767474 + bogo-ops-per-second-real-time: 341.769492 + - instance: 269 + bogo-ops: 5108 + bogo-ops-per-second-usr-sys-time: 341.290908 + bogo-ops-per-second-real-time: 341.292384 + - instance: 270 + bogo-ops: 5007 + bogo-ops-per-second-usr-sys-time: 341.953026 + bogo-ops-per-second-real-time: 334.553987 + - instance: 271 + bogo-ops: 5114 + bogo-ops-per-second-usr-sys-time: 341.823145 + bogo-ops-per-second-real-time: 341.823951 + - instance: 272 + bogo-ops: 5091 + bogo-ops-per-second-usr-sys-time: 340.401188 + bogo-ops-per-second-real-time: 340.402456 + - instance: 273 + bogo-ops: 5091 + bogo-ops-per-second-usr-sys-time: 340.134055 + bogo-ops-per-second-real-time: 340.134629 + - instance: 274 + bogo-ops: 5103 + bogo-ops-per-second-usr-sys-time: 341.277846 + bogo-ops-per-second-real-time: 341.277357 + - instance: 275 + bogo-ops: 5103 + bogo-ops-per-second-usr-sys-time: 341.239758 + bogo-ops-per-second-real-time: 341.239542 + - instance: 276 + bogo-ops: 5114 + bogo-ops-per-second-usr-sys-time: 341.937101 + bogo-ops-per-second-real-time: 341.936688 + - instance: 277 + bogo-ops: 5091 + bogo-ops-per-second-usr-sys-time: 340.288812 + bogo-ops-per-second-real-time: 340.288109 + - instance: 278 + bogo-ops: 5097 + bogo-ops-per-second-usr-sys-time: 340.751332 + bogo-ops-per-second-real-time: 340.752375 + - instance: 279 + bogo-ops: 5091 + bogo-ops-per-second-usr-sys-time: 340.143508 + bogo-ops-per-second-real-time: 340.142333 + - instance: 280 + bogo-ops: 5091 + bogo-ops-per-second-usr-sys-time: 340.391788 + bogo-ops-per-second-real-time: 340.390252 + - instance: 281 + bogo-ops: 5120 + bogo-ops-per-second-usr-sys-time: 342.258069 + bogo-ops-per-second-real-time: 342.260050 + - instance: 282 + bogo-ops: 5114 + bogo-ops-per-second-usr-sys-time: 341.850518 + bogo-ops-per-second-real-time: 341.851664 + - instance: 283 + bogo-ops: 5103 + bogo-ops-per-second-usr-sys-time: 341.222850 + bogo-ops-per-second-real-time: 341.218309 + - instance: 284 + bogo-ops: 5108 + bogo-ops-per-second-usr-sys-time: 341.481628 + bogo-ops-per-second-real-time: 341.482337 + - instance: 285 + bogo-ops: 5091 + bogo-ops-per-second-usr-sys-time: 340.369463 + bogo-ops-per-second-real-time: 340.370453 + - instance: 286 + bogo-ops: 5091 + bogo-ops-per-second-usr-sys-time: 340.612262 + bogo-ops-per-second-real-time: 340.336878 + - instance: 287 + bogo-ops: 5058 + bogo-ops-per-second-usr-sys-time: 339.987649 + bogo-ops-per-second-real-time: 338.303804 + - instance: 288 + bogo-ops: 5103 + bogo-ops-per-second-usr-sys-time: 341.079439 + bogo-ops-per-second-real-time: 341.076749 + - instance: 289 + bogo-ops: 5086 + bogo-ops-per-second-usr-sys-time: 340.172841 + bogo-ops-per-second-real-time: 340.130965 + - instance: 290 + bogo-ops: 5097 + bogo-ops-per-second-usr-sys-time: 340.767803 + bogo-ops-per-second-real-time: 340.769071 + - instance: 291 + bogo-ops: 5069 + bogo-ops-per-second-usr-sys-time: 340.096255 + bogo-ops-per-second-real-time: 338.962155 + - instance: 292 + bogo-ops: 5069 + bogo-ops-per-second-usr-sys-time: 339.131213 + bogo-ops-per-second-real-time: 338.867724 + - instance: 293 + bogo-ops: 5097 + bogo-ops-per-second-usr-sys-time: 340.867688 + bogo-ops-per-second-real-time: 340.869796 + - instance: 294 + bogo-ops: 4833 + bogo-ops-per-second-usr-sys-time: 323.275884 + bogo-ops-per-second-real-time: 323.122484 + - instance: 295 + bogo-ops: 5075 + bogo-ops-per-second-usr-sys-time: 339.419385 + bogo-ops-per-second-real-time: 339.420544 + - instance: 296 + bogo-ops: 5058 + bogo-ops-per-second-usr-sys-time: 338.300740 + bogo-ops-per-second-real-time: 338.298878 + - instance: 297 + bogo-ops: 5075 + bogo-ops-per-second-usr-sys-time: 339.392056 + bogo-ops-per-second-real-time: 339.393420 + - instance: 298 + bogo-ops: 5091 + bogo-ops-per-second-usr-sys-time: 340.528101 + bogo-ops-per-second-real-time: 340.528181 + - instance: 299 + bogo-ops: 5120 + bogo-ops-per-second-usr-sys-time: 342.428099 + bogo-ops-per-second-real-time: 342.426138 + - instance: 300 + bogo-ops: 4467 + bogo-ops-per-second-usr-sys-time: 298.788892 + bogo-ops-per-second-real-time: 298.785550 + - instance: 301 + bogo-ops: 5075 + bogo-ops-per-second-usr-sys-time: 339.335981 + bogo-ops-per-second-real-time: 339.338892 + - instance: 302 + bogo-ops: 5091 + bogo-ops-per-second-usr-sys-time: 340.442958 + bogo-ops-per-second-real-time: 340.442775 + - instance: 303 + bogo-ops: 5080 + bogo-ops-per-second-usr-sys-time: 339.817106 + bogo-ops-per-second-real-time: 339.819084 + - instance: 304 + bogo-ops: 5091 + bogo-ops-per-second-usr-sys-time: 340.518922 + bogo-ops-per-second-real-time: 340.517049 + - instance: 305 + bogo-ops: 5075 + bogo-ops-per-second-usr-sys-time: 339.309958 + bogo-ops-per-second-real-time: 339.310305 + - instance: 306 + bogo-ops: 5091 + bogo-ops-per-second-usr-sys-time: 340.446601 + bogo-ops-per-second-real-time: 340.441635 + - instance: 307 + bogo-ops: 5069 + bogo-ops-per-second-usr-sys-time: 339.108684 + bogo-ops-per-second-real-time: 339.106911 + - instance: 308 + bogo-ops: 5063 + bogo-ops-per-second-usr-sys-time: 338.706318 + bogo-ops-per-second-real-time: 338.708828 + - instance: 309 + bogo-ops: 5075 + bogo-ops-per-second-usr-sys-time: 339.297300 + bogo-ops-per-second-real-time: 339.295242 + - instance: 310 + bogo-ops: 5080 + bogo-ops-per-second-usr-sys-time: 339.601681 + bogo-ops-per-second-real-time: 339.601737 + - instance: 311 + bogo-ops: 5108 + bogo-ops-per-second-usr-sys-time: 341.555221 + bogo-ops-per-second-real-time: 341.557775 + - instance: 312 + bogo-ops: 5075 + bogo-ops-per-second-usr-sys-time: 339.239056 + bogo-ops-per-second-real-time: 339.239881 + - instance: 313 + bogo-ops: 5114 + bogo-ops-per-second-usr-sys-time: 342.113854 + bogo-ops-per-second-real-time: 342.084123 + - instance: 314 + bogo-ops: 5063 + bogo-ops-per-second-usr-sys-time: 338.701402 + bogo-ops-per-second-real-time: 338.700222 + - instance: 315 + bogo-ops: 5097 + bogo-ops-per-second-usr-sys-time: 340.884991 + bogo-ops-per-second-real-time: 340.886711 + - instance: 316 + bogo-ops: 5075 + bogo-ops-per-second-usr-sys-time: 339.380435 + bogo-ops-per-second-real-time: 339.382202 + - instance: 317 + bogo-ops: 5108 + bogo-ops-per-second-usr-sys-time: 341.727214 + bogo-ops-per-second-real-time: 341.692598 + - instance: 318 + bogo-ops: 5086 + bogo-ops-per-second-usr-sys-time: 340.220786 + bogo-ops-per-second-real-time: 340.221568 + - instance: 319 + bogo-ops: 5069 + bogo-ops-per-second-usr-sys-time: 338.854611 + bogo-ops-per-second-real-time: 338.857576 + - instance: 320 + bogo-ops: 5086 + bogo-ops-per-second-usr-sys-time: 340.223153 + bogo-ops-per-second-real-time: 340.220900 + - instance: 321 + bogo-ops: 5069 + bogo-ops-per-second-usr-sys-time: 338.861701 + bogo-ops-per-second-real-time: 338.857759 + - instance: 322 + bogo-ops: 5091 + bogo-ops-per-second-usr-sys-time: 340.447967 + bogo-ops-per-second-real-time: 340.445944 + - instance: 323 + bogo-ops: 5091 + bogo-ops-per-second-usr-sys-time: 340.639747 + bogo-ops-per-second-real-time: 340.641494 + - instance: 324 + bogo-ops: 5086 + bogo-ops-per-second-usr-sys-time: 340.309363 + bogo-ops-per-second-real-time: 340.307034 + - instance: 325 + bogo-ops: 5069 + bogo-ops-per-second-usr-sys-time: 339.156104 + bogo-ops-per-second-real-time: 339.157527 + - instance: 326 + bogo-ops: 5103 + bogo-ops-per-second-usr-sys-time: 341.188856 + bogo-ops-per-second-real-time: 341.190531 + - instance: 327 + bogo-ops: 5091 + bogo-ops-per-second-usr-sys-time: 340.461605 + bogo-ops-per-second-real-time: 340.459558 + - instance: 328 + bogo-ops: 5103 + bogo-ops-per-second-usr-sys-time: 341.176333 + bogo-ops-per-second-real-time: 341.175079 + - instance: 329 + bogo-ops: 5086 + bogo-ops-per-second-usr-sys-time: 340.066485 + bogo-ops-per-second-real-time: 340.069114 + - instance: 330 + bogo-ops: 5063 + bogo-ops-per-second-usr-sys-time: 339.039255 + bogo-ops-per-second-real-time: 338.694501 + - instance: 331 + bogo-ops: 5091 + bogo-ops-per-second-usr-sys-time: 340.621537 + bogo-ops-per-second-real-time: 340.622112 + - instance: 332 + bogo-ops: 5108 + bogo-ops-per-second-usr-sys-time: 341.654255 + bogo-ops-per-second-real-time: 341.654919 + - instance: 333 + bogo-ops: 5103 + bogo-ops-per-second-usr-sys-time: 341.279513 + bogo-ops-per-second-real-time: 341.279670 + - instance: 334 + bogo-ops: 5103 + bogo-ops-per-second-usr-sys-time: 341.349392 + bogo-ops-per-second-real-time: 341.347068 + - instance: 335 + bogo-ops: 5091 + bogo-ops-per-second-usr-sys-time: 340.624044 + bogo-ops-per-second-real-time: 340.619460 + - instance: 336 + bogo-ops: 5103 + bogo-ops-per-second-usr-sys-time: 341.421401 + bogo-ops-per-second-real-time: 341.415739 + - instance: 337 + bogo-ops: 5086 + bogo-ops-per-second-usr-sys-time: 340.088633 + bogo-ops-per-second-real-time: 340.086669 + - instance: 338 + bogo-ops: 5097 + bogo-ops-per-second-usr-sys-time: 340.912078 + bogo-ops-per-second-real-time: 340.910569 + - instance: 339 + bogo-ops: 5091 + bogo-ops-per-second-usr-sys-time: 340.583459 + bogo-ops-per-second-real-time: 340.585075 + - instance: 340 + bogo-ops: 5103 + bogo-ops-per-second-usr-sys-time: 341.518009 + bogo-ops-per-second-real-time: 341.449443 + - instance: 341 + bogo-ops: 5091 + bogo-ops-per-second-usr-sys-time: 340.514208 + bogo-ops-per-second-real-time: 340.511385 + - instance: 342 + bogo-ops: 5086 + bogo-ops-per-second-usr-sys-time: 340.315466 + bogo-ops-per-second-real-time: 340.302702 + - instance: 343 + bogo-ops: 5063 + bogo-ops-per-second-usr-sys-time: 338.893403 + bogo-ops-per-second-real-time: 338.894874 + - instance: 344 + bogo-ops: 5058 + bogo-ops-per-second-usr-sys-time: 338.568924 + bogo-ops-per-second-real-time: 338.569771 + - instance: 345 + bogo-ops: 5108 + bogo-ops-per-second-usr-sys-time: 341.615023 + bogo-ops-per-second-real-time: 341.595373 + - instance: 346 + bogo-ops: 5086 + bogo-ops-per-second-usr-sys-time: 340.250216 + bogo-ops-per-second-real-time: 340.248592 + - instance: 347 + bogo-ops: 5063 + bogo-ops-per-second-usr-sys-time: 338.782515 + bogo-ops-per-second-real-time: 338.780706 + - instance: 348 + bogo-ops: 5063 + bogo-ops-per-second-usr-sys-time: 338.888526 + bogo-ops-per-second-real-time: 338.888585 + - instance: 349 + bogo-ops: 5086 + bogo-ops-per-second-usr-sys-time: 340.464751 + bogo-ops-per-second-real-time: 340.463744 + - instance: 350 + bogo-ops: 5069 + bogo-ops-per-second-usr-sys-time: 338.984955 + bogo-ops-per-second-real-time: 338.983735 + - instance: 351 + bogo-ops: 5063 + bogo-ops-per-second-usr-sys-time: 338.799223 + bogo-ops-per-second-real-time: 338.791369 + - instance: 352 + bogo-ops: 5069 + bogo-ops-per-second-usr-sys-time: 339.230210 + bogo-ops-per-second-real-time: 339.217002 + - instance: 353 + bogo-ops: 5080 + bogo-ops-per-second-usr-sys-time: 340.407077 + bogo-ops-per-second-real-time: 340.063827 + - instance: 354 + bogo-ops: 5091 + bogo-ops-per-second-usr-sys-time: 340.670975 + bogo-ops-per-second-real-time: 340.670042 + - instance: 355 + bogo-ops: 5086 + bogo-ops-per-second-usr-sys-time: 340.506829 + bogo-ops-per-second-real-time: 340.491154 + - instance: 356 + bogo-ops: 5091 + bogo-ops-per-second-usr-sys-time: 340.759791 + bogo-ops-per-second-real-time: 340.728806 + - instance: 357 + bogo-ops: 5091 + bogo-ops-per-second-usr-sys-time: 340.604970 + bogo-ops-per-second-real-time: 340.596087 + - instance: 358 + bogo-ops: 5086 + bogo-ops-per-second-usr-sys-time: 340.529855 + bogo-ops-per-second-real-time: 340.480927 + - instance: 359 + bogo-ops: 5091 + bogo-ops-per-second-usr-sys-time: 340.596060 + bogo-ops-per-second-real-time: 340.575873 + - instance: 360 + bogo-ops: 5091 + bogo-ops-per-second-usr-sys-time: 340.651144 + bogo-ops-per-second-real-time: 340.623981 + - instance: 361 + bogo-ops: 5086 + bogo-ops-per-second-usr-sys-time: 340.450279 + bogo-ops-per-second-real-time: 340.397068 + - instance: 362 + bogo-ops: 5086 + bogo-ops-per-second-usr-sys-time: 340.323436 + bogo-ops-per-second-real-time: 340.323001 + - instance: 363 + bogo-ops: 5080 + bogo-ops-per-second-usr-sys-time: 339.986062 + bogo-ops-per-second-real-time: 339.985521 + - instance: 364 + bogo-ops: 5086 + bogo-ops-per-second-usr-sys-time: 340.522765 + bogo-ops-per-second-real-time: 340.517564 + - instance: 365 + bogo-ops: 5086 + bogo-ops-per-second-usr-sys-time: 340.292650 + bogo-ops-per-second-real-time: 340.295113 + - instance: 366 + bogo-ops: 5069 + bogo-ops-per-second-usr-sys-time: 339.149047 + bogo-ops-per-second-real-time: 339.151500 + - instance: 367 + bogo-ops: 5080 + bogo-ops-per-second-usr-sys-time: 340.067951 + bogo-ops-per-second-real-time: 340.070177 + - instance: 368 + bogo-ops: 5086 + bogo-ops-per-second-usr-sys-time: 340.200441 + bogo-ops-per-second-real-time: 340.198395 + - instance: 369 + bogo-ops: 5063 + bogo-ops-per-second-usr-sys-time: 338.899460 + bogo-ops-per-second-real-time: 338.894204 + - instance: 370 + bogo-ops: 5058 + bogo-ops-per-second-usr-sys-time: 338.522789 + bogo-ops-per-second-real-time: 338.526156 + - instance: 371 + bogo-ops: 5086 + bogo-ops-per-second-usr-sys-time: 340.272980 + bogo-ops-per-second-real-time: 340.274318 + - instance: 372 + bogo-ops: 5069 + bogo-ops-per-second-usr-sys-time: 339.149569 + bogo-ops-per-second-real-time: 339.151035 + - instance: 373 + bogo-ops: 5086 + bogo-ops-per-second-usr-sys-time: 340.423960 + bogo-ops-per-second-real-time: 340.426450 + - instance: 374 + bogo-ops: 6200 + bogo-ops-per-second-usr-sys-time: 415.077795 + bogo-ops-per-second-real-time: 415.072143 + - instance: 375 + bogo-ops: 5086 + bogo-ops-per-second-usr-sys-time: 340.405914 + bogo-ops-per-second-real-time: 340.406025 + - instance: 376 + bogo-ops: 5058 + bogo-ops-per-second-usr-sys-time: 338.474424 + bogo-ops-per-second-real-time: 338.473647 + - instance: 377 + bogo-ops: 5063 + bogo-ops-per-second-usr-sys-time: 339.798635 + bogo-ops-per-second-real-time: 339.027484 + - instance: 378 + bogo-ops: 5091 + bogo-ops-per-second-usr-sys-time: 340.657002 + bogo-ops-per-second-real-time: 340.654640 + - instance: 379 + bogo-ops: 5097 + bogo-ops-per-second-usr-sys-time: 341.243803 + bogo-ops-per-second-real-time: 341.245123 + - instance: 380 + bogo-ops: 5091 + bogo-ops-per-second-usr-sys-time: 340.827659 + bogo-ops-per-second-real-time: 340.829849 + - instance: 381 + bogo-ops: 5097 + bogo-ops-per-second-usr-sys-time: 341.123491 + bogo-ops-per-second-real-time: 341.123094 + - instance: 382 + bogo-ops: 5086 + bogo-ops-per-second-usr-sys-time: 340.457093 + bogo-ops-per-second-real-time: 340.455436 + - instance: 383 + bogo-ops: 5097 + bogo-ops-per-second-usr-sys-time: 341.221255 + bogo-ops-per-second-real-time: 341.220826 + - instance: 384 + bogo-ops: 5091 + bogo-ops-per-second-usr-sys-time: 340.743597 + bogo-ops-per-second-real-time: 340.745003 + - instance: 385 + bogo-ops: 5091 + bogo-ops-per-second-usr-sys-time: 340.776738 + bogo-ops-per-second-real-time: 340.777729 + - instance: 386 + bogo-ops: 5091 + bogo-ops-per-second-usr-sys-time: 340.724464 + bogo-ops-per-second-real-time: 340.722341 + - instance: 387 + bogo-ops: 5058 + bogo-ops-per-second-usr-sys-time: 338.605166 + bogo-ops-per-second-real-time: 338.604799 + - instance: 388 + bogo-ops: 5097 + bogo-ops-per-second-usr-sys-time: 341.408032 + bogo-ops-per-second-real-time: 341.383038 + - instance: 389 + bogo-ops: 5063 + bogo-ops-per-second-usr-sys-time: 338.897169 + bogo-ops-per-second-real-time: 338.898547 + - instance: 390 + bogo-ops: 5103 + bogo-ops-per-second-usr-sys-time: 341.517186 + bogo-ops-per-second-real-time: 341.469326 + - instance: 391 + bogo-ops: 5086 + bogo-ops-per-second-usr-sys-time: 340.490940 + bogo-ops-per-second-real-time: 340.491437 + - instance: 392 + bogo-ops: 5103 + bogo-ops-per-second-usr-sys-time: 341.636696 + bogo-ops-per-second-real-time: 341.630912 + - instance: 393 + bogo-ops: 5058 + bogo-ops-per-second-usr-sys-time: 339.371320 + bogo-ops-per-second-real-time: 338.803857 + - instance: 394 + bogo-ops: 5086 + bogo-ops-per-second-usr-sys-time: 340.575667 + bogo-ops-per-second-real-time: 340.575435 + - instance: 395 + bogo-ops: 5058 + bogo-ops-per-second-usr-sys-time: 338.734172 + bogo-ops-per-second-real-time: 338.733240 + - instance: 396 + bogo-ops: 5058 + bogo-ops-per-second-usr-sys-time: 338.741432 + bogo-ops-per-second-real-time: 338.741336 + - instance: 397 + bogo-ops: 5091 + bogo-ops-per-second-usr-sys-time: 340.818943 + bogo-ops-per-second-real-time: 340.819404 + - instance: 398 + bogo-ops: 5097 + bogo-ops-per-second-usr-sys-time: 341.254221 + bogo-ops-per-second-real-time: 341.253398 + - instance: 399 + bogo-ops: 5086 + bogo-ops-per-second-usr-sys-time: 340.541256 + bogo-ops-per-second-real-time: 340.540106 + - instance: 400 + bogo-ops: 5103 + bogo-ops-per-second-usr-sys-time: 341.630292 + bogo-ops-per-second-real-time: 341.632319 + - instance: 401 + bogo-ops: 5080 + bogo-ops-per-second-usr-sys-time: 340.291422 + bogo-ops-per-second-real-time: 340.289798 + - instance: 402 + bogo-ops: 5103 + bogo-ops-per-second-usr-sys-time: 341.546627 + bogo-ops-per-second-real-time: 341.548310 + - instance: 403 + bogo-ops: 5249 + bogo-ops-per-second-usr-sys-time: 351.481963 + bogo-ops-per-second-real-time: 351.483155 + - instance: 404 + bogo-ops: 5063 + bogo-ops-per-second-usr-sys-time: 339.098567 + bogo-ops-per-second-real-time: 339.098376 + - instance: 405 + bogo-ops: 5086 + bogo-ops-per-second-usr-sys-time: 340.689849 + bogo-ops-per-second-real-time: 340.684925 + - instance: 406 + bogo-ops: 5063 + bogo-ops-per-second-usr-sys-time: 339.356287 + bogo-ops-per-second-real-time: 339.108058 + - instance: 407 + bogo-ops: 5075 + bogo-ops-per-second-usr-sys-time: 339.880811 + bogo-ops-per-second-real-time: 339.880235 + - instance: 408 + bogo-ops: 5069 + bogo-ops-per-second-usr-sys-time: 339.277710 + bogo-ops-per-second-real-time: 339.276076 + - instance: 409 + bogo-ops: 5080 + bogo-ops-per-second-usr-sys-time: 340.315974 + bogo-ops-per-second-real-time: 340.316946 + - instance: 410 + bogo-ops: 5080 + bogo-ops-per-second-usr-sys-time: 340.305943 + bogo-ops-per-second-real-time: 340.307728 + - instance: 411 + bogo-ops: 5063 + bogo-ops-per-second-usr-sys-time: 338.956589 + bogo-ops-per-second-real-time: 338.955994 + - instance: 412 + bogo-ops: 5086 + bogo-ops-per-second-usr-sys-time: 340.675724 + bogo-ops-per-second-real-time: 340.677835 + - instance: 413 + bogo-ops: 5069 + bogo-ops-per-second-usr-sys-time: 339.511473 + bogo-ops-per-second-real-time: 339.329564 + - instance: 414 + bogo-ops: 5063 + bogo-ops-per-second-usr-sys-time: 338.984867 + bogo-ops-per-second-real-time: 338.984135 + - instance: 415 + bogo-ops: 5058 + bogo-ops-per-second-usr-sys-time: 338.828046 + bogo-ops-per-second-real-time: 338.828012 + - instance: 416 + bogo-ops: 5091 + bogo-ops-per-second-usr-sys-time: 340.927355 + bogo-ops-per-second-real-time: 340.929275 + - instance: 417 + bogo-ops: 5086 + bogo-ops-per-second-usr-sys-time: 340.606298 + bogo-ops-per-second-real-time: 340.605751 + - instance: 418 + bogo-ops: 4928 + bogo-ops-per-second-usr-sys-time: 332.198614 + bogo-ops-per-second-real-time: 330.199194 + - instance: 419 + bogo-ops: 5058 + bogo-ops-per-second-usr-sys-time: 338.860348 + bogo-ops-per-second-real-time: 338.858190 + - instance: 420 + bogo-ops: 5080 + bogo-ops-per-second-usr-sys-time: 340.189855 + bogo-ops-per-second-real-time: 340.188058 + - instance: 421 + bogo-ops: 5086 + bogo-ops-per-second-usr-sys-time: 340.823361 + bogo-ops-per-second-real-time: 340.813993 + - instance: 422 + bogo-ops: 5086 + bogo-ops-per-second-usr-sys-time: 340.652381 + bogo-ops-per-second-real-time: 340.651521 + - instance: 423 + bogo-ops: 5069 + bogo-ops-per-second-usr-sys-time: 339.630376 + bogo-ops-per-second-real-time: 339.630016 + - instance: 424 + bogo-ops: 5086 + bogo-ops-per-second-usr-sys-time: 340.623270 + bogo-ops-per-second-real-time: 340.621876 + - instance: 425 + bogo-ops: 5080 + bogo-ops-per-second-usr-sys-time: 340.272822 + bogo-ops-per-second-real-time: 340.274831 + - instance: 426 + bogo-ops: 5080 + bogo-ops-per-second-usr-sys-time: 340.204732 + bogo-ops-per-second-real-time: 340.202876 + - instance: 427 + bogo-ops: 5058 + bogo-ops-per-second-usr-sys-time: 338.715776 + bogo-ops-per-second-real-time: 338.711840 + - instance: 428 + bogo-ops: 5091 + bogo-ops-per-second-usr-sys-time: 341.014339 + bogo-ops-per-second-real-time: 341.013346 + - instance: 429 + bogo-ops: 5058 + bogo-ops-per-second-usr-sys-time: 338.894563 + bogo-ops-per-second-real-time: 338.896200 + - instance: 430 + bogo-ops: 5052 + bogo-ops-per-second-usr-sys-time: 338.488425 + bogo-ops-per-second-real-time: 338.486153 + - instance: 431 + bogo-ops: 5058 + bogo-ops-per-second-usr-sys-time: 338.888682 + bogo-ops-per-second-real-time: 338.883982 + - instance: 432 + bogo-ops: 5052 + bogo-ops-per-second-usr-sys-time: 338.608712 + bogo-ops-per-second-real-time: 338.607823 + - instance: 433 + bogo-ops: 5058 + bogo-ops-per-second-usr-sys-time: 338.852312 + bogo-ops-per-second-real-time: 338.852312 + - instance: 434 + bogo-ops: 5046 + bogo-ops-per-second-usr-sys-time: 338.122214 + bogo-ops-per-second-real-time: 338.123391 + - instance: 435 + bogo-ops: 5097 + bogo-ops-per-second-usr-sys-time: 341.566170 + bogo-ops-per-second-real-time: 341.538539 + - instance: 436 + bogo-ops: 5080 + bogo-ops-per-second-usr-sys-time: 340.487686 + bogo-ops-per-second-real-time: 340.489961 + - instance: 437 + bogo-ops: 5075 + bogo-ops-per-second-usr-sys-time: 340.117431 + bogo-ops-per-second-real-time: 340.116146 + - instance: 438 + bogo-ops: 5052 + bogo-ops-per-second-usr-sys-time: 338.636017 + bogo-ops-per-second-real-time: 338.634998 + - instance: 439 + bogo-ops: 5097 + bogo-ops-per-second-usr-sys-time: 341.588718 + bogo-ops-per-second-real-time: 341.561387 + - instance: 440 + bogo-ops: 4450 + bogo-ops-per-second-usr-sys-time: 298.220655 + bogo-ops-per-second-real-time: 298.220357 + - instance: 441 + bogo-ops: 5080 + bogo-ops-per-second-usr-sys-time: 340.441182 + bogo-ops-per-second-real-time: 340.440090 + - instance: 442 + bogo-ops: 5086 + bogo-ops-per-second-usr-sys-time: 340.710436 + bogo-ops-per-second-real-time: 340.709383 + - instance: 443 + bogo-ops: 5097 + bogo-ops-per-second-usr-sys-time: 341.477405 + bogo-ops-per-second-real-time: 341.479926 + - instance: 444 + bogo-ops: 5086 + bogo-ops-per-second-usr-sys-time: 340.735247 + bogo-ops-per-second-real-time: 340.734281 + - instance: 445 + bogo-ops: 5097 + bogo-ops-per-second-usr-sys-time: 341.527309 + bogo-ops-per-second-real-time: 341.526159 + - instance: 446 + bogo-ops: 5063 + bogo-ops-per-second-usr-sys-time: 339.242209 + bogo-ops-per-second-real-time: 339.242212 + - instance: 447 + bogo-ops: 5097 + bogo-ops-per-second-usr-sys-time: 341.629517 + bogo-ops-per-second-real-time: 341.629068 + - instance: 448 + bogo-ops: 5063 + bogo-ops-per-second-usr-sys-time: 339.383903 + bogo-ops-per-second-real-time: 339.383365 + - instance: 449 + bogo-ops: 5075 + bogo-ops-per-second-usr-sys-time: 340.115926 + bogo-ops-per-second-real-time: 340.117379 + - instance: 450 + bogo-ops: 5063 + bogo-ops-per-second-usr-sys-time: 339.303684 + bogo-ops-per-second-real-time: 339.304855 + - instance: 451 + bogo-ops: 5080 + bogo-ops-per-second-usr-sys-time: 340.513316 + bogo-ops-per-second-real-time: 340.513326 + - instance: 452 + bogo-ops: 5091 + bogo-ops-per-second-usr-sys-time: 341.226792 + bogo-ops-per-second-real-time: 341.226610 + - instance: 453 + bogo-ops: 5052 + bogo-ops-per-second-usr-sys-time: 338.512943 + bogo-ops-per-second-real-time: 338.512607 + - instance: 454 + bogo-ops: 5063 + bogo-ops-per-second-usr-sys-time: 339.488516 + bogo-ops-per-second-real-time: 339.439956 + - instance: 455 + bogo-ops: 5080 + bogo-ops-per-second-usr-sys-time: 340.374918 + bogo-ops-per-second-real-time: 340.374562 + - instance: 456 + bogo-ops: 5052 + bogo-ops-per-second-usr-sys-time: 338.701493 + bogo-ops-per-second-real-time: 338.700846 + - instance: 457 + bogo-ops: 5086 + bogo-ops-per-second-usr-sys-time: 340.882251 + bogo-ops-per-second-real-time: 340.882037 + - instance: 458 + bogo-ops: 5058 + bogo-ops-per-second-usr-sys-time: 338.875627 + bogo-ops-per-second-real-time: 338.877010 + - instance: 459 + bogo-ops: 5080 + bogo-ops-per-second-usr-sys-time: 340.490812 + bogo-ops-per-second-real-time: 340.488758 + - instance: 460 + bogo-ops: 5080 + bogo-ops-per-second-usr-sys-time: 340.498823 + bogo-ops-per-second-real-time: 340.496338 + - instance: 461 + bogo-ops: 5086 + bogo-ops-per-second-usr-sys-time: 340.875191 + bogo-ops-per-second-real-time: 340.874765 + - instance: 462 + bogo-ops: 5080 + bogo-ops-per-second-usr-sys-time: 340.660964 + bogo-ops-per-second-real-time: 340.661632 + - instance: 463 + bogo-ops: 5052 + bogo-ops-per-second-usr-sys-time: 338.748618 + bogo-ops-per-second-real-time: 338.748327 + - instance: 464 + bogo-ops: 5080 + bogo-ops-per-second-usr-sys-time: 340.638350 + bogo-ops-per-second-real-time: 340.638099 + - instance: 465 + bogo-ops: 5080 + bogo-ops-per-second-usr-sys-time: 340.523861 + bogo-ops-per-second-real-time: 340.524303 + - instance: 466 + bogo-ops: 5080 + bogo-ops-per-second-usr-sys-time: 340.572533 + bogo-ops-per-second-real-time: 340.571460 + - instance: 467 + bogo-ops: 5069 + bogo-ops-per-second-usr-sys-time: 339.934232 + bogo-ops-per-second-real-time: 339.934501 + - instance: 468 + bogo-ops: 5080 + bogo-ops-per-second-usr-sys-time: 340.704008 + bogo-ops-per-second-real-time: 340.703799 + - instance: 469 + bogo-ops: 5075 + bogo-ops-per-second-usr-sys-time: 340.188495 + bogo-ops-per-second-real-time: 340.190082 + - instance: 470 + bogo-ops: 5080 + bogo-ops-per-second-usr-sys-time: 340.460667 + bogo-ops-per-second-real-time: 340.459668 + - instance: 471 + bogo-ops: 5075 + bogo-ops-per-second-usr-sys-time: 340.204230 + bogo-ops-per-second-real-time: 340.203500 + - instance: 472 + bogo-ops: 5080 + bogo-ops-per-second-usr-sys-time: 340.458021 + bogo-ops-per-second-real-time: 340.458210 + - instance: 473 + bogo-ops: 5063 + bogo-ops-per-second-usr-sys-time: 339.485260 + bogo-ops-per-second-real-time: 339.484952 + - instance: 474 + bogo-ops: 5058 + bogo-ops-per-second-usr-sys-time: 339.034084 + bogo-ops-per-second-real-time: 339.033195 + - instance: 475 + bogo-ops: 5097 + bogo-ops-per-second-usr-sys-time: 341.638745 + bogo-ops-per-second-real-time: 341.631121 + - instance: 476 + bogo-ops: 5052 + bogo-ops-per-second-usr-sys-time: 338.851656 + bogo-ops-per-second-real-time: 338.850178 + - instance: 477 + bogo-ops: 5080 + bogo-ops-per-second-usr-sys-time: 340.498754 + bogo-ops-per-second-real-time: 340.500060 + - instance: 478 + bogo-ops: 5058 + bogo-ops-per-second-usr-sys-time: 339.159323 + bogo-ops-per-second-real-time: 339.157581 + - instance: 479 + bogo-ops: 5091 + bogo-ops-per-second-usr-sys-time: 341.465800 + bogo-ops-per-second-real-time: 341.459048 + - instance: 480 + bogo-ops: 5091 + bogo-ops-per-second-usr-sys-time: 341.421923 + bogo-ops-per-second-real-time: 341.421808 + - instance: 481 + bogo-ops: 5091 + bogo-ops-per-second-usr-sys-time: 341.326057 + bogo-ops-per-second-real-time: 341.325733 + - instance: 482 + bogo-ops: 5086 + bogo-ops-per-second-usr-sys-time: 341.177143 + bogo-ops-per-second-real-time: 341.178681 + - instance: 483 + bogo-ops: 5080 + bogo-ops-per-second-usr-sys-time: 340.633484 + bogo-ops-per-second-real-time: 340.635605 + - instance: 484 + bogo-ops: 5091 + bogo-ops-per-second-usr-sys-time: 341.208565 + bogo-ops-per-second-real-time: 341.209887 + - instance: 485 + bogo-ops: 5091 + bogo-ops-per-second-usr-sys-time: 341.315417 + bogo-ops-per-second-real-time: 341.315220 + - instance: 486 + bogo-ops: 5052 + bogo-ops-per-second-usr-sys-time: 338.677788 + bogo-ops-per-second-real-time: 338.677752 + - instance: 487 + bogo-ops: 5091 + bogo-ops-per-second-usr-sys-time: 341.378562 + bogo-ops-per-second-real-time: 341.375979 + - instance: 488 + bogo-ops: 5052 + bogo-ops-per-second-usr-sys-time: 338.763746 + bogo-ops-per-second-real-time: 338.762776 + - instance: 489 + bogo-ops: 5041 + bogo-ops-per-second-usr-sys-time: 338.169336 + bogo-ops-per-second-real-time: 338.167680 + - instance: 490 + bogo-ops: 5052 + bogo-ops-per-second-usr-sys-time: 338.765722 + bogo-ops-per-second-real-time: 338.765825 + - instance: 491 + bogo-ops: 5046 + bogo-ops-per-second-usr-sys-time: 338.513452 + bogo-ops-per-second-real-time: 338.490770 + - instance: 492 + bogo-ops: 5075 + bogo-ops-per-second-usr-sys-time: 340.445800 + bogo-ops-per-second-real-time: 340.444945 + - instance: 493 + bogo-ops: 5046 + bogo-ops-per-second-usr-sys-time: 338.358668 + bogo-ops-per-second-real-time: 338.316526 + - instance: 494 + bogo-ops: 5075 + bogo-ops-per-second-usr-sys-time: 340.303988 + bogo-ops-per-second-real-time: 340.301421 + - instance: 495 + bogo-ops: 5080 + bogo-ops-per-second-usr-sys-time: 340.771293 + bogo-ops-per-second-real-time: 340.770603 + - instance: 496 + bogo-ops: 5063 + bogo-ops-per-second-usr-sys-time: 339.561762 + bogo-ops-per-second-real-time: 339.555363 + - instance: 497 + bogo-ops: 5052 + bogo-ops-per-second-usr-sys-time: 338.748822 + bogo-ops-per-second-real-time: 338.748814 + - instance: 498 + bogo-ops: 5052 + bogo-ops-per-second-usr-sys-time: 338.910259 + bogo-ops-per-second-real-time: 338.910336 + - instance: 499 + bogo-ops: 5075 + bogo-ops-per-second-usr-sys-time: 340.281696 + bogo-ops-per-second-real-time: 340.281554 + - instance: 500 + bogo-ops: 5069 + bogo-ops-per-second-usr-sys-time: 340.096666 + bogo-ops-per-second-real-time: 340.095842 + - instance: 501 + bogo-ops: 5075 + bogo-ops-per-second-usr-sys-time: 340.545037 + bogo-ops-per-second-real-time: 340.497569 + - instance: 502 + bogo-ops: 5091 + bogo-ops-per-second-usr-sys-time: 341.486208 + bogo-ops-per-second-real-time: 341.486739 + - instance: 503 + bogo-ops: 5069 + bogo-ops-per-second-usr-sys-time: 340.148037 + bogo-ops-per-second-real-time: 340.148763 + - instance: 504 + bogo-ops: 5091 + bogo-ops-per-second-usr-sys-time: 341.554824 + bogo-ops-per-second-real-time: 341.553974 + - instance: 505 + bogo-ops: 5091 + bogo-ops-per-second-usr-sys-time: 341.497203 + bogo-ops-per-second-real-time: 341.495554 + - instance: 506 + bogo-ops: 5091 + bogo-ops-per-second-usr-sys-time: 341.498417 + bogo-ops-per-second-real-time: 341.497263 + - instance: 507 + bogo-ops: 5091 + bogo-ops-per-second-usr-sys-time: 341.601164 + bogo-ops-per-second-real-time: 341.600172 + - instance: 508 + bogo-ops: 5075 + bogo-ops-per-second-usr-sys-time: 340.606953 + bogo-ops-per-second-real-time: 340.578128 + - instance: 509 + bogo-ops: 5283 + bogo-ops-per-second-usr-sys-time: 354.288745 + bogo-ops-per-second-real-time: 354.289118 + - instance: 510 + bogo-ops: 5052 + bogo-ops-per-second-usr-sys-time: 339.078335 + bogo-ops-per-second-real-time: 339.076276 + - instance: 511 + bogo-ops: 5075 + bogo-ops-per-second-usr-sys-time: 340.528676 + bogo-ops-per-second-real-time: 340.528253 + - instance: 512 + bogo-ops: 5080 + bogo-ops-per-second-usr-sys-time: 341.227969 + bogo-ops-per-second-real-time: 340.958216 + - instance: 513 + bogo-ops: 5075 + bogo-ops-per-second-usr-sys-time: 340.482344 + bogo-ops-per-second-real-time: 340.481191 + - instance: 514 + bogo-ops: 5080 + bogo-ops-per-second-usr-sys-time: 340.819487 + bogo-ops-per-second-real-time: 340.819579 + - instance: 515 + bogo-ops: 5063 + bogo-ops-per-second-usr-sys-time: 339.557435 + bogo-ops-per-second-real-time: 339.557100 + - instance: 516 + bogo-ops: 5052 + bogo-ops-per-second-usr-sys-time: 338.799981 + bogo-ops-per-second-real-time: 338.798974 + - instance: 517 + bogo-ops: 5052 + bogo-ops-per-second-usr-sys-time: 338.838202 + bogo-ops-per-second-real-time: 338.838653 + - instance: 518 + bogo-ops: 5075 + bogo-ops-per-second-usr-sys-time: 340.571661 + bogo-ops-per-second-real-time: 340.571289 + - instance: 519 + bogo-ops: 5080 + bogo-ops-per-second-usr-sys-time: 340.906628 + bogo-ops-per-second-real-time: 340.903293 + - instance: 520 + bogo-ops: 5080 + bogo-ops-per-second-usr-sys-time: 340.674329 + bogo-ops-per-second-real-time: 340.676398 + - instance: 521 + bogo-ops: 5058 + bogo-ops-per-second-usr-sys-time: 339.433632 + bogo-ops-per-second-real-time: 339.434636 + - instance: 522 + bogo-ops: 5075 + bogo-ops-per-second-usr-sys-time: 340.684922 + bogo-ops-per-second-real-time: 340.616598 + - instance: 523 + bogo-ops: 5080 + bogo-ops-per-second-usr-sys-time: 340.759361 + bogo-ops-per-second-real-time: 340.758140 + - instance: 524 + bogo-ops: 5075 + bogo-ops-per-second-usr-sys-time: 340.628305 + bogo-ops-per-second-real-time: 340.600526 + - instance: 525 + bogo-ops: 5075 + bogo-ops-per-second-usr-sys-time: 340.537428 + bogo-ops-per-second-real-time: 340.536844 + - instance: 526 + bogo-ops: 5052 + bogo-ops-per-second-usr-sys-time: 339.026682 + bogo-ops-per-second-real-time: 339.026349 + - instance: 527 + bogo-ops: 5080 + bogo-ops-per-second-usr-sys-time: 340.859667 + bogo-ops-per-second-real-time: 340.859566 + - instance: 528 + bogo-ops: 5052 + bogo-ops-per-second-usr-sys-time: 339.138382 + bogo-ops-per-second-real-time: 339.137600 + - instance: 529 + bogo-ops: 5075 + bogo-ops-per-second-usr-sys-time: 340.687690 + bogo-ops-per-second-real-time: 340.687819 + - instance: 530 + bogo-ops: 5075 + bogo-ops-per-second-usr-sys-time: 340.629951 + bogo-ops-per-second-real-time: 340.631615 + - instance: 531 + bogo-ops: 5052 + bogo-ops-per-second-usr-sys-time: 339.059310 + bogo-ops-per-second-real-time: 339.057889 + - instance: 532 + bogo-ops: 5086 + bogo-ops-per-second-usr-sys-time: 341.625740 + bogo-ops-per-second-real-time: 341.467436 + - instance: 533 + bogo-ops: 5058 + bogo-ops-per-second-usr-sys-time: 339.553627 + bogo-ops-per-second-real-time: 339.555631 + - instance: 534 + bogo-ops: 5058 + bogo-ops-per-second-usr-sys-time: 339.609758 + bogo-ops-per-second-real-time: 339.581187 + - instance: 535 + bogo-ops: 5046 + bogo-ops-per-second-usr-sys-time: 338.745792 + bogo-ops-per-second-real-time: 338.745840 + - instance: 536 + bogo-ops: 5046 + bogo-ops-per-second-usr-sys-time: 338.752546 + bogo-ops-per-second-real-time: 338.750671 + - instance: 537 + bogo-ops: 5091 + bogo-ops-per-second-usr-sys-time: 341.562455 + bogo-ops-per-second-real-time: 341.553072 + - instance: 538 + bogo-ops: 5058 + bogo-ops-per-second-usr-sys-time: 340.151060 + bogo-ops-per-second-real-time: 339.447100 + - instance: 539 + bogo-ops: 5052 + bogo-ops-per-second-usr-sys-time: 339.604842 + bogo-ops-per-second-real-time: 339.101297 + - instance: 540 + bogo-ops: 5069 + bogo-ops-per-second-usr-sys-time: 340.260260 + bogo-ops-per-second-real-time: 340.260049 + - instance: 541 + bogo-ops: 5075 + bogo-ops-per-second-usr-sys-time: 340.749886 + bogo-ops-per-second-real-time: 340.738793 + - instance: 542 + bogo-ops: 5058 + bogo-ops-per-second-usr-sys-time: 340.349916 + bogo-ops-per-second-real-time: 339.659114 + - instance: 543 + bogo-ops: 5080 + bogo-ops-per-second-usr-sys-time: 340.854773 + bogo-ops-per-second-real-time: 340.853459 + - instance: 544 + bogo-ops: 5069 + bogo-ops-per-second-usr-sys-time: 340.134274 + bogo-ops-per-second-real-time: 340.134799 + - instance: 545 + bogo-ops: 5075 + bogo-ops-per-second-usr-sys-time: 340.739843 + bogo-ops-per-second-real-time: 340.740370 + - instance: 546 + bogo-ops: 5069 + bogo-ops-per-second-usr-sys-time: 340.271498 + bogo-ops-per-second-real-time: 340.198869 + - instance: 547 + bogo-ops: 5075 + bogo-ops-per-second-usr-sys-time: 340.571570 + bogo-ops-per-second-real-time: 340.571431 + - instance: 548 + bogo-ops: 5041 + bogo-ops-per-second-usr-sys-time: 340.288743 + bogo-ops-per-second-real-time: 338.558722 + - instance: 549 + bogo-ops: 5041 + bogo-ops-per-second-usr-sys-time: 338.301918 + bogo-ops-per-second-real-time: 338.302452 + - instance: 550 + bogo-ops: 5024 + bogo-ops-per-second-usr-sys-time: 339.107887 + bogo-ops-per-second-real-time: 337.322963 + - instance: 551 + bogo-ops: 5041 + bogo-ops-per-second-usr-sys-time: 338.333683 + bogo-ops-per-second-real-time: 338.333498 + - instance: 552 + bogo-ops: 5069 + bogo-ops-per-second-usr-sys-time: 340.466335 + bogo-ops-per-second-real-time: 340.466205 + - instance: 553 + bogo-ops: 5041 + bogo-ops-per-second-usr-sys-time: 338.302803 + bogo-ops-per-second-real-time: 338.294360 + - instance: 554 + bogo-ops: 5075 + bogo-ops-per-second-usr-sys-time: 340.742771 + bogo-ops-per-second-real-time: 340.744848 + - instance: 555 + bogo-ops: 5080 + bogo-ops-per-second-usr-sys-time: 340.980446 + bogo-ops-per-second-real-time: 340.980036 + - instance: 556 + bogo-ops: 5052 + bogo-ops-per-second-usr-sys-time: 339.207787 + bogo-ops-per-second-real-time: 339.208524 + - instance: 557 + bogo-ops: 4799 + bogo-ops-per-second-usr-sys-time: 340.884940 + bogo-ops-per-second-real-time: 322.114640 + - instance: 558 + bogo-ops: 5046 + bogo-ops-per-second-usr-sys-time: 338.808636 + bogo-ops-per-second-real-time: 338.808994 + - instance: 559 + bogo-ops: 4765 + bogo-ops-per-second-usr-sys-time: 338.694756 + bogo-ops-per-second-real-time: 320.079841 + - instance: 560 + bogo-ops: 5063 + bogo-ops-per-second-usr-sys-time: 340.619803 + bogo-ops-per-second-real-time: 339.953360 + - instance: 561 + bogo-ops: 5046 + bogo-ops-per-second-usr-sys-time: 338.910923 + bogo-ops-per-second-real-time: 338.846004 + - instance: 562 + bogo-ops: 5063 + bogo-ops-per-second-usr-sys-time: 340.613982 + bogo-ops-per-second-real-time: 339.973601 + - instance: 563 + bogo-ops: 4771 + bogo-ops-per-second-usr-sys-time: 339.206104 + bogo-ops-per-second-real-time: 320.486847 + - instance: 564 + bogo-ops: 5069 + bogo-ops-per-second-usr-sys-time: 340.564260 + bogo-ops-per-second-real-time: 340.479972 + - instance: 565 + bogo-ops: 5046 + bogo-ops-per-second-usr-sys-time: 339.206983 + bogo-ops-per-second-real-time: 338.793428 + - instance: 566 + bogo-ops: 5063 + bogo-ops-per-second-usr-sys-time: 340.160826 + bogo-ops-per-second-real-time: 340.161252 + - instance: 567 + bogo-ops: 4771 + bogo-ops-per-second-usr-sys-time: 339.532669 + bogo-ops-per-second-real-time: 320.449870 + - instance: 568 + bogo-ops: 4827 + bogo-ops-per-second-usr-sys-time: 340.435508 + bogo-ops-per-second-real-time: 324.161176 + - instance: 569 + bogo-ops: 4816 + bogo-ops-per-second-usr-sys-time: 340.628773 + bogo-ops-per-second-real-time: 323.533219 + - instance: 570 + bogo-ops: 5041 + bogo-ops-per-second-usr-sys-time: 339.005939 + bogo-ops-per-second-real-time: 338.367290 + - instance: 571 + bogo-ops: 4878 + bogo-ops-per-second-usr-sys-time: 340.442187 + bogo-ops-per-second-real-time: 327.700757 + - instance: 572 + bogo-ops: 5046 + bogo-ops-per-second-usr-sys-time: 338.920711 + bogo-ops-per-second-real-time: 338.918693 + - instance: 573 + bogo-ops: 5069 + bogo-ops-per-second-usr-sys-time: 340.539139 + bogo-ops-per-second-real-time: 340.541303 + - instance: 574 + bogo-ops: 5041 + bogo-ops-per-second-usr-sys-time: 339.026139 + bogo-ops-per-second-real-time: 338.406954 + - instance: 575 + bogo-ops: 5069 + bogo-ops-per-second-usr-sys-time: 340.417290 + bogo-ops-per-second-real-time: 340.394520 + - instance: 576 + bogo-ops: 5086 + bogo-ops-per-second-usr-sys-time: 341.809919 + bogo-ops-per-second-real-time: 341.492182 + - instance: 577 + bogo-ops: 5069 + bogo-ops-per-second-usr-sys-time: 340.436723 + bogo-ops-per-second-real-time: 340.406629 + - instance: 578 + bogo-ops: 5069 + bogo-ops-per-second-usr-sys-time: 340.439444 + bogo-ops-per-second-real-time: 340.439868 + - instance: 579 + bogo-ops: 5063 + bogo-ops-per-second-usr-sys-time: 339.975650 + bogo-ops-per-second-real-time: 339.911161 + - instance: 580 + bogo-ops: 5075 + bogo-ops-per-second-usr-sys-time: 340.919618 + bogo-ops-per-second-real-time: 340.918971 + - instance: 581 + bogo-ops: 5069 + bogo-ops-per-second-usr-sys-time: 340.534540 + bogo-ops-per-second-real-time: 340.534278 + - instance: 582 + bogo-ops: 4973 + bogo-ops-per-second-usr-sys-time: 340.184683 + bogo-ops-per-second-real-time: 334.222715 + - instance: 583 + bogo-ops: 5058 + bogo-ops-per-second-usr-sys-time: 339.683882 + bogo-ops-per-second-real-time: 339.679323 + - instance: 584 + bogo-ops: 5069 + bogo-ops-per-second-usr-sys-time: 340.681337 + bogo-ops-per-second-real-time: 340.648780 + - instance: 585 + bogo-ops: 5069 + bogo-ops-per-second-usr-sys-time: 340.656290 + bogo-ops-per-second-real-time: 340.659113 + - instance: 586 + bogo-ops: 5018 + bogo-ops-per-second-usr-sys-time: 338.678098 + bogo-ops-per-second-real-time: 337.040576 + - instance: 587 + bogo-ops: 5046 + bogo-ops-per-second-usr-sys-time: 338.984235 + bogo-ops-per-second-real-time: 338.984370 + - instance: 588 + bogo-ops: 5069 + bogo-ops-per-second-usr-sys-time: 340.598814 + bogo-ops-per-second-real-time: 340.594029 + - instance: 589 + bogo-ops: 5058 + bogo-ops-per-second-usr-sys-time: 339.937631 + bogo-ops-per-second-real-time: 339.937964 + - instance: 590 + bogo-ops: 5046 + bogo-ops-per-second-usr-sys-time: 340.661770 + bogo-ops-per-second-real-time: 339.057151 + - instance: 591 + bogo-ops: 5046 + bogo-ops-per-second-usr-sys-time: 339.204589 + bogo-ops-per-second-real-time: 339.206524 + - instance: 592 + bogo-ops: 5075 + bogo-ops-per-second-usr-sys-time: 341.108799 + bogo-ops-per-second-real-time: 341.112923 + - instance: 593 + bogo-ops: 5041 + bogo-ops-per-second-usr-sys-time: 338.885266 + bogo-ops-per-second-real-time: 338.706860 + - instance: 594 + bogo-ops: 5080 + bogo-ops-per-second-usr-sys-time: 341.277898 + bogo-ops-per-second-real-time: 341.212942 + - instance: 595 + bogo-ops: 5041 + bogo-ops-per-second-usr-sys-time: 338.950639 + bogo-ops-per-second-real-time: 338.947550 + - instance: 596 + bogo-ops: 5075 + bogo-ops-per-second-usr-sys-time: 341.460796 + bogo-ops-per-second-real-time: 340.989373 + - instance: 597 + bogo-ops: 5041 + bogo-ops-per-second-usr-sys-time: 338.861870 + bogo-ops-per-second-real-time: 338.861286 + - instance: 598 + bogo-ops: 5035 + bogo-ops-per-second-usr-sys-time: 338.814046 + bogo-ops-per-second-real-time: 338.334979 + - instance: 599 + bogo-ops: 5086 + bogo-ops-per-second-usr-sys-time: 341.676460 + bogo-ops-per-second-real-time: 341.671671 + - instance: 600 + bogo-ops: 4799 + bogo-ops-per-second-usr-sys-time: 338.730147 + bogo-ops-per-second-real-time: 322.588054 + - instance: 601 + bogo-ops: 5063 + bogo-ops-per-second-usr-sys-time: 340.360389 + bogo-ops-per-second-real-time: 340.359710 + - instance: 602 + bogo-ops: 5063 + bogo-ops-per-second-usr-sys-time: 340.720363 + bogo-ops-per-second-real-time: 340.245632 + - instance: 603 + bogo-ops: 5069 + bogo-ops-per-second-usr-sys-time: 340.547558 + bogo-ops-per-second-real-time: 340.547500 + - instance: 604 + bogo-ops: 5080 + bogo-ops-per-second-usr-sys-time: 341.491461 + bogo-ops-per-second-real-time: 341.423985 + - instance: 605 + bogo-ops: 5075 + bogo-ops-per-second-usr-sys-time: 340.843738 + bogo-ops-per-second-real-time: 340.843255 + - instance: 606 + bogo-ops: 5046 + bogo-ops-per-second-usr-sys-time: 339.133096 + bogo-ops-per-second-real-time: 339.132848 + - instance: 607 + bogo-ops: 5063 + bogo-ops-per-second-usr-sys-time: 340.771113 + bogo-ops-per-second-real-time: 340.322254 + - instance: 608 + bogo-ops: 5069 + bogo-ops-per-second-usr-sys-time: 340.651391 + bogo-ops-per-second-real-time: 340.653021 + - instance: 609 + bogo-ops: 5075 + bogo-ops-per-second-usr-sys-time: 341.671625 + bogo-ops-per-second-real-time: 341.216745 + - instance: 610 + bogo-ops: 5046 + bogo-ops-per-second-usr-sys-time: 339.074598 + bogo-ops-per-second-real-time: 339.074642 + - instance: 611 + bogo-ops: 5063 + bogo-ops-per-second-usr-sys-time: 340.808320 + bogo-ops-per-second-real-time: 340.355886 + - instance: 612 + bogo-ops: 5041 + bogo-ops-per-second-usr-sys-time: 338.865470 + bogo-ops-per-second-real-time: 338.861932 + - instance: 613 + bogo-ops: 5063 + bogo-ops-per-second-usr-sys-time: 340.405608 + bogo-ops-per-second-real-time: 340.318370 + - instance: 614 + bogo-ops: 5046 + bogo-ops-per-second-usr-sys-time: 339.202058 + bogo-ops-per-second-real-time: 339.203638 + - instance: 615 + bogo-ops: 5035 + bogo-ops-per-second-usr-sys-time: 338.363105 + bogo-ops-per-second-real-time: 338.352890 + - instance: 616 + bogo-ops: 5035 + bogo-ops-per-second-usr-sys-time: 338.918682 + bogo-ops-per-second-real-time: 338.472422 + - instance: 617 + bogo-ops: 5058 + bogo-ops-per-second-usr-sys-time: 340.487085 + bogo-ops-per-second-real-time: 340.037821 + - instance: 618 + bogo-ops: 5035 + bogo-ops-per-second-usr-sys-time: 338.380433 + bogo-ops-per-second-real-time: 338.357454 + - instance: 619 + bogo-ops: 4979 + bogo-ops-per-second-usr-sys-time: 340.074443 + bogo-ops-per-second-real-time: 334.495213 + - instance: 620 + bogo-ops: 5075 + bogo-ops-per-second-usr-sys-time: 341.699414 + bogo-ops-per-second-real-time: 341.195316 + - instance: 621 + bogo-ops: 5069 + bogo-ops-per-second-usr-sys-time: 340.681360 + bogo-ops-per-second-real-time: 340.649075 + - instance: 622 + bogo-ops: 5063 + bogo-ops-per-second-usr-sys-time: 340.401694 + bogo-ops-per-second-real-time: 340.402228 + - instance: 623 + bogo-ops: 4771 + bogo-ops-per-second-usr-sys-time: 340.221166 + bogo-ops-per-second-real-time: 320.867601 + - instance: 624 + bogo-ops: 5058 + bogo-ops-per-second-usr-sys-time: 340.638886 + bogo-ops-per-second-real-time: 340.197053 + - instance: 625 + bogo-ops: 5069 + bogo-ops-per-second-usr-sys-time: 341.677941 + bogo-ops-per-second-real-time: 340.944143 + - instance: 626 + bogo-ops: 5063 + bogo-ops-per-second-usr-sys-time: 340.883812 + bogo-ops-per-second-real-time: 340.460235 + - instance: 627 + bogo-ops: 5069 + bogo-ops-per-second-usr-sys-time: 340.866054 + bogo-ops-per-second-real-time: 340.866649 + - instance: 628 + bogo-ops: 4934 + bogo-ops-per-second-usr-sys-time: 336.184222 + bogo-ops-per-second-real-time: 331.771852 + - instance: 629 + bogo-ops: 5069 + bogo-ops-per-second-usr-sys-time: 340.692809 + bogo-ops-per-second-real-time: 340.694683 + - instance: 630 + bogo-ops: 5058 + bogo-ops-per-second-usr-sys-time: 340.335740 + bogo-ops-per-second-real-time: 340.283799 + - instance: 631 + bogo-ops: 5052 + bogo-ops-per-second-usr-sys-time: 339.675900 + bogo-ops-per-second-real-time: 339.674264 + - instance: 632 + bogo-ops: 5063 + bogo-ops-per-second-usr-sys-time: 340.482800 + bogo-ops-per-second-real-time: 340.481846 + - instance: 633 + bogo-ops: 5069 + bogo-ops-per-second-usr-sys-time: 341.393815 + bogo-ops-per-second-real-time: 341.008808 + - instance: 634 + bogo-ops: 5046 + bogo-ops-per-second-usr-sys-time: 339.154614 + bogo-ops-per-second-real-time: 339.153885 + - instance: 635 + bogo-ops: 5041 + bogo-ops-per-second-usr-sys-time: 339.333589 + bogo-ops-per-second-real-time: 338.961292 + - instance: 636 + bogo-ops: 5041 + bogo-ops-per-second-usr-sys-time: 339.024292 + bogo-ops-per-second-real-time: 339.023241 + - instance: 637 + bogo-ops: 5075 + bogo-ops-per-second-usr-sys-time: 341.630799 + bogo-ops-per-second-real-time: 341.245567 + - instance: 638 + bogo-ops: 5035 + bogo-ops-per-second-usr-sys-time: 338.910127 + bogo-ops-per-second-real-time: 338.614314 + - instance: 639 + bogo-ops: 5041 + bogo-ops-per-second-usr-sys-time: 338.888592 + bogo-ops-per-second-real-time: 338.888779 + +... diff --git a/hwbench/tests/parsing/stressng/v02103b/output b/hwbench/tests/parsing/stressng/v02103b/output new file mode 100644 index 00000000..e79fe5c6 --- /dev/null +++ b/hwbench/tests/parsing/stressng/v02103b/output @@ -0,0 +1,5 @@ +{ + "bogo ops/s": 217929.51, + "effective_runtime": 14.95 +} + diff --git a/hwbench/tests/parsing/stressng/v02103b/stderr b/hwbench/tests/parsing/stressng/v02103b/stderr new file mode 100644 index 00000000..e69de29b diff --git a/hwbench/tests/parsing/stressng/v02103b/stdout b/hwbench/tests/parsing/stressng/v02103b/stdout new file mode 100644 index 00000000..41755f55 --- /dev/null +++ b/hwbench/tests/parsing/stressng/v02103b/stdout @@ -0,0 +1,11 @@ +stress-ng: info: [205686] setting to a 15 secs run per stressor +stress-ng: info: [205686] dispatching hogs: 640 cpu +stress-ng: info: [205686] note: boost is disabled and this may impact top performance; setting /sys/devices/system/cpu/cpufreq/boost to 1 may improve performance. +stress-ng: metrc: [205686] stressor bogo ops real time usr time sys time bogo ops/s bogo ops/s CPU used per RSS Max +stress-ng: metrc: [205686] (secs) (secs) (secs) (real time) (usr+sys time) instance (%) (KB) +stress-ng: metrc: [205686] cpu 3257280 14.95 9544.59 0.38 217929.51 341.26 99.78 0 +stress-ng: info: [205686] skipped: 0 +stress-ng: info: [205686] passed: 640: cpu (640) +stress-ng: info: [205686] failed: 0 +stress-ng: info: [205686] metrics untrustworthy: 0 +stress-ng: info: [205686] successful run completed in 15.02 secs diff --git a/hwbench/tests/parsing/stressng/v02103b/version b/hwbench/tests/parsing/stressng/v02103b/version new file mode 100644 index 00000000..26115cdf --- /dev/null +++ b/hwbench/tests/parsing/stressng/v02103b/version @@ -0,0 +1 @@ +0.21.03 diff --git a/hwbench/tests/parsing/stressng/v02103b/version-stderr b/hwbench/tests/parsing/stressng/v02103b/version-stderr new file mode 100644 index 00000000..e69de29b diff --git a/hwbench/tests/parsing/stressng/v02103b/version-stdout b/hwbench/tests/parsing/stressng/v02103b/version-stdout new file mode 100644 index 00000000..8141d4c0 --- /dev/null +++ b/hwbench/tests/parsing/stressng/v02103b/version-stdout @@ -0,0 +1 @@ +stress-ng, version 0.21.03 (gcc 14.3.1, x86_64 Linux 7.2.0-rc2) diff --git a/hwbench/utils/external.py b/hwbench/utils/external.py index deffff76..64d078c7 100644 --- a/hwbench/utils/external.py +++ b/hwbench/utils/external.py @@ -33,9 +33,18 @@ def parse_cmd(self, stdout: bytes, stderr: bytes): def parse_version(self, stdout: bytes, stderr: bytes) -> str: return "" + @property + def output_basename(self) -> str: + """Basename of this run's output files (stdout/stderr/version-*). + + Defaults to the command name; benchmarks override it to stay unique + across the many runs a single job expands into (see ExternalBench). + """ + return self.name + def _write_output(self, name: str, content: bytes): if len(content) > 0: - self.out_dir.joinpath(f"{self.name}-{name}").write_bytes(content) + self.out_dir.joinpath(f"{self.output_basename}-{name}").write_bytes(content) def run(self): """Returns the output of parse_cmd (a json-able type)""" diff --git a/pyproject.toml b/pyproject.toml index c868236d..577ba1bc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -18,6 +18,7 @@ dependencies = [ "redfish", "packaging", "pyudev>=0.24.3", + "pyyaml>=6.0", ] [project.urls] @@ -37,6 +38,7 @@ dev = [ "pytest>=8.3.3", "ruff>=0.7.1", "types-cachetools>=5.5.0.20240820", + "types-pyyaml>=6.0.12", "uv>=0.4.27", ] @@ -77,6 +79,15 @@ ignore = [ "ISC001", ] +[tool.ruff.lint.isort] +# PyYAML installs the top-level module "yaml"; ruff cannot infer it is +# third-party unless the project deps are on its path. The isolated +# `uv tool run ruff` used by `make check` groups it with the first-party +# imports, while CI (deps installed) treats it as third-party -- producing +# conflicting demands for the stress-ng import block. Pin it so the isort +# classification, and thus the layout, is identical in both. +known-third-party = ["yaml"] + [[tool.mypy.overrides]] module = ["pyudev.*"] ignore_missing_imports = true diff --git a/uv.lock b/uv.lock index d6d4794b..5cf1c3fb 100644 --- a/uv.lock +++ b/uv.lock @@ -1,5 +1,5 @@ version = 1 -revision = 2 +revision = 3 requires-python = ">=3.12" [[package]] @@ -219,6 +219,7 @@ dependencies = [ { name = "cachetools" }, { name = "packaging" }, { name = "pyudev" }, + { name = "pyyaml" }, { name = "redfish" }, ] @@ -242,6 +243,7 @@ dev = [ { name = "pytest" }, { name = "ruff" }, { name = "types-cachetools" }, + { name = "types-pyyaml" }, { name = "uv" }, ] @@ -255,6 +257,7 @@ requires-dist = [ { name = "pillow", marker = "extra == 'graph-ci'", specifier = ">=11.0.0" }, { name = "pycairo", marker = "extra == 'graph'" }, { name = "pyudev", specifier = ">=0.24.3" }, + { name = "pyyaml", specifier = ">=6.0" }, { name = "redfish" }, ] provides-extras = ["graph-ci", "graph"] @@ -266,6 +269,7 @@ dev = [ { name = "pytest", specifier = ">=8.3.3" }, { name = "ruff", specifier = ">=0.7.1" }, { name = "types-cachetools", specifier = ">=5.5.0.20240820" }, + { name = "types-pyyaml", specifier = ">=6.0.12" }, { name = "uv", specifier = ">=0.4.27" }, ] @@ -725,6 +729,52 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/2a/51/3dc0cd6498b24dea3cdeaed648568e3ca7454d41334d840b114156d7479f/pyudev-0.24.4-py3-none-any.whl", hash = "sha256:b3b6b01c68e6fc628428cc45ff3fe6c277afbb5d96507f14473ddb4a6b959e00", size = 62784, upload-time = "2025-10-08T17:26:57.664Z" }, ] +[[package]] +name = "pyyaml" +version = "6.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/33/422b98d2195232ca1826284a76852ad5a86fe23e31b009c9886b2d0fb8b2/pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196", size = 182063, upload-time = "2025-09-25T21:32:11.445Z" }, + { url = "https://files.pythonhosted.org/packages/89/a0/6cf41a19a1f2f3feab0e9c0b74134aa2ce6849093d5517a0c550fe37a648/pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0", size = 173973, upload-time = "2025-09-25T21:32:12.492Z" }, + { url = "https://files.pythonhosted.org/packages/ed/23/7a778b6bd0b9a8039df8b1b1d80e2e2ad78aa04171592c8a5c43a56a6af4/pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28", size = 775116, upload-time = "2025-09-25T21:32:13.652Z" }, + { url = "https://files.pythonhosted.org/packages/65/30/d7353c338e12baef4ecc1b09e877c1970bd3382789c159b4f89d6a70dc09/pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c", size = 844011, upload-time = "2025-09-25T21:32:15.21Z" }, + { url = "https://files.pythonhosted.org/packages/8b/9d/b3589d3877982d4f2329302ef98a8026e7f4443c765c46cfecc8858c6b4b/pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc", size = 807870, upload-time = "2025-09-25T21:32:16.431Z" }, + { url = "https://files.pythonhosted.org/packages/05/c0/b3be26a015601b822b97d9149ff8cb5ead58c66f981e04fedf4e762f4bd4/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e", size = 761089, upload-time = "2025-09-25T21:32:17.56Z" }, + { url = "https://files.pythonhosted.org/packages/be/8e/98435a21d1d4b46590d5459a22d88128103f8da4c2d4cb8f14f2a96504e1/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea", size = 790181, upload-time = "2025-09-25T21:32:18.834Z" }, + { url = "https://files.pythonhosted.org/packages/74/93/7baea19427dcfbe1e5a372d81473250b379f04b1bd3c4c5ff825e2327202/pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5", size = 137658, upload-time = "2025-09-25T21:32:20.209Z" }, + { url = "https://files.pythonhosted.org/packages/86/bf/899e81e4cce32febab4fb42bb97dcdf66bc135272882d1987881a4b519e9/pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b", size = 154003, upload-time = "2025-09-25T21:32:21.167Z" }, + { url = "https://files.pythonhosted.org/packages/1a/08/67bd04656199bbb51dbed1439b7f27601dfb576fb864099c7ef0c3e55531/pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd", size = 140344, upload-time = "2025-09-25T21:32:22.617Z" }, + { url = "https://files.pythonhosted.org/packages/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", size = 181669, upload-time = "2025-09-25T21:32:23.673Z" }, + { url = "https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", size = 173252, upload-time = "2025-09-25T21:32:25.149Z" }, + { url = "https://files.pythonhosted.org/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", size = 767081, upload-time = "2025-09-25T21:32:26.575Z" }, + { url = "https://files.pythonhosted.org/packages/49/1e/a55ca81e949270d5d4432fbbd19dfea5321eda7c41a849d443dc92fd1ff7/pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5", size = 841159, upload-time = "2025-09-25T21:32:27.727Z" }, + { url = "https://files.pythonhosted.org/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6", size = 801626, upload-time = "2025-09-25T21:32:28.878Z" }, + { url = "https://files.pythonhosted.org/packages/f9/11/ba845c23988798f40e52ba45f34849aa8a1f2d4af4b798588010792ebad6/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6", size = 753613, upload-time = "2025-09-25T21:32:30.178Z" }, + { url = "https://files.pythonhosted.org/packages/3d/e0/7966e1a7bfc0a45bf0a7fb6b98ea03fc9b8d84fa7f2229e9659680b69ee3/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be", size = 794115, upload-time = "2025-09-25T21:32:31.353Z" }, + { url = "https://files.pythonhosted.org/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", size = 137427, upload-time = "2025-09-25T21:32:32.58Z" }, + { url = "https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", size = 154090, upload-time = "2025-09-25T21:32:33.659Z" }, + { url = "https://files.pythonhosted.org/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246, upload-time = "2025-09-25T21:32:34.663Z" }, + { url = "https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814, upload-time = "2025-09-25T21:32:35.712Z" }, + { url = "https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809, upload-time = "2025-09-25T21:32:36.789Z" }, + { url = "https://files.pythonhosted.org/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454, upload-time = "2025-09-25T21:32:37.966Z" }, + { url = "https://files.pythonhosted.org/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", size = 836355, upload-time = "2025-09-25T21:32:39.178Z" }, + { url = "https://files.pythonhosted.org/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", size = 794175, upload-time = "2025-09-25T21:32:40.865Z" }, + { url = "https://files.pythonhosted.org/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", size = 755228, upload-time = "2025-09-25T21:32:42.084Z" }, + { url = "https://files.pythonhosted.org/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", size = 789194, upload-time = "2025-09-25T21:32:43.362Z" }, + { url = "https://files.pythonhosted.org/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", size = 156429, upload-time = "2025-09-25T21:32:57.844Z" }, + { url = "https://files.pythonhosted.org/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", size = 143912, upload-time = "2025-09-25T21:32:59.247Z" }, + { url = "https://files.pythonhosted.org/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", size = 189108, upload-time = "2025-09-25T21:32:44.377Z" }, + { url = "https://files.pythonhosted.org/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", size = 183641, upload-time = "2025-09-25T21:32:45.407Z" }, + { url = "https://files.pythonhosted.org/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", size = 831901, upload-time = "2025-09-25T21:32:48.83Z" }, + { url = "https://files.pythonhosted.org/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", size = 861132, upload-time = "2025-09-25T21:32:50.149Z" }, + { url = "https://files.pythonhosted.org/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", size = 839261, upload-time = "2025-09-25T21:32:51.808Z" }, + { url = "https://files.pythonhosted.org/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", size = 805272, upload-time = "2025-09-25T21:32:52.941Z" }, + { url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923, upload-time = "2025-09-25T21:32:54.537Z" }, + { url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062, upload-time = "2025-09-25T21:32:55.767Z" }, + { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" }, +] + [[package]] name = "redfish" version = "3.3.4" @@ -825,6 +875,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/98/2d/8d821ed80f6c2c5b427f650bf4dc25b80676ed63d03388e4b637d2557107/types_cachetools-6.2.0.20251022-py3-none-any.whl", hash = "sha256:698eb17b8f16b661b90624708b6915f33dbac2d185db499ed57e4997e7962cad", size = 9341, upload-time = "2025-10-22T03:03:57.036Z" }, ] +[[package]] +name = "types-pyyaml" +version = "6.0.12.20260518" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b8/83/4a1afc3fbfcf5b8d46fc390cd95ed6b0dc9010a265f4e9f46314efffa37a/types_pyyaml-6.0.12.20260518.tar.gz", hash = "sha256:d917f83fb38462550338c1297faedd860b3ec83912b96b1e3d73255f7473e466", size = 17850, upload-time = "2026-05-18T06:01:58.675Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/06/a2/c01db32be2ae7d6a1689972f3c492b149ee4e164b12fdfd9f64b50888215/types_pyyaml-6.0.12.20260518-py3-none-any.whl", hash = "sha256:d2150f75a231c9fe9c7463bd29487d93e60bac90400287351384bc2284eba7cd", size = 20312, upload-time = "2026-05-18T06:01:57.368Z" }, +] + [[package]] name = "typing-extensions" version = "4.15.0"