| .github/benchmark/benchmark.jmx | ●●●●● patch | view | raw | blame | history | |
| .github/benchmark/summary.sh | ●●●●● patch | view | raw | blame | history | |
| .github/workflows/benchmark.yml | ●●●●● patch | view | raw | blame | history |
.github/benchmark/benchmark.jmx
@@ -17,7 +17,7 @@ <jmeterTestPlan version="1.2" properties="5.0" jmeter="5.6.3"> <hashTree> <TestPlan guiclass="TestPlanGui" testclass="TestPlan" testname="OpenDJ vs OpenLDAP - LDAP benchmark" enabled="true"> <stringProp name="TestPlan.comments">Parametrized LDAP benchmark. Admin bind is cached once per thread (Once Only Controller, labelled ADMIN_CONNECT, excluded from metrics). Data ops (ADD/SEARCH/COMPARE/MODIFY/DELETE/ADD WITHOUT DELETE) reuse the cached admin connection. The measured user authentication is a single bind/unbind (test=sbind, own connection) after MODIFY has set the userPassword.</stringProp> <stringProp name="TestPlan.comments">Parametrized LDAP benchmark. Admin bind is cached once per thread (Once Only Controller, labelled ADMIN_CONNECT, excluded from metrics). Data ops (ADD/SEARCH/COMPARE/MODIFY/DELETE/READD) reuse the cached admin connection. The measured user authentication is a single bind/unbind (test=sbind, own connection) after MODIFY has set the userPassword.</stringProp> <boolProp name="TestPlan.functional_mode">false</boolProp> <boolProp name="TestPlan.tearDown_on_shutdown">true</boolProp> <boolProp name="TestPlan.serialize_threadgroups">false</boolProp> @@ -268,8 +268,8 @@ </LDAPExtSampler> <hashTree/> <!-- ADD WITHOUT DELETE (admin connection): accumulation; unique RDN via __UUID. --> <LDAPExtSampler guiclass="LdapExtTestSamplerGui" testclass="LDAPExtSampler" testname="ADD WITHOUT DELETE" enabled="true"> <!-- READD (admin connection): re-add without delete (accumulation); unique RDN via __UUID. --> <LDAPExtSampler guiclass="LdapExtTestSamplerGui" testclass="LDAPExtSampler" testname="READD" enabled="true"> <stringProp name="servername"></stringProp> <stringProp name="port"></stringProp> <stringProp name="rootdn"></stringProp> .github/benchmark/summary.sh
@@ -32,7 +32,7 @@ DJ_IMG="${6:-}" # Operations to compare, in workflow order. ADMIN_CONNECT and Total are excluded. OPS=("ADD" "SEARCH" "COMPARE" "MODIFY" "BIND" "DELETE" "ADD WITHOUT DELETE") OPS=("ADD" "SEARCH" "COMPARE" "MODIFY" "BIND" "DELETE" "READD") # m <file> <label> <field> -> numeric value (0 if absent), rounded to 1 decimal. m() { jq -r --arg l "$2" --arg f "$3" '((.[$l][$f]) // 0) | (.*10 | round / 10)' "$1"; } @@ -85,51 +85,63 @@ echo "" # ---------------------------------------------------------------- Chart helpers # Build a Mermaid list "x, y, z" of values for all OPS from <file> <field>, via <m|mi>. series() { # <fn> <file> <field> local fn="$1" file="$2" field="$3" out="" v for op in "${OPS[@]}"; do v=$("$fn" "$file" "$op" "$field") out+="${out:+, }${v}" done # Mermaid xychart-beta has no grouped bars/legend, so to get two adjacent (non-overlapping) # columns per operation we repeat each op on the x-axis and zero-pad the two series: OpenLDAP # bars land on the left tick of each pair, OpenDJ on the right tick. # # x-axis: each op twice -> "ADD", "ADD", "SEARCH", "SEARCH", ... xaxis_pairs() { local out="" op for op in "${OPS[@]}"; do out+="${out:+, }\"${op}\", \"${op}\""; done printf '%s' "$out" } # x-axis with every label quoted (labels contain spaces). xaxis() { local out="" op for op in "${OPS[@]}"; do out+="${out:+, }\"${op}\""; done # OpenLDAP series: value then 0 per op (bar on the left tick of each pair). series_ol() { # <fn> <file> <field> local fn="$1" file="$2" field="$3" out="" v for op in "${OPS[@]}"; do v=$("$fn" "$file" "$op" "$field"); out+="${out:+, }${v}, 0"; done printf '%s' "$out" } # OpenDJ series: 0 then value per op (bar on the right tick of each pair). series_dj() { # <fn> <file> <field> local fn="$1" file="$2" field="$3" out="" v for op in "${OPS[@]}"; do v=$("$fn" "$file" "$op" "$field"); out+="${out:+, }0, ${v}"; done printf '%s' "$out" } XAXIS="$(xaxis)" XAXIS="$(xaxis_pairs)" # Fix the two series colors (series 1 = OpenLDAP blue, series 2 = OpenDJ orange). PALETTE='%%{init: {"themeVariables": {"xyChart": {"plotColorPalette": "#4e79a7, #f28e2b"}}}}%%' CAPTION="_Each operation has two columns: ๐ฆ OpenLDAP (left) ยท ๐ง OpenDJ (right)._" # ---------------------------------------------------------------- Throughput chart echo "### Comparative chart โ throughput per operation (ops/s, higher is better)" echo "" echo "_First bar series = OpenLDAP, second bar series = OpenDJ._" echo "$CAPTION" echo "" echo '```mermaid' echo "$PALETTE" echo "xychart-beta" echo " title \"Throughput per operation (ops/s) โ OpenLDAP vs OpenDJ\"" echo " x-axis [${XAXIS}]" echo " y-axis \"ops/s\"" echo " bar [$(series m "$OL_JSON" throughput)]" echo " bar [$(series m "$DJ_JSON" throughput)]" echo " bar [$(series_ol m "$OL_JSON" throughput)]" echo " bar [$(series_dj m "$DJ_JSON" throughput)]" echo '```' echo "" # ---------------------------------------------------------------- Latency chart echo "### Comparative chart โ mean latency per operation (ms, lower is better)" echo "" echo "_First bar series = OpenLDAP, second bar series = OpenDJ._" echo "$CAPTION" echo "" echo '```mermaid' echo "$PALETTE" echo "xychart-beta" echo " title \"Mean latency per operation (ms) โ OpenLDAP vs OpenDJ\"" echo " x-axis [${XAXIS}]" echo " y-axis \"ms\"" echo " bar [$(series m "$OL_JSON" meanResTime)]" echo " bar [$(series m "$DJ_JSON" meanResTime)]" echo " bar [$(series_ol m "$OL_JSON" meanResTime)]" echo " bar [$(series_dj m "$DJ_JSON" meanResTime)]" echo '```' echo "" @@ -139,6 +151,7 @@ echo "- \`BIND\` is the measured **user authentication** (\`test=sbind\`, single bind/unbind on its" echo " own connection) as \`cn=user_<n>,ou=People\` with the password set by \`MODIFY\`. The admin" echo " connection bind (\`ADMIN_CONNECT\`) is cached once per thread and excluded from these results." echo "- Default password storage scheme differs by server (OpenDJ PBKDF2/Salted-SHA vs OpenLDAP" echo " SSHA); this is a legitimate part of authentication cost." echo "- MODIFY sends the password in cleartext; each server hashes it on write with the **same" echo " scheme (SSHA-256)** โ OpenLDAP via the pw-sha2 module + ppolicy hash-cleartext, OpenDJ via" echo " its Salted SHA-256 default scheme โ so BIND authentication is compared on equal footing." echo "- Full interactive JMeter HTML dashboards are attached as the \`jmeter-reports\` artifact." .github/workflows/benchmark.yml
@@ -78,7 +78,7 @@ - uses: actions/checkout@v6 - name: Cache JMeter uses: actions/cache@v4 uses: actions/cache@v5 with: path: ~/jmeter key: jmeter-${{ env.JMETER }} @@ -103,15 +103,32 @@ -e LDAP_TLS=false \ "$OPENLDAP_IMAGE" - name: Wait for OpenLDAP + seed ou=People - name: Configure OpenLDAP (SSHA-256 hash-on-write, seed) run: | for i in $(seq 1 90); do if ldapsearch -x -H ldap://localhost:2389 -D "cn=admin,$BASEDN" -w password \ -b "$BASEDN" -s base dn >/dev/null 2>&1; then echo "OpenLDAP is up"; break fi sleep 2 done wait_ldap() { for i in $(seq 1 90); do ldapsearch -x -H ldap://localhost:2389 -D "cn=admin,$BASEDN" -w password \ -b "$BASEDN" -s base dn >/dev/null 2>&1 && { echo "OpenLDAP is up"; return 0; } sleep 2 done } le() { docker exec -i openldap "$@" -Y EXTERNAL -H ldapi:/// >/dev/null 2>&1 || true; } wait_ldap # Load pw-sha2 (provides {SSHA256}) and ppolicy (overlay) modules; osixia ships both # in /usr/lib/ldap. Create the module list entry or append the values, then restart so # the modules are active in the running slapd. printf 'dn: cn=module{0},cn=config\nobjectClass: olcModuleList\nolcModuleLoad: pw-sha2\nolcModuleLoad: ppolicy\n' | le ldapadd printf 'dn: cn=module{0},cn=config\nchangetype: modify\nadd: olcModuleLoad\nolcModuleLoad: pw-sha2\n' | le ldapmodify printf 'dn: cn=module{0},cn=config\nchangetype: modify\nadd: olcModuleLoad\nolcModuleLoad: ppolicy\n' | le ldapmodify docker restart openldap wait_ldap # Make slapd hash cleartext userPassword with {SSHA256} on a plain modify: set the global # password-hash and enable the ppolicy overlay's hash_cleartext on the mdb database. printf 'dn: olcDatabase={-1}frontend,cn=config\nchangetype: modify\nreplace: olcPasswordHash\nolcPasswordHash: {SSHA256}\n' | le ldapmodify DBDN="$(docker exec openldap ldapsearch -Y EXTERNAL -H ldapi:/// -LLL -b cn=config '(olcDatabase=*mdb)' dn 2>/dev/null | sed -n 's/^dn: //p' | head -1)" [ -n "$DBDN" ] || DBDN="olcDatabase={1}mdb,cn=config" printf 'dn: olcOverlay=ppolicy,%s\nobjectClass: olcOverlayConfig\nobjectClass: olcPPolicyConfig\nolcOverlay: ppolicy\nolcPPolicyHashCleartext: TRUE\n' "$DBDN" | le ldapadd # Seed ou=People (after the restart so it survives any re-init). ldapadd -x -H ldap://localhost:2389 -D "cn=admin,$BASEDN" -w password \ -f .github/benchmark/people.ldif || true @@ -141,6 +158,11 @@ -Jjmeter.reportgenerator.sample_filter="$SAMPLE_FILTER" \ -l openldap.jtl -e -o openldap - name: Stop OpenLDAP run: | docker logs openldap 2>&1 | tail -n 100 || true docker rm -f openldap || true - name: Start OpenDJ run: | docker run -d --name opendj -p 1389:1389 \ @@ -161,6 +183,16 @@ ldapadd -x -H ldap://localhost:1389 -D "cn=Directory Manager" -w password \ -f .github/benchmark/people.ldif || true - name: Configure OpenDJ password policy (SSHA-256 hash-on-write) run: | # Hash cleartext userPassword with Salted SHA-256 on write, matching OpenLDAP. docker exec opendj /opt/opendj/bin/dsconfig set-password-policy-prop \ --policy-name "Default Password Policy" \ --set default-password-storage-scheme:"Salted SHA-256" \ --hostname localhost --port 4444 \ --bindDN "cn=Directory Manager" --bindPassword password \ --trustAll --no-prompt - name: Capture OpenDJ version run: | # `|| true` guards the bind so a transient ldapsearch failure can't trip pipefail; @@ -187,6 +219,11 @@ -Jjmeter.reportgenerator.sample_filter="$SAMPLE_FILTER" \ -l opendj.jtl -e -o opendj - name: Stop OpenDJ run: | docker logs opendj 2>&1 | tail -n 100 || true docker rm -f opendj || true # ---------------------------------------------------------------- Report - name: Build job summary run: | @@ -197,7 +234,7 @@ - name: Upload JMeter reports if: always() uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v7 with: name: jmeter-reports path: |