|
| 1 | +#!/usr/bin/env bash |
| 2 | +# The contents of this file are subject to the terms of the Common Development and |
| 3 | +# Distribution License (the License). You may not use this file except in compliance with the |
| 4 | +# License. |
| 5 | +# |
| 6 | +# You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the |
| 7 | +# specific language governing permission and limitations under the License. |
| 8 | +# |
| 9 | +# When distributing Covered Software, include this CDDL Header Notice in each file and include |
| 10 | +# the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL |
| 11 | +# Header, with the fields enclosed by brackets [] replaced by your own identifying |
| 12 | +# information: "Portions copyright [year] [name of copyright owner]". |
| 13 | +# |
| 14 | +# Copyright 2026 3A Systems, LLC. |
| 15 | +# |
| 16 | +# Render an LDAP benchmark comparison report (versions + per-operation table + QuickChart charts) |
| 17 | +# for two servers A and B to stdout — intended to be appended to $GITHUB_STEP_SUMMARY. The report |
| 18 | +# is generic: server names, versions and images are all parameters, and benchmark-specific notes |
| 19 | +# are NOT included here (append them separately, e.g. `cat notes.md >> $GITHUB_STEP_SUMMARY`). |
| 20 | +# |
| 21 | +# Usage: |
| 22 | +# summary.sh <A_name> <A_statistics.json> <A_version> <A_image> \ |
| 23 | +# <B_name> <B_statistics.json> <B_version> <B_image> |
| 24 | +# |
| 25 | +# The admin connection bind is labelled ADMIN_CONNECT in the plan and is intentionally |
| 26 | +# skipped here, so it never pollutes the per-operation comparison. |
| 27 | +set -euo pipefail |
| 28 | + |
| 29 | +A_NAME="${1:?server A name required}" |
| 30 | +A_JSON="${2:?server A statistics.json required}" |
| 31 | +A_VER="${3:-unknown}" |
| 32 | +A_IMG="${4:-}" |
| 33 | +B_NAME="${5:?server B name required}" |
| 34 | +B_JSON="${6:?server B statistics.json required}" |
| 35 | +B_VER="${7:-unknown}" |
| 36 | +B_IMG="${8:-}" |
| 37 | + |
| 38 | +A_COLOR="#4e79a7" # blue = server A |
| 39 | +B_COLOR="#f28e2b" # orange = server B |
| 40 | + |
| 41 | +# Operations to compare, in workflow order. ADMIN_CONNECT and Total are excluded. |
| 42 | +OPS=("ADD" "SEARCH" "COMPARE" "MODIFY" "BIND" "DELETE" "READD") |
| 43 | + |
| 44 | +# m <file> <label> <field> -> numeric value (0 if absent), rounded to 1 decimal. |
| 45 | +m() { jq -r --arg l "$2" --arg f "$3" '((.[$l][$f]) // 0) | (.*10 | round / 10)' "$1"; } |
| 46 | +# mi <file> <label> <field> -> integer value (0 if absent). |
| 47 | +mi() { jq -r --arg l "$2" --arg f "$3" '((.[$l][$f]) // 0) | round' "$1"; } |
| 48 | + |
| 49 | +echo "## 🔬 Benchmark: ${A_NAME} vs ${B_NAME}" |
| 50 | +echo "" |
| 51 | + |
| 52 | +# ---------------------------------------------------------------- Versions |
| 53 | +echo "### Versions" |
| 54 | +echo "" |
| 55 | +echo "| Server | Version | Image |" |
| 56 | +echo "|---|---|---|" |
| 57 | +echo "| **${A_NAME}** | \`${A_VER}\` | \`${A_IMG:-n/a}\` |" |
| 58 | +echo "| **${B_NAME}** | \`${B_VER}\` | \`${B_IMG:-n/a}\` |" |
| 59 | +echo "" |
| 60 | + |
| 61 | +# ---------------------------------------------------------------- Totals |
| 62 | +a_tot_tp=$(m "$A_JSON" Total throughput) |
| 63 | +b_tot_tp=$(m "$B_JSON" Total throughput) |
| 64 | +a_tot_n=$(mi "$A_JSON" Total sampleCount) |
| 65 | +b_tot_n=$(mi "$B_JSON" Total sampleCount) |
| 66 | +a_tot_e=$(mi "$A_JSON" Total errorCount) |
| 67 | +b_tot_e=$(mi "$B_JSON" Total errorCount) |
| 68 | +a_tot_mean=$(m "$A_JSON" Total meanResTime) |
| 69 | +b_tot_mean=$(m "$B_JSON" Total meanResTime) |
| 70 | + |
| 71 | +echo "### Totals (all operations, ADMIN_CONNECT excluded by the plan label)" |
| 72 | +echo "" |
| 73 | +echo "| Server | Throughput (tests/s) | Mean (ms) | Samples | Errors |" |
| 74 | +echo "|---|--:|--:|--:|--:|" |
| 75 | +echo "| **${A_NAME}** | ${a_tot_tp} | ${a_tot_mean} | ${a_tot_n} | ${a_tot_e} |" |
| 76 | +echo "| **${B_NAME}** | ${b_tot_tp} | ${b_tot_mean} | ${b_tot_n} | ${b_tot_e} |" |
| 77 | +echo "" |
| 78 | + |
| 79 | +# ---------------------------------------------------------------- Per-op table |
| 80 | +echo "### Per-operation latency" |
| 81 | +echo "" |
| 82 | +echo "| Operation | mean ms ${A_NAME} | mean ms ${B_NAME} | p99 ms ${A_NAME} | p99 ms ${B_NAME} | err ${A_NAME} | err ${B_NAME} |" |
| 83 | +echo "|---|--:|--:|--:|--:|--:|--:|" |
| 84 | +for op in "${OPS[@]}"; do |
| 85 | + printf '| %s | %s | %s | %s | %s | %s | %s |\n' \ |
| 86 | + "$op" \ |
| 87 | + "$(m "$A_JSON" "$op" meanResTime)" "$(m "$B_JSON" "$op" meanResTime)" \ |
| 88 | + "$(mi "$A_JSON" "$op" pct3ResTime)" "$(mi "$B_JSON" "$op" pct3ResTime)" \ |
| 89 | + "$(mi "$A_JSON" "$op" errorCount)" "$(mi "$B_JSON" "$op" errorCount)" |
| 90 | +done |
| 91 | +echo "" |
| 92 | + |
| 93 | +# ---------------------------------------------------------------- Chart helpers (QuickChart) |
| 94 | +# Mermaid xychart-beta can't do grouped bars / a legend and crowds 14 x-labels, so render proper |
| 95 | +# grouped bar charts via QuickChart (Chart.js) as images: https://quickchart.io/chart?c=<config>. |
| 96 | + |
| 97 | +# JSON array of the OPS labels: ["ADD","SEARCH",...]. |
| 98 | +labels_json() { |
| 99 | + local out="" op |
| 100 | + for op in "${OPS[@]}"; do out+="${out:+,}\"${op}\""; done |
| 101 | + printf '[%s]' "$out" |
| 102 | +} |
| 103 | +# Comma-joined values for all OPS from <file> <field> via the <m> helper. |
| 104 | +vals() { # <fn> <file> <field> |
| 105 | + local fn="$1" file="$2" field="$3" out="" v |
| 106 | + for op in "${OPS[@]}"; do v=$("$fn" "$file" "$op" "$field"); out+="${out:+,}${v}"; done |
| 107 | + printf '%s' "$out" |
| 108 | +} |
| 109 | +urienc() { jq -rn --arg s "$1" '$s|@uri'; } # URL-encode the chart config |
| 110 | +qc() { printf 'https://quickchart.io/chart?w=%s&h=%s&c=%s' "$1" "$2" "$(urienc "$3")"; } |
| 111 | + |
| 112 | +# ---------------------------------------------------------------- Total throughput chart |
| 113 | +echo "### Total throughput (tests/s, higher is better)" |
| 114 | +echo "" |
| 115 | +echo "_Per-operation throughput is not charted: every op runs once per loop iteration, so each" |
| 116 | +echo "op's throughput just equals the loop rate. The meaningful throughput is the aggregate._" |
| 117 | +echo "" |
| 118 | +TP_CFG="{\"type\":\"bar\",\"data\":{\"labels\":[\"${A_NAME}\",\"${B_NAME}\"],\"datasets\":[{\"label\":\"tests/s\",\"backgroundColor\":[\"$A_COLOR\",\"$B_COLOR\"],\"data\":[${a_tot_tp},${b_tot_tp}]}]},\"options\":{\"legend\":{\"display\":false},\"title\":{\"display\":true,\"text\":\"Total throughput (tests/s)\"}}}" |
| 119 | +echo ")" |
| 120 | +echo "" |
| 121 | + |
| 122 | +# ---------------------------------------------------------------- Latency chart (grouped bars) |
| 123 | +echo "### p99 latency per operation (ms, lower is better)" |
| 124 | +echo "" |
| 125 | +echo "_🟦 ${A_NAME} · 🟧 ${B_NAME} — grouped bars per operation._" |
| 126 | +echo "" |
| 127 | +LAT_CFG="{\"type\":\"bar\",\"data\":{\"labels\":$(labels_json),\"datasets\":[{\"label\":\"${A_NAME}\",\"backgroundColor\":\"$A_COLOR\",\"data\":[$(vals mi "$A_JSON" pct3ResTime)]},{\"label\":\"${B_NAME}\",\"backgroundColor\":\"$B_COLOR\",\"data\":[$(vals mi "$B_JSON" pct3ResTime)]}]},\"options\":{\"title\":{\"display\":true,\"text\":\"p99 latency per operation (ms)\"}}}" |
| 128 | +echo ")" |
| 129 | +echo "" |
0 commit comments