Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
156 changes: 45 additions & 111 deletions benchmarks/cpp/benchmark_report.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,30 @@
import os
import platform
import argparse
import shutil
import subprocess
import sys
from pathlib import Path
import matplotlib.pyplot as plt
import numpy as np
from matplotlib.ticker import FuncFormatter
from collections import defaultdict
from datetime import datetime

sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
from plot_style import ( # noqa: E402
BAR_EDGE_COLOR,
GROUP_BAR_WIDTH,
GROUP_X,
apply_benchmark_style,
add_compact_legend,
format_markdown_with_prettier,
format_throughput_tick,
save_benchmark_figure,
serializer_offset,
set_grouped_operation_axis,
style_throughput_axis,
)

apply_benchmark_style(plt)

try:
import psutil

Expand Down Expand Up @@ -226,80 +242,16 @@ def load_benchmark_data(json_file):
system_info["CPU Cores (from benchmark)"] = context["num_cpus"]


# === Plotting ===
def format_tps_label(tps):
if tps >= 1e9:
return f"{tps / 1e9:.2f}G"
if tps >= 1e6:
return f"{tps / 1e6:.2f}M"
if tps >= 1e3:
return f"{tps / 1e3:.2f}K"
return f"{tps:.0f}"


def format_tps_tick(tps, _position):
return format_tps_label(tps)


def plot_datatype(ax, datatype, operation):
"""Plot a single datatype/operation throughput comparison."""
if datatype not in data or operation not in data[datatype]:
ax.set_title(f"{datatype} {operation} - No Data")
ax.axis("off")
return

libs = set(data[datatype][operation].keys())
lib_order = [lib for lib in SERIALIZER_ORDER if lib in libs]

times = [data[datatype][operation].get(lib, 0) for lib in lib_order]
throughput = [1e9 / t if t > 0 else 0 for t in times]
colors = [COLORS.get(lib, "#888888") for lib in lib_order]

x = np.arange(len(lib_order))
bars = ax.bar(x, throughput, color=colors, width=0.6)

ax.set_title(f"{operation.capitalize()} Throughput (higher is better)")
ax.set_xticks(x)
ax.set_xticklabels([SERIALIZER_LABELS.get(lib, lib) for lib in lib_order])
ax.set_ylabel("Throughput (ops/sec)")
ax.grid(True, axis="y", linestyle="--", alpha=0.5)
ax.ticklabel_format(style="scientific", axis="y", scilimits=(0, 0))

# Add value labels on bars
for bar, tps_val in zip(bars, throughput):
height = bar.get_height()
ax.annotate(
format_tps_label(tps_val),
xy=(bar.get_x() + bar.get_width() / 2, height),
xytext=(0, 3),
textcoords="offset points",
ha="center",
va="bottom",
fontsize=9,
)
return format_throughput_tick(tps, _position)


# === Create plots ===
plot_images = []
datatypes = ordered_datatypes(data.keys())
operations = ["serialize", "deserialize"]

for datatype in datatypes:
fig, axes = plt.subplots(1, 2, figsize=(12, 5))
for i, op in enumerate(operations):
plot_datatype(axes[i], datatype, op)
fig.suptitle(f"{datatype.capitalize()} Throughput", fontsize=14)
fig.tight_layout(rect=[0, 0, 1, 0.95])
plot_path = os.path.join(output_dir, f"{datatype}.png")
plt.savefig(plot_path, dpi=150)
plot_images.append((datatype, plot_path))
plt.close()

# === Create combined TPS comparison plot ===
non_list_datatypes = [dt for dt in datatypes if not dt.endswith("list")]
list_datatypes = [dt for dt in datatypes if dt.endswith("list")]


def plot_throughput_grid_subplot(ax, datatype):
if datatype not in data:
ax.set_title(f"{format_datatype_table_label(datatype)}\nNo Data")
Expand All @@ -320,41 +272,44 @@ def plot_throughput_grid_subplot(ax, datatype):
return

operations = ["serialize", "deserialize"]
x = np.arange(len(operations))
width = 0.8 / len(available_libs)
x = GROUP_X
for idx, lib in enumerate(available_libs):
times = [data[datatype][operation].get(lib, 0) for operation in operations]
tps = [1e9 / t if t > 0 else 0 for t in times]
offset = (idx - (len(available_libs) - 1) / 2) * width
offset = serializer_offset(idx, len(available_libs))
ax.bar(
x + offset,
tps,
width,
GROUP_BAR_WIDTH,
label=SERIALIZER_LABELS.get(lib, lib),
color=COLORS.get(lib, "#888888"),
edgecolor=BAR_EDGE_COLOR,
linewidth=0.8,
)

ax.set_title(format_datatype_table_label(datatype))
ax.set_xticks(x)
ax.set_xticklabels(["Serialize", "Deserialize"])
ax.grid(True, axis="y", linestyle="--", alpha=0.5)
max_tps = max(
1e9 / data[datatype][operation][lib]
for operation in operations
for lib in available_libs
if data[datatype][operation].get(lib, 0) > 0
)
ax.set_ylim(0, max_tps * 1.12)
ax.set_title(format_datatype_table_label(datatype), pad=8)
set_grouped_operation_axis(ax)
style_throughput_axis(ax)
ax.yaxis.set_major_formatter(FuncFormatter(format_tps_tick))
ax.legend(loc="upper right", fontsize=8, framealpha=0.9)
add_compact_legend(ax)


fig, axes = plt.subplots(2, 3, figsize=(18, 10))
fig, axes = plt.subplots(2, 3, figsize=(16.5, 9.0))
for index, (ax, datatype) in enumerate(zip(axes.flat, DATATYPE_ORDER)):
plot_throughput_grid_subplot(ax, datatype)
if index % 3 == 0:
ax.set_ylabel("Throughput (ops/sec)")
else:
ax.tick_params(axis="y", labelleft=False)
ax.yaxis.get_offset_text().set_visible(False)
fig.suptitle("C++ Serialization Throughput", fontsize=14)
fig.tight_layout()
ax.set_ylabel("Throughput (ops/sec)", labelpad=10)
fig.suptitle("C++ Serialization Throughput", fontsize=15, fontweight="normal", y=0.955)
fig.tight_layout(rect=[0.02, 0.02, 0.995, 0.965], w_pad=1.2, h_pad=1.25)
combined_plot_path = os.path.join(output_dir, "throughput.png")
plt.savefig(combined_plot_path, dpi=150)
plot_images.append(("throughput", combined_plot_path))
save_benchmark_figure(fig, combined_plot_path)
plt.close()

# === Markdown report ===
Expand All @@ -368,35 +323,16 @@ def plot_throughput_grid_subplot(ax, datatype):
"cd ..\n",
"python benchmark_report.py --json-file build/benchmark_results.json --output-dir report\n",
"```\n\n",
"## Benchmark Plot\n\n",
"The plot shows throughput (ops/sec); higher is better.\n\n",
f"![Throughput]({args.plot_prefix}throughput.png)\n\n",
"## Hardware & OS Info\n\n",
"| Key | Value |\n",
"|-----|-------|\n",
]
for k, v in system_info.items():
md_report.append(f"| {k} | {v} |\n")

# Plots section
md_report.append("\n## Benchmark Plots\n")
md_report.append("\nAll class-level plots below show throughput (ops/sec).\n")
plot_images_sorted = sorted(
plot_images,
key=lambda item: (
0 if item[0] == "throughput" else 1,
DATATYPE_ORDER_INDEX.get(item[0], len(DATATYPE_ORDER)),
item[0],
),
)
for datatype, img in plot_images_sorted:
img_filename = os.path.basename(img)
img_path_report = args.plot_prefix + img_filename
plot_title = (
"Throughput"
if datatype == "throughput"
else format_datatype_table_label(datatype)
)
md_report.append(f"\n### {plot_title}\n\n")
md_report.append(f"![{plot_title}]({img_path_report})\n")

# Results table
md_report.append("\n## Benchmark Results\n\n")
md_report.append("### Timing Results (nanoseconds)\n\n")
Expand Down Expand Up @@ -492,9 +428,7 @@ def plot_throughput_grid_subplot(ax, datatype):
with open(report_path, "w", encoding="utf-8") as f:
f.writelines(md_report)

prettier = shutil.which("prettier")
if prettier is not None:
subprocess.run([prettier, "--write", report_path], check=True)
format_markdown_with_prettier(report_path)

print(f"✅ Plots saved in: {output_dir}")
print(f"📄 Markdown report generated at: {report_path}")
18 changes: 17 additions & 1 deletion benchmarks/cpp/run.sh
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ set -e
export ENABLE_FORY_DEBUG_OUTPUT=0
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
cd "$SCRIPT_DIR"
DOCS_DIR="$SCRIPT_DIR/../../docs/benchmarks/cpp"

# Colors for output
RED='\033[0;31m'
Expand All @@ -33,6 +34,7 @@ DATA=""
SERIALIZER=""
DEBUG_BUILD=false
DURATION=""
COPY_DOCS=true

# Parse arguments
usage() {
Expand All @@ -47,6 +49,7 @@ usage() {
echo " Filter benchmark by serializer"
echo " --duration <seconds> Minimum time to run each benchmark (e.g., 10, 30)"
echo " --debug Build with debug symbols and low optimization for profiling"
echo " --no-copy-docs Skip copying report/plots into docs/benchmarks/cpp"
echo " --help Show this help message"
echo ""
echo "Examples:"
Expand Down Expand Up @@ -80,6 +83,10 @@ while [[ $# -gt 0 ]]; do
DEBUG_BUILD=true
shift
;;
--no-copy-docs)
COPY_DOCS=false
shift
;;
--help|-h)
usage
;;
Expand Down Expand Up @@ -205,10 +212,19 @@ if ! python3 -c "import matplotlib" 2>/dev/null; then
fi

python3 benchmark_report.py --json-file build/benchmark_results.json --output-dir report
if [[ "$COPY_DOCS" == true ]]; then
mkdir -p "$DOCS_DIR"
cp report/README.md "$DOCS_DIR/README.md"
cp report/throughput.png "$DOCS_DIR/throughput.png"
echo -e "${GREEN}Copied report and throughput plot to: ${DOCS_DIR}${NC}"
fi
echo ""

echo -e "${GREEN}=== All done! ===${NC}"
echo -e "Report generated at: ${SCRIPT_DIR}/report/REPORT.md"
echo -e "Report generated at: ${SCRIPT_DIR}/report/README.md"
echo -e "Plots saved in: ${SCRIPT_DIR}/report/"
if [[ "$COPY_DOCS" == true ]]; then
echo -e "Docs sync: ${DOCS_DIR}"
fi
echo ""
echo -e "For profiling/flamegraph, run: ${YELLOW}./profile.sh --help${NC}"
4 changes: 2 additions & 2 deletions benchmarks/csharp/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ This runs all benchmark cases and generates:

- `build/benchmark_results.json`
- `report/README.md`
- `report/throughput.png` and per-datatype throughput plots (for example `report/struct.png`)
- `report/throughput.png`

## Run Options

Expand Down Expand Up @@ -79,4 +79,4 @@ Per-case winners vary by payload and operation. The full breakdown is generated

- `benchmarks/csharp/build/benchmark_results.json`
- `benchmarks/csharp/report/README.md`
- `benchmarks/csharp/report/*.png`
- `benchmarks/csharp/report/throughput.png`
Loading
Loading