Skip to content

Commit 3eae488

Browse files
committed
Merge remote-tracking branch 'origin/master' into features/modify-1
2 parents 466fc15 + a6216bf commit 3eae488

295 files changed

Lines changed: 7401 additions & 1385 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.gitattributes

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# Keep HTML checked out with LF on all platforms so javadoc doclint
2+
# (JDK 25/26) does not treat CR (from CRLF) as part of a multi-line tag name.
3+
*.html text eol=lf

.github/benchmark/benchmark.jmx

Lines changed: 327 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
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+
# Compare two OpenDJ Docker images with the LDAP benchmark (.github/benchmark/benchmark.jmx) and
17+
# append the comparison report to $GITHUB_STEP_SUMMARY. Both sides are OpenDJ, so no per-server
18+
# hashing/index setup is needed (identical product => identical default password scheme).
19+
#
20+
# Usage:
21+
# compare-opendj.sh <A_name> <A_image> <B_name> <B_image>
22+
# Env: THREADS (default 200), DURATION (default 150), JMETER_VERSION (default 5.6.3).
23+
# A_BACKEND / B_BACKEND (optional) -- userRoot backend type per side (e.g. je, pdb);
24+
# when set, passed to the container as -e BACKEND_TYPE=<type>. Empty => image default.
25+
set -euo pipefail
26+
27+
A_NAME="${1:?server A name required}"
28+
A_IMAGE="${2:?server A image required}"
29+
B_NAME="${3:?server B name required}"
30+
B_IMAGE="${4:?server B image required}"
31+
32+
THREADS="${THREADS:-200}"
33+
DURATION="${DURATION:-150}"
34+
JMETER_VERSION="${JMETER_VERSION:-5.6.3}"
35+
A_BACKEND="${A_BACKEND:-}"
36+
B_BACKEND="${B_BACKEND:-}"
37+
BASEDN="dc=example,dc=com"
38+
BENCHPW="benchPass1"
39+
HERE="$(cd "$(dirname "$0")" && pwd)" # .github/benchmark
40+
41+
# ---------------------------------------------------------------- dependencies
42+
if ! command -v ldapsearch >/dev/null 2>&1 || ! command -v jq >/dev/null 2>&1; then
43+
sudo apt-get update -qq
44+
sudo apt-get install -y -qq ldap-utils jq
45+
fi
46+
JM="$HOME/jmeter/apache-jmeter-$JMETER_VERSION/bin/jmeter"
47+
if [ ! -x "$JM" ]; then
48+
mkdir -p "$HOME/jmeter"
49+
curl -fsSL "https://archive.apache.org/dist/jmeter/binaries/apache-jmeter-$JMETER_VERSION.tgz" -o /tmp/jmeter.tgz
50+
tar -xzf /tmp/jmeter.tgz -C "$HOME/jmeter"
51+
fi
52+
53+
wait_dj() { # poll OpenDJ readiness on localhost:1389
54+
for _ in $(seq 1 90); do
55+
ldapsearch -x -H ldap://localhost:1389 -D "cn=Directory Manager" -w password \
56+
-b "$BASEDN" -s base dn >/dev/null 2>&1 && return 0
57+
sleep 2
58+
done
59+
return 1
60+
}
61+
62+
# bench_one <image> <out-slug> [backend-type] -> prints the captured server version (stdout only)
63+
bench_one() {
64+
local image="$1" out="$2" backend="${3:-}" ver=""
65+
docker rm -f opendj-bench >/dev/null 2>&1 || true
66+
# ${backend:+...} expands to "-e BACKEND_TYPE=<type>" only when a type is given; the
67+
# values used (je/pdb) contain no spaces, so word-splitting is safe and portable.
68+
if docker run -d --name opendj-bench -p 1389:1389 \
69+
-e ROOT_PASSWORD=password -e BASE_DN="$BASEDN" -e ADD_BASE_ENTRY=--addBaseEntry \
70+
${backend:+-e BACKEND_TYPE=$backend} \
71+
"$image" >/dev/null 2>&1; then
72+
wait_dj || echo "WARN: $image not ready in time" >&2
73+
ldapadd -x -H ldap://localhost:1389 -D "cn=Directory Manager" -w password \
74+
-f "$HERE/people.ldif" >/dev/null 2>&1 || true
75+
ver="$( { ldapsearch -x -LLL -H ldap://localhost:1389 -D 'cn=Directory Manager' -w password \
76+
-b '' -s base fullVendorVersion 2>/dev/null || true; } | sed -n 's/^fullVendorVersion: //p')"
77+
rm -rf "$out" "$out.jtl"
78+
HEAP="-Xms1g -Xmx2g" "$JM" -n -t "$HERE/benchmark.jmx" \
79+
-Jhost=localhost -Jport=1389 -Jbasedn="$BASEDN" \
80+
-Jadminbinddn="cn=Directory Manager" -Jadminbindpw=password -Jbenchpw="$BENCHPW" \
81+
-Jthreads="$THREADS" -Jduration="$DURATION" -Jrampup=0 \
82+
-Jjmeter.reportgenerator.sample_filter='^(?!ADMIN_CONNECT).*' \
83+
-l "$out.jtl" -e -o "$out" > "$out.jmeter.out" 2>&1 || true
84+
docker logs opendj-bench > "$out.docker.log" 2>&1 || true
85+
# surface distinct error messages to the step log (stderr; stdout carries the version)
86+
if [ -f "$out.jtl" ]; then
87+
local errs
88+
errs="$(awk -F',' 'NR==1{for(i=1;i<=NF;i++)h[$i]=i; next}
89+
tolower($h["success"])=="false"{print $h["label"]" | "$h["responseCode"]" | "$h["responseMessage"]}' \
90+
"$out.jtl" 2>/dev/null | sort | uniq -c | sort -rn | head -10)"
91+
[ -z "$errs" ] || { echo "[$out] errors (count | op | code | message):" >&2; echo "$errs" >&2; }
92+
fi
93+
else
94+
echo "ERROR: failed to start image $image" >&2
95+
fi
96+
docker rm -f opendj-bench >/dev/null 2>&1 || true
97+
[ -n "$ver" ] || ver="$image"
98+
printf '%s' "$ver"
99+
}
100+
101+
echo "Benchmarking ${A_NAME} (${A_IMAGE}${A_BACKEND:+, backend=$A_BACKEND}) @ ${THREADS} threads / ${DURATION}s ..."
102+
A_VER="$(bench_one "$A_IMAGE" a "$A_BACKEND")"
103+
echo "Benchmarking ${B_NAME} (${B_IMAGE}${B_BACKEND:+, backend=$B_BACKEND}) @ ${THREADS} threads / ${DURATION}s ..."
104+
B_VER="$(bench_one "$B_IMAGE" b "$B_BACKEND")"
105+
106+
{
107+
bash "$HERE/summary.sh" \
108+
"$A_NAME" a/statistics.json "$A_VER" "$A_IMAGE" \
109+
"$B_NAME" b/statistics.json "$B_VER" "$B_IMAGE"
110+
echo ""
111+
echo "### Notes"
112+
echo ""
113+
if [ -n "${A_BACKEND}${B_BACKEND}" ]; then
114+
echo "- **${A_NAME}** (\`BACKEND_TYPE=${A_BACKEND:-default}\`) and **${B_NAME}**"
115+
echo " (\`BACKEND_TYPE=${B_BACKEND:-default}\`) are the same OpenDJ image; only the userRoot"
116+
echo " backend type differs, so the comparison isolates the storage backend."
117+
else
118+
echo "- **${A_NAME}** = freshly built image; **${B_NAME}** = latest released image. Both are"
119+
echo " OpenDJ, so they share the same default password storage scheme (hashing parity is automatic)."
120+
fi
121+
echo "- The admin connection bind (\`ADMIN_CONNECT\`) is cached per thread and excluded; \`BIND\` is"
122+
echo " the measured user authentication (\`test=sbind\`, single bind/unbind)."
123+
} >> "${GITHUB_STEP_SUMMARY:-/dev/stdout}"

.github/benchmark/people.ldif

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
# The contents of this file are subject to the terms of the Common Development and
2+
# Distribution License (the License). You may not use this file except in compliance with the
3+
# License.
4+
#
5+
# You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
6+
# specific language governing permission and limitations under the License.
7+
#
8+
# When distributing Covered Software, include this CDDL Header Notice in each file and include
9+
# the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
10+
# Header, with the fields enclosed by brackets [] replaced by your own identifying
11+
# information: "Portions copyright [year] [name of copyright owner]".
12+
#
13+
# Copyright 2026 3A Systems, LLC.
14+
dn: ou=People,dc=example,dc=com
15+
objectClass: top
16+
objectClass: organizationalUnit
17+
ou: People

.github/benchmark/summary.sh

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
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 "![Total throughput (tests/s)]($(qc 500 320 "$TP_CFG"))"
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 "![p99 latency per operation (ms)]($(qc 900 400 "$LAT_CFG"))"
129+
echo ""

0 commit comments

Comments
 (0)