]> git.ipfire.org Git - thirdparty/freeradius-server.git/commitdiff
Four new valgrind profiling multi-server tests
authorMarc-Andre Casavant <marc.casavant@inkbridge.io>
Thu, 23 Apr 2026 13:47:28 +0000 (09:47 -0400)
committerArran Cudbard-Bell <a.cudbardb@freeradius.org>
Thu, 21 May 2026 17:39:52 +0000 (13:39 -0400)
57 files changed:
.gitignore
src/tests/multi-server/README.md
src/tests/multi-server/all.mk
src/tests/multi-server/configs/freeradius/common/mods-available/ldap [new file with mode: 0644]
src/tests/multi-server/configs/freeradius/common/mods-available/pap [new file with mode: 0644]
src/tests/multi-server/configs/freeradius/common/mods-available/sql [new file with mode: 0644]
src/tests/multi-server/configs/freeradius/profiling-ldap/load-generator-packets/packet.conf [new file with mode: 0644]
src/tests/multi-server/configs/freeradius/profiling-ldap/mods-config/files/authorize [new file with mode: 0644]
src/tests/multi-server/configs/freeradius/profiling-ldap/proto_load_config.env [new file with mode: 0644]
src/tests/multi-server/configs/freeradius/profiling-ldap/radiusd.conf.j2 [new file with mode: 0644]
src/tests/multi-server/configs/freeradius/profiling-ldap/stats/load-generator-stats.csv [new file with mode: 0644]
src/tests/multi-server/configs/freeradius/profiling-ldap/template.d/virtual-server-templates [new file with mode: 0644]
src/tests/multi-server/configs/freeradius/profiling-mysql/load-generator-packets/packet.conf [new file with mode: 0644]
src/tests/multi-server/configs/freeradius/profiling-mysql/mods-config/files/authorize [new file with mode: 0644]
src/tests/multi-server/configs/freeradius/profiling-mysql/proto_load_config.env [new file with mode: 0644]
src/tests/multi-server/configs/freeradius/profiling-mysql/radiusd.conf.j2 [new file with mode: 0644]
src/tests/multi-server/configs/freeradius/profiling-mysql/stats/load-generator-stats.csv [new file with mode: 0644]
src/tests/multi-server/configs/freeradius/profiling-mysql/template.d/virtual-server-templates [new file with mode: 0644]
src/tests/multi-server/configs/freeradius/profiling-server-pap-auth/load-generator-packets/packet.conf [new file with mode: 0644]
src/tests/multi-server/configs/freeradius/profiling-server-pap-auth/mods-config/files/authorize [new file with mode: 0644]
src/tests/multi-server/configs/freeradius/profiling-server-pap-auth/proto_load_config.env [new file with mode: 0644]
src/tests/multi-server/configs/freeradius/profiling-server-pap-auth/radiusd.conf.j2 [new file with mode: 0644]
src/tests/multi-server/configs/freeradius/profiling-server-pap-auth/stats/load-generator-stats.csv [new file with mode: 0644]
src/tests/multi-server/configs/freeradius/profiling-server-pap-auth/template.d/virtual-server-templates [new file with mode: 0644]
src/tests/multi-server/configs/freeradius/profiling-server/load-generator-packets/packet.conf [new file with mode: 0644]
src/tests/multi-server/configs/freeradius/profiling-server/mods-config/files/authorize [new file with mode: 0644]
src/tests/multi-server/configs/freeradius/profiling-server/proto_load_config.env [new file with mode: 0644]
src/tests/multi-server/configs/freeradius/profiling-server/radiusd.conf.j2 [new file with mode: 0644]
src/tests/multi-server/configs/freeradius/profiling-server/stats/load-generator-stats.csv [new file with mode: 0644]
src/tests/multi-server/configs/freeradius/profiling-server/template.d/virtual-server-templates [new file with mode: 0644]
src/tests/multi-server/configs/mariadb/default/init.sql [new file with mode: 0644]
src/tests/multi-server/environments/profiling-ldap.yml.j2 [new file with mode: 0644]
src/tests/multi-server/environments/profiling-mysql.yml.j2 [new file with mode: 0644]
src/tests/multi-server/environments/profiling-pap-auth.yml.j2 [new file with mode: 0644]
src/tests/multi-server/environments/profiling.yml.j2 [new file with mode: 0644]
src/tests/multi-server/scripts/docker/build/Dockerfile.multi-server-prof [new file with mode: 0644]
src/tests/multi-server/scripts/docker/build/build_image.sh [new file with mode: 0755]
src/tests/multi-server/scripts/docker/build/run_container.sh [new file with mode: 0755]
src/tests/multi-server/scripts/profiling/README.md [new file with mode: 0644]
src/tests/multi-server/scripts/profiling/generate_callgrind_report.py [new file with mode: 0755]
src/tests/multi-server/scripts/profiling/start_valgrind_profiling.sh [new file with mode: 0755]
src/tests/multi-server/tests/prof-accept/5min.test.yml [new file with mode: 0644]
src/tests/multi-server/tests/prof-accept/environment.yml.j2 [new symlink]
src/tests/multi-server/tests/prof-accept/short.ci.test.yml [new file with mode: 0644]
src/tests/multi-server/tests/prof-accept/template.yml.j2 [new file with mode: 0644]
src/tests/multi-server/tests/prof-ldap/5min.test.yml [new file with mode: 0644]
src/tests/multi-server/tests/prof-ldap/environment.yml.j2 [new symlink]
src/tests/multi-server/tests/prof-ldap/short.ci.test.yml [new file with mode: 0644]
src/tests/multi-server/tests/prof-ldap/template.yml.j2 [new file with mode: 0644]
src/tests/multi-server/tests/prof-mysql/5min.test.yml [new file with mode: 0644]
src/tests/multi-server/tests/prof-mysql/environment.yml.j2 [new symlink]
src/tests/multi-server/tests/prof-mysql/short.ci.test.yml [new file with mode: 0644]
src/tests/multi-server/tests/prof-mysql/template.yml.j2 [new file with mode: 0644]
src/tests/multi-server/tests/prof-pap-auth/5min.test.yml [new file with mode: 0644]
src/tests/multi-server/tests/prof-pap-auth/environment.yml.j2 [new symlink]
src/tests/multi-server/tests/prof-pap-auth/short.ci.test.yml [new file with mode: 0644]
src/tests/multi-server/tests/prof-pap-auth/template.yml.j2 [new file with mode: 0644]

index 43a846c03b5615ec63a5226e369b1bfe5c13df01..42f4e66905a9918e40131894f6778e2260c54709 100644 (file)
@@ -98,3 +98,6 @@ main-site.yml
 
 # Claude AI
 build-2f*
+
+# Profiling results
+prof-results/
index 57a29ebe660863ceadb2b00fcc7c119ec8848f63..2ef90d28a1a8ddaa65a0270e8e909726c1035500 100644 (file)
@@ -29,7 +29,7 @@ make test.multi-server.ci
 ### A specific test
 
 ```bash
-make test.multi-server.proxy-accept.short.ci
+make test.multi-server.proxy-accept.short_ci
 ```
 
 ### Parallel execution
@@ -80,3 +80,60 @@ finding `*.test.yml` files within them.
 - `*.test.yml` - Test parameter file (discovered by `make test.multi-server`)
 - `*.ci.test.yml` - CI test parameter file (also discovered by `make test.multi-server.ci`)
 - `*.yml.j2` - Jinja2 template (rendered, not treated as a test)
+
+# Multi-Server Profiling Tests
+
+Four suites instrument FreeRADIUS under Valgrind to collect heap and call-graph profiles:
+
+| Suite | What it exercises |
+| --- | --- |
+| `prof-accept` | Plain RADIUS accept (no external services) |
+| `prof-pap-auth` | PAP authentication |
+| `prof-ldap` | Authentication backed by an LDAP server |
+| `prof-mysql` | Authentication backed by a MySQL database |
+
+Results land in `prof-results/<suite>/<test>/<branch>/<commit>/<run-index>/`.
+
+### Docker image dependencies
+
+Profiling suites require images that are not needed by regular multi-server tests.
+The `freeradius-prof.image` target builds or verifies them automatically before any `prof-*` test runs:
+
+- `freeradius40x-build/ubuntu24:latest` — crossbuild base image
+- `freeradius4-<profile>/ubuntu24:latest` — FreeRADIUS profiling base image
+- `freeradius-prof:latest` — final multi-server profiling image (built by `build_image.sh`)
+
+The `prof-ldap` suite additionally requires:
+
+- `freeradius4/openldap-prof:latest` — built via the `openldap.image` target
+
+To build all profiling images explicitly:
+
+```bash
+make freeradius-prof.image
+make openldap.image   # prof-ldap only
+```
+
+### Running on Linux
+
+Profiling tests run the same way as any other multi-server test:
+
+```bash
+make test.multi-server.prof-mysql.short_ci
+```
+
+### Running on macOS (Apple Silicon)
+
+The profiling image is based on a `crossbuild.<distro>` base image that is built for
+`linux/amd64`. On Apple Silicon you must pass `BUILD_PLATFORM=linux/amd64` so that
+Docker pulls and runs the correct platform variant:
+
+```bash
+make test.multi-server.prof-mysql.short_ci BUILD_PLATFORM=linux/amd64
+```
+
+This applies to all four profiling suites and to the image-build targets as well:
+
+```bash
+make freeradius-prof.image BUILD_PLATFORM=linux/amd64
+```
index b911ef3342c97586dda84f862daacbc65bb6058c..846146bf2495bd69e7fdb44285034f74a10b35e8 100644 (file)
@@ -7,12 +7,16 @@
 #
 # Usage:
 #   make -f src/tests/multi-server/all.mk test.multi-server                         # run all tests
-#   make -f src/tests/multi-server/all.mk test.multi-server.5hs-autoaccept.short    # run single test
+#   make -f src/tests/multi-server/all.mk test.multi-server.ci                      # run all ci tests
+#   make -f src/tests/multi-server/all.mk test.multi-server.proxy-accept.short_ci   # run single test
 #   make -f src/tests/multi-server/all.mk clean.test.multi-server                   # clean logs
 #
 
 SHELL := /bin/bash
 
+PROFILE ?= default-profiling
+BUILD_PLATFORM ?=
+
 #
 #  Allow for stand-alone builds from the local directory.
 #
@@ -28,6 +32,10 @@ endif
 DIR    := $(abspath ${top_srcdir}/src/tests/multi-server)
 OUTPUT := $(abspath $(BUILD_DIR)/tests/multi-server)
 
+GIT_BRANCH        := $(or $(shell git -C $(top_srcdir) rev-parse --abbrev-ref HEAD 2>/dev/null | tr '/' '_'),unknown-branch)
+GIT_COMMIT        := $(or $(shell git -C $(top_srcdir) rev-parse --short HEAD 2>/dev/null),unknown-commit)
+PROF_RESULTS_ROOT := $(abspath $(top_srcdir)/prof-results)
+
 # FIXME: We should be using packaged versions of the multi-server test framework
 # instead of cloning from git.
 TEST_MULTI_SERVER_GIT_REPO   := https://github.com/InkbridgeNetworks/radenv.git
@@ -139,6 +147,9 @@ TEST_MULTI_SERVER_RENDERED.${1}.${2}     := $$(patsubst $$(DIR)/tests/${1}/%.j2,
 
 $$(foreach j,$$(TEST_MULTI_SERVER_JINJA_FILES.${1}.${2}),$$(eval $$(call TEST_MULTI_SERVER_RENDER,${1},${2},${3},$$j)))
 
+.PHONY: render.test.multi-server.${1}.${2}
+render.test.multi-server.${1}.${2}: $$(TEST_MULTI_SERVER_RENDERED.${1}.${2})
+
 .PHONY: test.multi-server.${1}.${2}
 test.multi-server.${1}.${2}: $$(TEST_MULTI_SERVER_RENDERED.${1}.${2})
        $$(eval CMD := cd $(TEST_MULTI_SERVER_FRAMEWORK_DIR) && . .venv/bin/activate && DATA_PATH="${4}" python3 -m src.multi_server_test $(TEST_MULTI_SERVER_FLAGS) --project-name "${1}-${2}" --compose "${4}/environment.yml" --test "${4}/template.yml" --use-files --listener-dir "${4}/listener" --log-dir "${4}/logs" --output "${4}/logs/result.log")
@@ -165,19 +176,72 @@ test.multi-server.${1}.${2}: $$(TEST_MULTI_SERVER_RENDERED.${1}.${2})
        }
 endef
 
+#
+#  TEST_MULTI_SERVER_PROF_INSTANCE - like TEST_MULTI_SERVER_INSTANCE but
+#  computes PROF_RESULTS_PATH and exports it to docker compose so valgrind
+#  output lands in:
+#    $(PROF_RESULTS_ROOT)/<suite>/<test>/<branch>/<commit>/<run-index>
+#
+#  ${1} = suite dir name
+#  ${2} = test name
+#  ${3} = params file path
+#  ${4} = test output directory
+#
+define TEST_MULTI_SERVER_PROF_INSTANCE
+TEST_MULTI_SERVER_JINJA_FILES.${1}.${2}  := $$(wildcard $$(DIR)/tests/${1}/*.j2)
+TEST_MULTI_SERVER_RENDERED.${1}.${2}     := $$(patsubst $$(DIR)/tests/${1}/%.j2,${4}/%,$$(TEST_MULTI_SERVER_JINJA_FILES.${1}.${2}))
+
+$$(foreach j,$$(TEST_MULTI_SERVER_JINJA_FILES.${1}.${2}),$$(eval $$(call TEST_MULTI_SERVER_RENDER,${1},${2},${3},$$j)))
+
+.PHONY: render.test.multi-server.${1}.${2}
+render.test.multi-server.${1}.${2}: $$(TEST_MULTI_SERVER_RENDERED.${1}.${2})
+
+.PHONY: test.multi-server.${1}.${2}
+test.multi-server.${1}.${2}: $$(TEST_MULTI_SERVER_RENDERED.${1}.${2})
+       ${Q}mkdir -p "${4}/logs" "${4}/listener"
+       ${Q}echo "MULTI-SERVER-TEST test.multi-server.${1}.${2}"
+       ${Q}PROF_BASE="$(PROF_RESULTS_ROOT)/${1}/${2}/$(GIT_BRANCH)/$(GIT_COMMIT)"; \
+       EXISTING=$$$$( find "$$$$PROF_BASE" -mindepth 1 -maxdepth 1 -type d 2>/dev/null | wc -l | tr -d ' ' ); \
+       RUN_INDEX=$$$$((EXISTING + 1)); \
+       PROF_RESULTS_PATH="$$$$PROF_BASE/$$$$RUN_INDEX"; \
+       mkdir -p "$$$$PROF_RESULTS_PATH" && \
+       echo "PROF_RESULTS_PATH: $$$$PROF_RESULTS_PATH" && \
+       cd $(TEST_MULTI_SERVER_FRAMEWORK_DIR) && . .venv/bin/activate && \
+       DATA_PATH="${4}" PROF_RESULTS_PATH="$$$$PROF_RESULTS_PATH" \
+       python3 -m src.multi_server_test $(TEST_MULTI_SERVER_FLAGS) --project-name "${1}-${2}" --compose "${4}/environment.yml" --test "${4}/template.yml" --use-files --listener-dir "${4}/listener" --log-dir "${4}/logs" --output "${4}/logs/result.log" \
+       > "${4}/logs/stdout.log" 2> "${4}/logs/stderr.log" || \
+       { \
+           echo "FAILED: test.multi-server.${1}.${2}"; \
+           for f in ${4}/logs/* ${4}/listener/*; do \
+               [ -f "$$$$f" ] || continue; \
+               echo ""; \
+               echo "=== $$$$f ==="; \
+               case "$$$$f" in \
+                   */listener/*) \
+                       echo "-- line-type counts --"; \
+                       awk '{print $$$$1}' "$$$$f" | sort | uniq -c; \
+                       echo "-- last 200 lines --"; \
+                       ;; \
+               esac; \
+               tail -200 "$$$$f"; \
+           done; \
+           exit 1; \
+       }
+endef
+
 #
 #  TEST_MULTI_SERVER - define all test instances for a suite.
 #
 #  Discovers *.yml param files in the suite directory and generates
 #  render + test targets for each.
 #
-#  ${1} = suite dir name (e.g., 5hs-autoaccept)
+#  ${1} = suite dir name (e.g. proxy-accept)
 #
 define TEST_MULTI_SERVER
 TEST_MULTI_SERVER_PARAM_FILES.${1} := $$(wildcard $$(DIR)/tests/${1}/*.test.yml)
 TEST_MULTI_SERVER_TESTS.${1}       := $$(foreach p,$$(TEST_MULTI_SERVER_PARAM_FILES.${1}),test.multi-server.${1}.$$(subst .,_,$$(patsubst %.test.yml,%,$$(notdir $$p))))
 
-$$(foreach p,$$(TEST_MULTI_SERVER_PARAM_FILES.${1}),$$(eval $$(call TEST_MULTI_SERVER_INSTANCE,${1},$$(subst .,_,$$(patsubst %.test.yml,%,$$(notdir $$p))),$$p,$(OUTPUT)/${1}/$$(subst .,_,$$(patsubst %.test.yml,%,$$(notdir $$p))))))
+$$(foreach p,$$(TEST_MULTI_SERVER_PARAM_FILES.${1}),$$(eval $$(call $(if $(filter prof-%,${1}),TEST_MULTI_SERVER_PROF_INSTANCE,TEST_MULTI_SERVER_INSTANCE),${1},$$(subst .,_,$$(patsubst %.test.yml,%,$$(notdir $$p))),$$p,$(OUTPUT)/${1}/$$(subst .,_,$$(patsubst %.test.yml,%,$$(notdir $$p))))))
 endef
 
 ######################################################################
@@ -195,6 +259,8 @@ $(foreach s,$(TEST_MULTI_SERVER_SUITES),$(eval $(call TEST_MULTI_SERVER,$s)))
 
 TEST_MULTI_SERVER_ALL_TESTS := $(foreach s,$(TEST_MULTI_SERVER_SUITES),$(TEST_MULTI_SERVER_TESTS.$(s)))
 
+TEST_MULTI_SERVER_PROF_TESTS := $(foreach s,$(filter prof-%,$(TEST_MULTI_SERVER_SUITES)),$(TEST_MULTI_SERVER_TESTS.$(s)))
+
 ######################################################################
 #
 #  Top-level targets
@@ -215,6 +281,70 @@ TEST_MULTI_SERVER_CI_TESTS := $(filter %_ci,$(TEST_MULTI_SERVER_ALL_TESTS))
 .PHONY: test.multi-server.ci
 test.multi-server.ci: $(TEST_MULTI_SERVER_CI_TESTS)
 
+#
+#  Ensure the freeradius-prof image is present before running
+#  any of the profiling tests.
+#
+
+
+# Crossbuild image
+FREERADIUS_CROSSBUILD_IMAGE := freeradius40x-build/ubuntu24:latest
+# Base profiling image, FreeRADIUS not built on this image
+FREERADIUS_PROF_IMAGE := freeradius4-$(PROFILE)/ubuntu24:latest
+# Multi-server profiling image; FreeRADIUS dev build specifically for profiling
+FREERADIUS_RADENV_PROF_IMAGE := freeradius-prof:latest
+
+.PHONY: freeradius-prof.image
+freeradius-prof.image:
+       ${Q}if [ -n "$(FORCE_IMAGE_REBUILD)" ]; then \
+               $(MAKE) -C $(top_srcdir) crossbuild.ubuntu24.profile.regen; \
+               $(MAKE) -C $(top_srcdir) crossbuild.ubuntu24.profile.build; \
+               ./src/tests/multi-server/scripts/docker/build/build_image.sh $(if $(BUILD_PLATFORM),BUILD_PLATFORM=$(BUILD_PLATFORM)); \
+       elif [ -z "$$(docker images -q $(FREERADIUS_PROF_IMAGE) 2>/dev/null)" ]; then \
+               $(MAKE) -C $(top_srcdir) crossbuild.ubuntu24.profile.regen; \
+               $(MAKE) -C $(top_srcdir) crossbuild.ubuntu24.profile.build; \
+               ./src/tests/multi-server/scripts/docker/build/build_image.sh $(if $(BUILD_PLATFORM),BUILD_PLATFORM=$(BUILD_PLATFORM)); \
+       elif [ -z "$$(docker images -q $(FREERADIUS_RADENV_PROF_IMAGE) 2>/dev/null)" ]; then \
+               ./src/tests/multi-server/scripts/docker/build/build_image.sh $(if $(BUILD_PLATFORM),BUILD_PLATFORM=$(BUILD_PLATFORM)); \
+       else \
+               echo "$(FREERADIUS_PROF_IMAGE) and $(FREERADIUS_RADENV_PROF_IMAGE) available, skipping image creation"; \
+       fi
+
+$(TEST_MULTI_SERVER_PROF_TESTS): freeradius-prof.image
+
+#
+#  Copy the valgrind profiling helper script into each prof test's output dir
+#  so it is available alongside the rendered test configs.
+#
+PROFILING_SCRIPT_SRC := $(DIR)/scripts/profiling/start_valgrind_profiling.sh
+
+define TEST_MULTI_SERVER_PROF_SCRIPT
+$(OUTPUT)/${1}/${2}/start_valgrind_profiling.sh: $(PROFILING_SCRIPT_SRC)
+       $${Q}mkdir -p $$(@D)
+       $${Q}cp $$< $$@
+
+test.multi-server.${1}.${2}: $(OUTPUT)/${1}/${2}/start_valgrind_profiling.sh
+endef
+
+$(foreach s,$(filter prof-%,$(TEST_MULTI_SERVER_SUITES)),$(foreach p,$(TEST_MULTI_SERVER_PARAM_FILES.$(s)),$(eval $(call TEST_MULTI_SERVER_PROF_SCRIPT,$(s),$(subst .,_,$(patsubst %.test.yml,%,$(notdir $(p))))))))
+
+#
+#  Ensure the ldap image is present before running prof-ldap tests.
+#  Builds it automatically via docker.openldap.prof if not found.
+#
+OPENLDAP_PROF_IMAGE := freeradius4/openldap-prof:latest
+
+.PHONY: openldap.image
+openldap.image:
+       ${Q}if [ -n "$(FORCE_IMAGE_REBUILD)" ] || [ -z "$$(docker images -q $(OPENLDAP_PROF_IMAGE) 2>/dev/null)" ]; then \
+               $(MAKE) -C $(top_srcdir) docker.openldap.prof; \
+       else \
+               echo "$(OPENLDAP_PROF_IMAGE) available, skipping image creation"; \
+       fi
+       ${Q}docker tag $(OPENLDAP_PROF_IMAGE) openldap:latest
+
+$(TEST_MULTI_SERVER_TESTS.prof-ldap): openldap.image
+
 .PHONY: clean.test.multi-server
 clean.test.multi-server:
        ${Q}rm -rf $(OUTPUT)
diff --git a/src/tests/multi-server/configs/freeradius/common/mods-available/ldap b/src/tests/multi-server/configs/freeradius/common/mods-available/ldap
new file mode 100644 (file)
index 0000000..7dd2725
--- /dev/null
@@ -0,0 +1,30 @@
+ldap {
+       server = 'openldap'
+       identity = 'cn=admin,dc=example,dc=com'
+       password = 'adminpassword'
+       base_dn = 'dc=example,dc=com'
+
+       update {
+               control.Password.With-Header    += 'userPassword'
+       }
+
+       user {
+               base_dn = "${..base_dn}"
+               filter = "(uid=%{Stripped-User-Name || User-Name})"
+       }
+
+       options {
+               res_timeout = 10
+               srv_timelimit = 3
+               net_timeout = 10
+       }
+
+       pool {
+               start = 0
+               min = 1
+               max = 5
+               connecting = 2
+               uses = 0
+               lifetime = 0
+       }
+}
diff --git a/src/tests/multi-server/configs/freeradius/common/mods-available/pap b/src/tests/multi-server/configs/freeradius/common/mods-available/pap
new file mode 100644 (file)
index 0000000..5c6cde5
--- /dev/null
@@ -0,0 +1,4 @@
+
+pap {
+       password_attribute = User-Password
+}
diff --git a/src/tests/multi-server/configs/freeradius/common/mods-available/sql b/src/tests/multi-server/configs/freeradius/common/mods-available/sql
new file mode 100644 (file)
index 0000000..9c29375
--- /dev/null
@@ -0,0 +1,35 @@
+sql {
+    dialect = "mysql"
+    driver = "${dialect}"
+
+    $-INCLUDE ${modconfdir}/sql/driver/${driver}
+
+    server = "mariadb"
+    port = 3306
+    login = "radius"
+    password = "radpass"
+
+    radius_db = "radius"
+
+    acct_table1 = "radacct"
+    acct_table2 = "radacct"
+    postauth_table = "radpostauth"
+    authcheck_table = "radcheck"
+    groupcheck_table = "radgroupcheck"
+    authreply_table = "radreply"
+    groupreply_table = "radgroupreply"
+    usergroup_table = "radusergroup"
+
+    pool {
+        start = 0
+        min = 1
+        max = 32
+        connecting = 2
+        uses = 0
+        lifetime = 0
+    }
+
+    group_attribute = "${.:instance}-Group"
+
+    $INCLUDE ${modconfdir}/${.:name}/main/${dialect}/queries.conf
+}
diff --git a/src/tests/multi-server/configs/freeradius/profiling-ldap/load-generator-packets/packet.conf b/src/tests/multi-server/configs/freeradius/profiling-ldap/load-generator-packets/packet.conf
new file mode 100644 (file)
index 0000000..46c2a8f
--- /dev/null
@@ -0,0 +1,3 @@
+User-Name       = "testuser"
+User-Password   = "testpass"
+Calling-Station-ID = "F1-F2-F3-F4-F5-F6"
diff --git a/src/tests/multi-server/configs/freeradius/profiling-ldap/mods-config/files/authorize b/src/tests/multi-server/configs/freeradius/profiling-ldap/mods-config/files/authorize
new file mode 100644 (file)
index 0000000..eba3cb9
--- /dev/null
@@ -0,0 +1 @@
+testuser Password.Cleartext := "testpass"
diff --git a/src/tests/multi-server/configs/freeradius/profiling-ldap/proto_load_config.env b/src/tests/multi-server/configs/freeradius/profiling-ldap/proto_load_config.env
new file mode 100644 (file)
index 0000000..f97e41e
--- /dev/null
@@ -0,0 +1,14 @@
+export TEST_LOADGEN_START_PPS="100"
+export TEST_LOADGEN_MAX_PPS="100"
+export TEST_LOADGEN_DURATION="60"
+export TEST_LOADGEN_STEP="100"
+export TEST_LOADGEN_PARALLEL="1"
+export TEST_LOADGEN_MAX_BACKLOG="1000"
+export TEST_LOADGEN_REPEAT="no"
+export TEST_LOADGEN_NUM_MESSAGES=0
+
+for ((pps=$TEST_LOADGEN_START_PPS; pps<=$TEST_LOADGEN_MAX_PPS; pps+=$TEST_LOADGEN_STEP)); do
+  TEST_LOADGEN_NUM_MESSAGES=$((TEST_LOADGEN_NUM_MESSAGES + TEST_LOADGEN_DURATION * pps))
+done
+
+export TEST_LOADGEN_NUM_MESSAGES
diff --git a/src/tests/multi-server/configs/freeradius/profiling-ldap/radiusd.conf.j2 b/src/tests/multi-server/configs/freeradius/profiling-ldap/radiusd.conf.j2
new file mode 100644 (file)
index 0000000..f699156
--- /dev/null
@@ -0,0 +1,139 @@
+name       = profiling-server
+
+raddbdir   = /etc/freeradius
+confdir    = ${raddbdir}
+modconfdir = ${confdir}/mods-config
+logdir     = /var/log/freeradius
+radacctdir = ${logdir}/radacct
+
+security {
+       allow_core_dumps = yes
+}
+
+modules {
+
+       # Common ldap module configuration
+       {% include "freeradius/common/mods-available/ldap" %}
+
+       # Common pap module configuration
+       {% include "freeradius/common/mods-available/pap" %}
+
+       # Common always module configuration, required for the control policy
+       {% include "freeradius/common/mods-available/always" %}
+
+}
+
+policy {
+
+       # Common control policy configuration, needed for accept action
+       {% include "freeradius/common/policy.d/control" %}
+
+}
+
+server profiling-server {
+
+       namespace = radius
+
+       listen load {
+               handler = load
+               type = Access-Request
+               transport = step
+
+               step {
+
+                       # Default packet config to use by proto_load module
+                       filename = ${confdir}/load-generator-packets/packet.conf
+
+                       # Saving proto_load statistics disabled by default, can be enabled for debugging purposes.
+                       csv = ${confdir}/stats/load-generator-stats.csv
+
+                       max_attributes = 64
+
+                       #
+                       # The load profile is configured via environment variables set
+                       # in the testcase configuration files.
+                       #
+                       start_pps = $ENV{TEST_LOADGEN_START_PPS}
+                       max_pps   = $ENV{TEST_LOADGEN_MAX_PPS}
+                       duration  = $ENV{TEST_LOADGEN_DURATION}
+                       step = $ENV{TEST_LOADGEN_STEP}
+                       max_backlog = $ENV{TEST_LOADGEN_MAX_BACKLOG}
+                       parallel = $ENV{TEST_LOADGEN_PARALLEL}
+                       num_messages = $ENV{TEST_LOADGEN_NUM_MESSAGES}
+                       repeat = no
+               }
+       }
+
+       listen authentication {
+               type = Access-Request
+               transport = udp
+               require_message_authenticator = auto
+               limit_proxy_state = auto
+
+               limit {
+                       max_clients = 256
+                       max_connections = 256
+                       idle_timeout = 60.0
+                       dynamic_timeout = 600.0
+                       nak_lifetime = 30.0
+                       cleanup_delay = 5.0
+               }
+
+               udp {
+                       ipaddr = *
+                       port = 1812
+                       networks {
+                               allow = 127/8
+                               allow = 192.0.2/24
+                       }
+               }
+
+               tcp {
+                       ipaddr = *
+                       port = 1812
+                       networks {
+                               allow = 127/8
+                               allow = 192.0.2/24
+                       }
+               }
+       }
+
+       listen authentication {
+               type = Access-Request
+               transport = tcp
+
+               tcp {
+                       ipaddr = *
+                       port = 1812
+                       networks {
+                               allow = 127/8
+                               allow = 192.0.2/24
+                       }
+               }
+       }
+
+       client localhost {
+               shortname = client-localhost
+               ipaddr = *
+               secret = testing123
+       }
+
+       recv Access-Request {
+               ldap
+               # pap module always listed last
+               pap
+       }
+
+       authenticate pap {
+               pap
+       }
+
+       send Access-Accept {
+
+       }
+
+       send Access-Reject {
+
+       }
+
+}
diff --git a/src/tests/multi-server/configs/freeradius/profiling-ldap/stats/load-generator-stats.csv b/src/tests/multi-server/configs/freeradius/profiling-ldap/stats/load-generator-stats.csv
new file mode 100644 (file)
index 0000000..34c1880
--- /dev/null
@@ -0,0 +1 @@
+"time","last_packet","rtt","rttvar","pps","pps_accepted","sent","received","backlog","max_backlog","<usec","us","10us","100us","ms","10ms","100ms","s","blocked"
diff --git a/src/tests/multi-server/configs/freeradius/profiling-ldap/template.d/virtual-server-templates b/src/tests/multi-server/configs/freeradius/profiling-ldap/template.d/virtual-server-templates
new file mode 100644 (file)
index 0000000..7c410b4
--- /dev/null
@@ -0,0 +1,3 @@
+#
+# virtual server templates placeholder
+#
diff --git a/src/tests/multi-server/configs/freeradius/profiling-mysql/load-generator-packets/packet.conf b/src/tests/multi-server/configs/freeradius/profiling-mysql/load-generator-packets/packet.conf
new file mode 100644 (file)
index 0000000..46c2a8f
--- /dev/null
@@ -0,0 +1,3 @@
+User-Name       = "testuser"
+User-Password   = "testpass"
+Calling-Station-ID = "F1-F2-F3-F4-F5-F6"
diff --git a/src/tests/multi-server/configs/freeradius/profiling-mysql/mods-config/files/authorize b/src/tests/multi-server/configs/freeradius/profiling-mysql/mods-config/files/authorize
new file mode 100644 (file)
index 0000000..eba3cb9
--- /dev/null
@@ -0,0 +1 @@
+testuser Password.Cleartext := "testpass"
diff --git a/src/tests/multi-server/configs/freeradius/profiling-mysql/proto_load_config.env b/src/tests/multi-server/configs/freeradius/profiling-mysql/proto_load_config.env
new file mode 100644 (file)
index 0000000..f97e41e
--- /dev/null
@@ -0,0 +1,14 @@
+export TEST_LOADGEN_START_PPS="100"
+export TEST_LOADGEN_MAX_PPS="100"
+export TEST_LOADGEN_DURATION="60"
+export TEST_LOADGEN_STEP="100"
+export TEST_LOADGEN_PARALLEL="1"
+export TEST_LOADGEN_MAX_BACKLOG="1000"
+export TEST_LOADGEN_REPEAT="no"
+export TEST_LOADGEN_NUM_MESSAGES=0
+
+for ((pps=$TEST_LOADGEN_START_PPS; pps<=$TEST_LOADGEN_MAX_PPS; pps+=$TEST_LOADGEN_STEP)); do
+  TEST_LOADGEN_NUM_MESSAGES=$((TEST_LOADGEN_NUM_MESSAGES + TEST_LOADGEN_DURATION * pps))
+done
+
+export TEST_LOADGEN_NUM_MESSAGES
diff --git a/src/tests/multi-server/configs/freeradius/profiling-mysql/radiusd.conf.j2 b/src/tests/multi-server/configs/freeradius/profiling-mysql/radiusd.conf.j2
new file mode 100644 (file)
index 0000000..288d426
--- /dev/null
@@ -0,0 +1,139 @@
+name       = profiling-server
+
+raddbdir   = /etc/freeradius
+confdir    = ${raddbdir}
+modconfdir = ${confdir}/mods-config
+logdir     = /var/log/freeradius
+radacctdir = ${logdir}/radacct
+
+security {
+       allow_core_dumps = yes
+}
+
+modules {
+
+       # Common sql module configuration
+       {% include "freeradius/common/mods-available/sql" %}
+
+       # Common pap module configuration
+       {% include "freeradius/common/mods-available/pap" %}
+
+       # Common always module configuration, required for the control policy
+       {% include "freeradius/common/mods-available/always" %}
+
+}
+
+policy {
+
+       # Common control policy configuration, needed for accept action
+       {% include "freeradius/common/policy.d/control" %}
+
+}
+
+server profiling-server {
+
+       namespace = radius
+
+       listen load {
+               handler = load
+               type = Access-Request
+               transport = step
+
+               step {
+
+                       # Default packet config to use by proto_load module
+                       filename = ${confdir}/load-generator-packets/packet.conf
+
+                       # Saving proto_load statistics disabled by default, can be enabled for debugging purposes.
+                       csv = ${confdir}/stats/load-generator-stats.csv
+
+                       max_attributes = 64
+
+                       #
+                       # The load profile is configured via environment variables set
+                       # in the testcase configuration files.
+                       #
+                       start_pps = $ENV{TEST_LOADGEN_START_PPS}
+                       max_pps   = $ENV{TEST_LOADGEN_MAX_PPS}
+                       duration  = $ENV{TEST_LOADGEN_DURATION}
+                       step = $ENV{TEST_LOADGEN_STEP}
+                       max_backlog = $ENV{TEST_LOADGEN_MAX_BACKLOG}
+                       parallel = $ENV{TEST_LOADGEN_PARALLEL}
+                       num_messages = $ENV{TEST_LOADGEN_NUM_MESSAGES}
+                       repeat = no
+               }
+       }
+
+       listen authentication {
+               type = Access-Request
+               transport = udp
+               require_message_authenticator = auto
+               limit_proxy_state = auto
+
+               limit {
+                       max_clients = 256
+                       max_connections = 256
+                       idle_timeout = 60.0
+                       dynamic_timeout = 600.0
+                       nak_lifetime = 30.0
+                       cleanup_delay = 5.0
+               }
+
+               udp {
+                       ipaddr = *
+                       port = 1812
+                       networks {
+                               allow = 127/8
+                               allow = 192.0.2/24
+                       }
+               }
+
+               tcp {
+                       ipaddr = *
+                       port = 1812
+                       networks {
+                               allow = 127/8
+                               allow = 192.0.2/24
+                       }
+               }
+       }
+
+       listen authentication {
+               type = Access-Request
+               transport = tcp
+
+               tcp {
+                       ipaddr = *
+                       port = 1812
+                       networks {
+                               allow = 127/8
+                               allow = 192.0.2/24
+                       }
+               }
+       }
+
+       client localhost {
+               shortname = client-localhost
+               ipaddr = *
+               secret = testing123
+       }
+
+       recv Access-Request {
+               sql
+               # pap module always listed last
+               pap
+       }
+
+       authenticate pap {
+               pap
+       }
+
+       send Access-Accept {
+
+       }
+
+       send Access-Reject {
+
+       }
+
+}
diff --git a/src/tests/multi-server/configs/freeradius/profiling-mysql/stats/load-generator-stats.csv b/src/tests/multi-server/configs/freeradius/profiling-mysql/stats/load-generator-stats.csv
new file mode 100644 (file)
index 0000000..34c1880
--- /dev/null
@@ -0,0 +1 @@
+"time","last_packet","rtt","rttvar","pps","pps_accepted","sent","received","backlog","max_backlog","<usec","us","10us","100us","ms","10ms","100ms","s","blocked"
diff --git a/src/tests/multi-server/configs/freeradius/profiling-mysql/template.d/virtual-server-templates b/src/tests/multi-server/configs/freeradius/profiling-mysql/template.d/virtual-server-templates
new file mode 100644 (file)
index 0000000..7c410b4
--- /dev/null
@@ -0,0 +1,3 @@
+#
+# virtual server templates placeholder
+#
diff --git a/src/tests/multi-server/configs/freeradius/profiling-server-pap-auth/load-generator-packets/packet.conf b/src/tests/multi-server/configs/freeradius/profiling-server-pap-auth/load-generator-packets/packet.conf
new file mode 100644 (file)
index 0000000..46c2a8f
--- /dev/null
@@ -0,0 +1,3 @@
+User-Name       = "testuser"
+User-Password   = "testpass"
+Calling-Station-ID = "F1-F2-F3-F4-F5-F6"
diff --git a/src/tests/multi-server/configs/freeradius/profiling-server-pap-auth/mods-config/files/authorize b/src/tests/multi-server/configs/freeradius/profiling-server-pap-auth/mods-config/files/authorize
new file mode 100644 (file)
index 0000000..eba3cb9
--- /dev/null
@@ -0,0 +1 @@
+testuser Password.Cleartext := "testpass"
diff --git a/src/tests/multi-server/configs/freeradius/profiling-server-pap-auth/proto_load_config.env b/src/tests/multi-server/configs/freeradius/profiling-server-pap-auth/proto_load_config.env
new file mode 100644 (file)
index 0000000..f97e41e
--- /dev/null
@@ -0,0 +1,14 @@
+export TEST_LOADGEN_START_PPS="100"
+export TEST_LOADGEN_MAX_PPS="100"
+export TEST_LOADGEN_DURATION="60"
+export TEST_LOADGEN_STEP="100"
+export TEST_LOADGEN_PARALLEL="1"
+export TEST_LOADGEN_MAX_BACKLOG="1000"
+export TEST_LOADGEN_REPEAT="no"
+export TEST_LOADGEN_NUM_MESSAGES=0
+
+for ((pps=$TEST_LOADGEN_START_PPS; pps<=$TEST_LOADGEN_MAX_PPS; pps+=$TEST_LOADGEN_STEP)); do
+  TEST_LOADGEN_NUM_MESSAGES=$((TEST_LOADGEN_NUM_MESSAGES + TEST_LOADGEN_DURATION * pps))
+done
+
+export TEST_LOADGEN_NUM_MESSAGES
diff --git a/src/tests/multi-server/configs/freeradius/profiling-server-pap-auth/radiusd.conf.j2 b/src/tests/multi-server/configs/freeradius/profiling-server-pap-auth/radiusd.conf.j2
new file mode 100644 (file)
index 0000000..f3cf677
--- /dev/null
@@ -0,0 +1,139 @@
+name       = profiling-server
+
+raddbdir   = /etc/freeradius
+confdir    = ${raddbdir}
+modconfdir = ${confdir}/mods-config
+logdir     = /var/log/freeradius
+radacctdir = ${logdir}/radacct
+
+security {
+       allow_core_dumps = yes
+}
+
+modules {
+
+       # Common files module configuration
+       {% include "freeradius/common/mods-available/files" %}
+
+       # Common pap module configuration
+       {% include "freeradius/common/mods-available/pap" %}
+
+       # Common always module configuration, required for the control policy
+       {% include "freeradius/common/mods-available/always" %}
+
+}
+
+policy {
+
+       # Common control policy configuration, needed for accept action
+       {% include "freeradius/common/policy.d/control" %}
+
+}
+
+server profiling-server {
+
+       namespace = radius
+
+       listen load {
+               handler = load
+               type = Access-Request
+               transport = step
+
+               step {
+
+                       # Default packet config to use by proto_load module
+                       filename = ${confdir}/load-generator-packets/packet.conf
+
+                       # Saving proto_load statistics disabled by default, can be enabled for debugging purposes.
+                       csv = ${confdir}/stats/load-generator-stats.csv
+
+                       max_attributes = 64
+
+                       #
+                       # The load profile is configured via environment variables set
+                       # in the testcase configuration files.
+                       #
+                       start_pps = $ENV{TEST_LOADGEN_START_PPS}
+                       max_pps   = $ENV{TEST_LOADGEN_MAX_PPS}
+                       duration  = $ENV{TEST_LOADGEN_DURATION}
+                       step = $ENV{TEST_LOADGEN_STEP}
+                       max_backlog = $ENV{TEST_LOADGEN_MAX_BACKLOG}
+                       parallel = $ENV{TEST_LOADGEN_PARALLEL}
+                       num_messages = $ENV{TEST_LOADGEN_NUM_MESSAGES}
+                       repeat = no
+               }
+       }
+
+       listen authentication {
+               type = Access-Request
+               transport = udp
+               require_message_authenticator = auto
+               limit_proxy_state = auto
+
+               limit {
+                       max_clients = 256
+                       max_connections = 256
+                       idle_timeout = 60.0
+                       dynamic_timeout = 600.0
+                       nak_lifetime = 30.0
+                       cleanup_delay = 5.0
+               }
+
+               udp {
+                       ipaddr = *
+                       port = 1812
+                       networks {
+                               allow = 127/8
+                               allow = 192.0.2/24
+                       }
+               }
+
+               tcp {
+                       ipaddr = *
+                       port = 1812
+                       networks {
+                               allow = 127/8
+                               allow = 192.0.2/24
+                       }
+               }
+       }
+
+       listen authentication {
+               type = Access-Request
+               transport = tcp
+
+               tcp {
+                       ipaddr = *
+                       port = 1812
+                       networks {
+                               allow = 127/8
+                               allow = 192.0.2/24
+                       }
+               }
+       }
+
+       client localhost {
+               shortname = client-localhost
+               ipaddr = *
+               secret = testing123
+       }
+
+       recv Access-Request {
+               files
+               # pap module always listed last
+               pap
+       }
+
+       authenticate pap {
+               pap
+       }
+
+       send Access-Accept {
+
+       }
+
+       send Access-Reject {
+
+       }
+
+}
diff --git a/src/tests/multi-server/configs/freeradius/profiling-server-pap-auth/stats/load-generator-stats.csv b/src/tests/multi-server/configs/freeradius/profiling-server-pap-auth/stats/load-generator-stats.csv
new file mode 100644 (file)
index 0000000..34c1880
--- /dev/null
@@ -0,0 +1 @@
+"time","last_packet","rtt","rttvar","pps","pps_accepted","sent","received","backlog","max_backlog","<usec","us","10us","100us","ms","10ms","100ms","s","blocked"
diff --git a/src/tests/multi-server/configs/freeradius/profiling-server-pap-auth/template.d/virtual-server-templates b/src/tests/multi-server/configs/freeradius/profiling-server-pap-auth/template.d/virtual-server-templates
new file mode 100644 (file)
index 0000000..7c410b4
--- /dev/null
@@ -0,0 +1,3 @@
+#
+# virtual server templates placeholder
+#
diff --git a/src/tests/multi-server/configs/freeradius/profiling-server/load-generator-packets/packet.conf b/src/tests/multi-server/configs/freeradius/profiling-server/load-generator-packets/packet.conf
new file mode 100644 (file)
index 0000000..46c2a8f
--- /dev/null
@@ -0,0 +1,3 @@
+User-Name       = "testuser"
+User-Password   = "testpass"
+Calling-Station-ID = "F1-F2-F3-F4-F5-F6"
diff --git a/src/tests/multi-server/configs/freeradius/profiling-server/mods-config/files/authorize b/src/tests/multi-server/configs/freeradius/profiling-server/mods-config/files/authorize
new file mode 100644 (file)
index 0000000..eba3cb9
--- /dev/null
@@ -0,0 +1 @@
+testuser Password.Cleartext := "testpass"
diff --git a/src/tests/multi-server/configs/freeradius/profiling-server/proto_load_config.env b/src/tests/multi-server/configs/freeradius/profiling-server/proto_load_config.env
new file mode 100644 (file)
index 0000000..f97e41e
--- /dev/null
@@ -0,0 +1,14 @@
+export TEST_LOADGEN_START_PPS="100"
+export TEST_LOADGEN_MAX_PPS="100"
+export TEST_LOADGEN_DURATION="60"
+export TEST_LOADGEN_STEP="100"
+export TEST_LOADGEN_PARALLEL="1"
+export TEST_LOADGEN_MAX_BACKLOG="1000"
+export TEST_LOADGEN_REPEAT="no"
+export TEST_LOADGEN_NUM_MESSAGES=0
+
+for ((pps=$TEST_LOADGEN_START_PPS; pps<=$TEST_LOADGEN_MAX_PPS; pps+=$TEST_LOADGEN_STEP)); do
+  TEST_LOADGEN_NUM_MESSAGES=$((TEST_LOADGEN_NUM_MESSAGES + TEST_LOADGEN_DURATION * pps))
+done
+
+export TEST_LOADGEN_NUM_MESSAGES
diff --git a/src/tests/multi-server/configs/freeradius/profiling-server/radiusd.conf.j2 b/src/tests/multi-server/configs/freeradius/profiling-server/radiusd.conf.j2
new file mode 100644 (file)
index 0000000..bd04320
--- /dev/null
@@ -0,0 +1,125 @@
+name       = profiling-server
+
+raddbdir   = /etc/freeradius
+confdir    = ${raddbdir}
+modconfdir = ${confdir}/mods-config
+logdir     = /var/log/freeradius
+radacctdir = ${logdir}/radacct
+
+security {
+       allow_core_dumps = yes
+}
+
+modules {
+
+       # Common always module configuration, required for the control policy
+       {% include "freeradius/common/mods-available/always" %}
+
+}
+
+policy {
+       # Common control policy configuration, needed for accept action
+       {% include "freeradius/common/policy.d/control" %}
+}
+
+server profiling-server {
+
+       namespace = radius
+
+       listen load {
+               handler = load
+               type = Access-Request
+               transport = step
+
+               step {
+
+                       # Default packet config to use by proto_load module
+                       filename = ${confdir}/load-generator-packets/packet.conf
+
+                       # Saving proto_load statistics disabled by default, can be enabled for debugging purposes.
+                       csv = ${confdir}/stats/load-generator-stats.csv
+
+                       max_attributes = 64
+
+                       #
+                       # The load profile is configured via environment variables set
+                       # in the testcase configuration files.
+                       #
+                       start_pps = $ENV{TEST_LOADGEN_START_PPS}
+                       max_pps   = $ENV{TEST_LOADGEN_MAX_PPS}
+                       duration  = $ENV{TEST_LOADGEN_DURATION}
+                       step = $ENV{TEST_LOADGEN_STEP}
+                       max_backlog = $ENV{TEST_LOADGEN_MAX_BACKLOG}
+                       parallel = $ENV{TEST_LOADGEN_PARALLEL}
+                       num_messages = $ENV{TEST_LOADGEN_NUM_MESSAGES}
+                       repeat = no
+               }
+       }
+
+       listen authentication {
+               type = Access-Request
+               transport = udp
+               require_message_authenticator = auto
+               limit_proxy_state = auto
+
+               limit {
+                       max_clients = 256
+                       max_connections = 256
+                       idle_timeout = 60.0
+                       dynamic_timeout = 600.0
+                       nak_lifetime = 30.0
+                       cleanup_delay = 5.0
+               }
+
+               udp {
+                       ipaddr = *
+                       port = 1812
+                       networks {
+                               allow = 127/8
+                               allow = 192.0.2/24
+                       }
+               }
+
+               tcp {
+                       ipaddr = *
+                       port = 1812
+                       networks {
+                               allow = 127/8
+                               allow = 192.0.2/24
+                       }
+               }
+       }
+
+       listen authentication {
+               type = Access-Request
+               transport = tcp
+
+               tcp {
+                       ipaddr = *
+                       port = 1812
+                       networks {
+                               allow = 127/8
+                               allow = 192.0.2/24
+                       }
+               }
+       }
+
+       client localhost {
+               shortname = client-localhost
+               ipaddr = *
+               secret = testing123
+       }
+
+       recv Access-Request {
+               accept
+       }
+
+       send Access-Accept {
+
+       }
+
+       send Access-Reject {
+
+       }
+
+}
diff --git a/src/tests/multi-server/configs/freeradius/profiling-server/stats/load-generator-stats.csv b/src/tests/multi-server/configs/freeradius/profiling-server/stats/load-generator-stats.csv
new file mode 100644 (file)
index 0000000..34c1880
--- /dev/null
@@ -0,0 +1 @@
+"time","last_packet","rtt","rttvar","pps","pps_accepted","sent","received","backlog","max_backlog","<usec","us","10us","100us","ms","10ms","100ms","s","blocked"
diff --git a/src/tests/multi-server/configs/freeradius/profiling-server/template.d/virtual-server-templates b/src/tests/multi-server/configs/freeradius/profiling-server/template.d/virtual-server-templates
new file mode 100644 (file)
index 0000000..7c410b4
--- /dev/null
@@ -0,0 +1,3 @@
+#
+# virtual server templates placeholder
+#
diff --git a/src/tests/multi-server/configs/mariadb/default/init.sql b/src/tests/multi-server/configs/mariadb/default/init.sql
new file mode 100644 (file)
index 0000000..7349768
--- /dev/null
@@ -0,0 +1,136 @@
+CREATE TABLE radcheck (
+  id INT(11) NOT NULL AUTO_INCREMENT,
+  username VARCHAR(64) NOT NULL DEFAULT '',
+  attribute VARCHAR(64) NOT NULL DEFAULT '',
+  op CHAR(2) NOT NULL DEFAULT '==',
+  value VARCHAR(253) NOT NULL DEFAULT '',
+  PRIMARY KEY (id),
+  KEY username (username(32))
+);
+
+INSERT INTO radcheck (username, attribute, op, value)
+VALUES ('testuser', 'Password.Cleartext', ':=', 'testpass');
+
+CREATE TABLE radreply (
+  id INT(11) NOT NULL AUTO_INCREMENT,
+  username VARCHAR(64) NOT NULL DEFAULT '',
+  attribute VARCHAR(64) NOT NULL DEFAULT '',
+  op CHAR(2) NOT NULL DEFAULT '==',
+  value VARCHAR(253) NOT NULL DEFAULT '',
+  PRIMARY KEY (id),
+  KEY username (username(32))
+);
+
+INSERT INTO radreply (username, attribute, op, value)
+VALUES ('testuser', 'Reply-Message', ':=', 'Hello, testuser!');
+
+
+# Set up the other tables similarly
+CREATE TABLE radacct (
+  radacctid bigint(21) NOT NULL auto_increment,
+  acctsessionid varchar(64) NOT NULL default '',
+  acctuniqueid varchar(32) NOT NULL default '',
+  username varchar(64) NOT NULL default '',
+  groupname varchar(64) NOT NULL default '',
+  realm varchar(64) default '',
+  nasipaddress varchar(15) NOT NULL default '',
+  nasportid varchar(32) default NULL,
+  nasporttype varchar(32) default NULL,
+  acctstarttime datetime NULL default NULL,
+  acctupdatetime datetime NULL default NULL,
+  acctstoptime datetime NULL default NULL,
+  acctinterval int(12) default NULL,
+  acctsessiontime int(12) unsigned default NULL,
+  acctauthentic varchar(32) default NULL,
+  connectinfo_start varchar(50) default NULL,
+  connectinfo_stop varchar(50) default NULL,
+  acctinputoctets bigint(20) default NULL,
+  acctoutputoctets bigint(20) default NULL,
+  calledstationid varchar(50) NOT NULL default '',
+  callingstationid varchar(50) NOT NULL default '',
+  acctterminatecause varchar(32) NOT NULL default '',
+  servicetype varchar(32) default NULL,
+  framedprotocol varchar(32) default NULL,
+  framedipaddress varchar(15) NOT NULL default '',
+  framedipv6address varchar(45) NOT NULL default '',
+  framedipv6prefix varchar(45) NOT NULL default '',
+  framedinterfaceid varchar(44) NOT NULL default '',
+  delegatedipv6prefix varchar(45) NOT NULL default '',
+  class varchar(64) default NULL,
+  PRIMARY KEY (radacctid),
+  UNIQUE KEY acctuniqueid (acctuniqueid),
+  KEY username (username),
+  KEY framedipaddress (framedipaddress),
+  KEY framedipv6address (framedipv6address),
+  KEY framedipv6prefix (framedipv6prefix),
+  KEY framedinterfaceid (framedinterfaceid),
+  KEY delegatedipv6prefix (delegatedipv6prefix),
+  KEY acctsessionid (acctsessionid),
+  KEY acctsessiontime (acctsessiontime),
+  KEY acctstarttime (acctstarttime),
+  KEY acctinterval (acctinterval),
+  KEY acctstoptime (acctstoptime),
+  KEY nasipaddress (nasipaddress),
+  INDEX bulk_close (acctstoptime, nasipaddress, acctstarttime)
+) ENGINE = INNODB;
+
+CREATE TABLE radgroupcheck (
+  id int(11) unsigned NOT NULL auto_increment,
+  groupname varchar(64) NOT NULL default '',
+  attribute varchar(64)  NOT NULL default '',
+  op char(2) NOT NULL DEFAULT '==',
+  value varchar(253)  NOT NULL default '',
+  PRIMARY KEY  (id),
+  KEY groupname (groupname(32))
+);
+
+CREATE TABLE radgroupreply (
+  id int(11) unsigned NOT NULL auto_increment,
+  groupname varchar(64) NOT NULL default '',
+  attribute varchar(64)  NOT NULL default '',
+  op char(2) NOT NULL DEFAULT '=',
+  value varchar(253)  NOT NULL default '',
+  PRIMARY KEY  (id),
+  KEY groupname (groupname(32))
+);
+
+CREATE TABLE radusergroup (
+  id int(11) unsigned NOT NULL auto_increment,
+  username varchar(64) NOT NULL default '',
+  groupname varchar(64) NOT NULL default '',
+  priority int(11) NOT NULL default '1',
+  PRIMARY KEY  (id),
+  KEY username (username(32))
+);
+
+CREATE TABLE radpostauth (
+  id int(11) NOT NULL auto_increment,
+  username varchar(64) NOT NULL default '',
+  pass varchar(64) NOT NULL default '',
+  reply varchar(32) NOT NULL default '',
+  authdate timestamp(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6),
+  class varchar(64) NOT NULL default '',
+  PRIMARY KEY  (id)
+) ENGINE = INNODB;
+
+CREATE TABLE nas (
+  id int(10) NOT NULL auto_increment,
+  nasname varchar(128) NOT NULL,
+  shortname varchar(32),
+  type varchar(30) DEFAULT 'other',
+  ports int(5),
+  secret varchar(60) DEFAULT 'secret' NOT NULL,
+  server varchar(64),
+  community varchar(50),
+  description varchar(200) DEFAULT 'RADIUS Client',
+  require_ma varchar(4) DEFAULT 'auto',
+  limit_proxy_state varchar(4) DEFAULT 'auto',
+  PRIMARY KEY (id),
+  KEY nasname (nasname)
+) ENGINE = INNODB;
+
+CREATE TABLE IF NOT EXISTS nasreload (
+  nasipaddress varchar(15) NOT NULL,
+  reloadtime datetime NOT NULL,
+  PRIMARY KEY (nasipaddress)
+) ENGINE = INNODB;
diff --git a/src/tests/multi-server/environments/profiling-ldap.yml.j2 b/src/tests/multi-server/environments/profiling-ldap.yml.j2
new file mode 100644 (file)
index 0000000..615469c
--- /dev/null
@@ -0,0 +1,54 @@
+# ---------------------------------------------------------------
+# Docker Compose Test Environment:
+#
+#   A single server instance for profiling
+#
+# ---------------------------------------------------------------
+x-common-config: &id001
+  cap_add:
+  - NET_ADMIN
+  - SYS_PTRACE
+  environment:
+    TEST_PROJECT_NAME: ${COMPOSE_PROJECT_NAME}
+    TEST_SUBNET: {{ test_subnet | default('172.16.0.0/12') }}
+services:
+  openldap:
+    image: openldap:latest
+    healthcheck:
+      test: ["CMD-SHELL", "ldapsearch -x -H ldap://localhost -D 'cn=admin,dc=example,dc=com' -w adminpassword -b 'dc=example,dc=com' > /dev/null 2>&1"]
+      interval: 2s
+      timeout: 5s
+      retries: 10
+      start_period: 15s
+    <<: *id001
+  profiling-server:
+    image: freeradius-prof:latest
+    depends_on:
+      openldap:
+        condition: service_healthy
+    volumes:
+    # profiling-server server config
+    - ${DATA_PATH}/freeradius/profiling-ldap/radiusd.conf:/etc/freeradius/radiusd.conf
+    # files module configuration
+    - ${DATA_PATH}/freeradius/profiling-ldap/mods-config/files/authorize:/etc/freeradius/mods-config/files/authorize
+    # proto_load packet configuration and statistics output
+    - ${DATA_PATH}/freeradius/profiling-ldap/proto_load_config.env:/etc/freeradius/proto_load_config.env
+    - ${DATA_PATH}/freeradius/profiling-ldap/load-generator-packets/:/etc/freeradius/load-generator-packets/
+    - ${DATA_PATH}/freeradius/profiling-ldap/stats/load-generator-stats.csv:/etc/freeradius/stats/load-generator-stats.csv
+    # Profiling scripts
+    - ${DATA_PATH}/start_valgrind_profiling.sh:/etc/freeradius/start_valgrind_profiling.sh
+    # Profiling results directory
+    - ${PROF_RESULTS_PATH}/:/etc/prof-results/
+    # Listener directory
+    - ${LISTENER_DIR}/:/var/run/multi-server/
+    entrypoint:
+    - bash
+    - -lc
+    - |
+      # Keep the container alive.  The test framework starts FreeRADIUS
+      # and runs commands via 'docker exec' so it can control timing.
+      #
+      # Start the server after configuring environment variables from test case's template.yml.j2 file.
+      sleep infinity
+    <<: *id001
+
diff --git a/src/tests/multi-server/environments/profiling-mysql.yml.j2 b/src/tests/multi-server/environments/profiling-mysql.yml.j2
new file mode 100644 (file)
index 0000000..ee51956
--- /dev/null
@@ -0,0 +1,63 @@
+# ---------------------------------------------------------------
+# Docker Compose Test Environment:
+#
+#   A single server instance for profiling
+#
+# ---------------------------------------------------------------
+x-common-config: &id001
+  cap_add:
+  - NET_ADMIN
+  - SYS_PTRACE
+  environment:
+    TEST_PROJECT_NAME: ${COMPOSE_PROJECT_NAME}
+    TEST_SUBNET: {{ test_subnet | default('172.16.0.0/12') }}
+services:
+  mariadb:
+    image: mariadb:10.5
+    environment:
+      MYSQL_ROOT_PASSWORD: rootpass
+      MYSQL_DATABASE: radius
+      MYSQL_USER: radius
+      MYSQL_PASSWORD: radpass
+    restart: unless-stopped
+    healthcheck:
+      test: ["CMD-SHELL", "mysqladmin ping -h localhost -u root -p${MYSQL_ROOT_PASSWORD} > /dev/null 2>&1"]
+      interval: 2s
+      timeout: 5s
+      retries: 10
+      start_period: 15s
+    volumes:
+    - /tmp/${COMPOSE_PROJECT_NAME}-mariadb-data:/var/lib/mysql
+    - ${DATA_PATH}/mariadb/default/init.sql:/docker-entrypoint-initdb.d/init.sql
+    <<: *id001
+  profiling-server:
+    image: freeradius-prof:latest
+    depends_on:
+      mariadb:
+        condition: service_healthy
+    volumes:
+    # profiling-server server config
+    - ${DATA_PATH}/freeradius/profiling-mysql/radiusd.conf:/etc/freeradius/radiusd.conf
+    # files module configuration
+    - ${DATA_PATH}/freeradius/profiling-mysql/mods-config/files/authorize:/etc/freeradius/mods-config/files/authorize
+    # proto_load packet configuration and statistics output
+    - ${DATA_PATH}/freeradius/profiling-mysql/proto_load_config.env:/etc/freeradius/proto_load_config.env
+    - ${DATA_PATH}/freeradius/profiling-mysql/load-generator-packets/:/etc/freeradius/load-generator-packets/
+    - ${DATA_PATH}/freeradius/profiling-mysql/stats/load-generator-stats.csv:/etc/freeradius/stats/load-generator-stats.csv
+    # Profiling scripts
+    - ${DATA_PATH}/start_valgrind_profiling.sh:/etc/freeradius/start_valgrind_profiling.sh
+    # Profiling results directory
+    - ${PROF_RESULTS_PATH}/:/etc/prof-results/
+    # Listener directory
+    - ${LISTENER_DIR}/:/var/run/multi-server/
+    entrypoint:
+    - bash
+    - -lc
+    - |
+      # Keep the container alive.  The test framework starts FreeRADIUS
+      # and runs commands via 'docker exec' so it can control timing.
+      #
+      # Start the server after configuring environment variables from test case's template.yml.j2 file.
+      sleep infinity
+    <<: *id001
+
diff --git a/src/tests/multi-server/environments/profiling-pap-auth.yml.j2 b/src/tests/multi-server/environments/profiling-pap-auth.yml.j2
new file mode 100644 (file)
index 0000000..3f7832f
--- /dev/null
@@ -0,0 +1,41 @@
+# ---------------------------------------------------------------
+# Docker Compose Test Environment:
+#
+#   A single server instance for profiling
+#
+# ---------------------------------------------------------------
+x-common-config: &id001
+  cap_add:
+  - NET_ADMIN
+  - SYS_PTRACE
+  environment:
+    TEST_PROJECT_NAME: ${COMPOSE_PROJECT_NAME}
+    TEST_SUBNET: {{ test_subnet | default('172.16.0.0/12') }}
+services:
+  profiling-server:
+    image: freeradius-prof:latest
+    volumes:
+    # profiling-server server config
+    - ${DATA_PATH}/freeradius/profiling-server-pap-auth/radiusd.conf:/etc/freeradius/radiusd.conf
+    # files module configuration
+    - ${DATA_PATH}/freeradius/profiling-server-pap-auth/mods-config/files/authorize:/etc/freeradius/mods-config/files/authorize
+    # proto_load packet configuration and statistics output
+    - ${DATA_PATH}/freeradius/profiling-server-pap-auth/proto_load_config.env:/etc/freeradius/proto_load_config.env
+    - ${DATA_PATH}/freeradius/profiling-server-pap-auth/load-generator-packets/:/etc/freeradius/load-generator-packets/
+    - ${DATA_PATH}/freeradius/profiling-server-pap-auth/stats/load-generator-stats.csv:/etc/freeradius/stats/load-generator-stats.csv
+    # Profiling scripts
+    - ${DATA_PATH}/start_valgrind_profiling.sh:/etc/freeradius/start_valgrind_profiling.sh
+    # Profiling results directory
+    - ${PROF_RESULTS_PATH}/:/etc/prof-results/
+    # Listener directory
+    - ${LISTENER_DIR}/:/var/run/multi-server/
+    entrypoint:
+    - bash
+    - -lc
+    - |
+      # Keep the container alive.  The test framework starts FreeRADIUS
+      # and runs commands via 'docker exec' so it can control timing.
+      #
+      # Start the server after configuring environment variables from test case's template.yml.j2 file.
+      sleep infinity
+    <<: *id001
diff --git a/src/tests/multi-server/environments/profiling.yml.j2 b/src/tests/multi-server/environments/profiling.yml.j2
new file mode 100644 (file)
index 0000000..fdf7be5
--- /dev/null
@@ -0,0 +1,39 @@
+# ---------------------------------------------------------------
+# Docker Compose Test Environment:
+#
+#   A single server instance for profiling
+#
+# ---------------------------------------------------------------
+x-common-config: &id001
+  cap_add:
+  - NET_ADMIN
+  - SYS_PTRACE
+  environment:
+    TEST_PROJECT_NAME: ${COMPOSE_PROJECT_NAME}
+    TEST_SUBNET: {{ test_subnet | default('172.16.0.0/12') }}
+services:
+  profiling-server:
+    image: freeradius-prof:latest
+    volumes:
+    # profiling-server server config
+    - ${DATA_PATH}/freeradius/profiling-server/radiusd.conf:/etc/freeradius/radiusd.conf
+    # proto_load packet configuration and statistics output
+    - ${DATA_PATH}/freeradius/profiling-server/proto_load_config.env:/etc/freeradius/proto_load_config.env
+    - ${DATA_PATH}/freeradius/profiling-server/load-generator-packets/:/etc/freeradius/load-generator-packets/
+    - ${DATA_PATH}/freeradius/profiling-server/stats/load-generator-stats.csv:/etc/freeradius/stats/load-generator-stats.csv
+    # Profiling scripts
+    - ${DATA_PATH}/start_valgrind_profiling.sh:/etc/freeradius/start_valgrind_profiling.sh
+    # Profiling results directory
+    - ${PROF_RESULTS_PATH}/:/etc/prof-results/
+    # Listener directory
+    - ${LISTENER_DIR}/:/var/run/multi-server/
+    entrypoint:
+    - bash
+    - -lc
+    - |
+      # Keep the container alive.  The test framework starts FreeRADIUS
+      # and runs commands via 'docker exec' so it can control timing.
+      #
+      # Start the server after configuring environment variables from test case's template.yml.j2 file.
+      sleep infinity
+    <<: *id001
diff --git a/src/tests/multi-server/scripts/docker/build/Dockerfile.multi-server-prof b/src/tests/multi-server/scripts/docker/build/Dockerfile.multi-server-prof
new file mode 100644 (file)
index 0000000..76448ac
--- /dev/null
@@ -0,0 +1,36 @@
+# Dockerfile for multi-server profiling tests.
+# Builds off of "base" profiling image, configures FreeRADIUS and builds it.
+FROM freeradius4-default-profiling/ubuntu24:latest
+
+# CFLAGS used for profiling build:
+#   -g3                          Maximum debug info for callgrind symbol resolution
+#   -O1                          Basic optimisation for realistic hotspot costs; inlining/vectorisation/
+#                                unrolling disabled below so the call graph matches the source.
+#   -fno-omit-frame-pointer      Keep frame pointers for callgrind stack walking
+#   -fno-inline                  Preserve call edges (suppresses CC_HINT(flatten) too)
+#   -Dalways_inline=             Strip always_inline, which -fno-inline does not suppress
+#   -fno-plt                     Resolve cross-library calls via GOT rather than PLT stubs;
+#                                PLT stubs have no DWARF info and show as ??? in callgrind.
+#   -fno-builtin                 Keep stdlib calls (memcpy, strlen, etc.) visible in the graph
+#   -fno-optimize-sibling-calls  Suppress tail-call elimination (-O1 can still apply it)
+RUN ./configure \
+    --enable-developer \
+    --disable-verify-ptr \
+    --with-raddbdir=/etc/freeradius \
+    CFLAGS="-g3 -O1 -fno-omit-frame-pointer -fno-inline -Dalways_inline= -fno-plt -fno-builtin -fno-optimize-sibling-calls" \
+    LDFLAGS="-fno-omit-frame-pointer"
+
+RUN make
+
+# FreeRADIUS installed in /etc/freeradius
+RUN make install
+
+# Setup softlinks to be able to run server with `freeradius` cmd
+RUN ln -sf /usr/local/sbin/radiusd /usr/local/sbin/freeradius && \
+    ln -sf /etc/freeradius/radiusd.conf /etc/freeradius/freeradius.conf && \
+    ln -sf /etc/freeradius /etc/raddb
+
+# Generate the self-signed RSA (and DH/EC) certificates for testing.
+RUN cd /etc/freeradius/certs && make
+
+
diff --git a/src/tests/multi-server/scripts/docker/build/build_image.sh b/src/tests/multi-server/scripts/docker/build/build_image.sh
new file mode 100755 (executable)
index 0000000..e77889e
--- /dev/null
@@ -0,0 +1,15 @@
+#!/bin/bash
+for arg in "$@"; do
+  case $arg in
+    BUILD_PLATFORM=*) BUILD_PLATFORM="${arg#*=}" ;;
+  esac
+done
+
+# This allows us to build an image on Apple Silicon where the base image was built on an linux/amd64 platform.
+# Example usage: BUILD_PLATFORM=linux/amd64 ./build_image.sh
+PLATFORM_ARG=""
+if [ -n "${BUILD_PLATFORM}" ]; then
+    PLATFORM_ARG="--platform=${BUILD_PLATFORM}"
+fi
+
+docker build ${PLATFORM_ARG} -f src/tests/multi-server/scripts/docker/build/Dockerfile.multi-server-prof -t freeradius-prof:latest .
diff --git a/src/tests/multi-server/scripts/docker/build/run_container.sh b/src/tests/multi-server/scripts/docker/build/run_container.sh
new file mode 100755 (executable)
index 0000000..35c0b42
--- /dev/null
@@ -0,0 +1,15 @@
+#!/bin/bash
+for arg in "$@"; do
+  case $arg in
+    BUILD_PLATFORM=*) BUILD_PLATFORM="${arg#*=}" ;;
+  esac
+done
+
+# This allows us to run a container on Apple Silicon where the base image was built on an linux/amd64 platform.
+# Example usage: BUILD_PLATFORM=linux/amd64 ./run_container.sh
+PLATFORM_ARG=""
+if [ -n "${BUILD_PLATFORM}" ]; then
+    PLATFORM_ARG="--platform=${BUILD_PLATFORM}"
+fi
+
+docker run -it --rm ${PLATFORM_ARG} -v "$(pwd)/prof-results:/etc/prof-results" --name freeradius-radenv-container freeradius-prof:latest
diff --git a/src/tests/multi-server/scripts/profiling/README.md b/src/tests/multi-server/scripts/profiling/README.md
new file mode 100644 (file)
index 0000000..cd58387
--- /dev/null
@@ -0,0 +1,30 @@
+## generate_callgrind_report.py
+
+python3 src/tests/multi-server/scripts/generate_callgrind_report.py \
+  <results_dir> \
+  --title "FreeRADIUS prof-accept 5min" \
+  --text-output valgrind_report_radenv_prof_accept.txt \
+  --md-output valgrind_report_radenv_prof_accept.md
+
+## Generate text based report from Valgrind/Callgrind results
+callgrind_annotate $(find . -name "callgrind.out.*" -size +0c | sort) > callgrind_report.txt
+
+## Generate SVG sharable file of valgrind/callgrind results
+
+Dependency: ```brew install gprof2dot```
+
+Generate SVG file for one worker thread:
+```
+gprof2dot --format=callgrind \
+  <path-to-prof-results>/callgrind.out.1004-04 \
+  | dot -Tsvg -o callgraph_thread04.svg
+```
+
+Generate SVG file per worker thread:
+```
+for f in <path-to-prof-results>/callgrind.out.1004-{04..12}; do
+  thread=$(grep "^thread:" "$f" | awk '{print $2}')
+  gprof2dot --format=callgrind "$f" \
+    | dot -Tsvg -o "callgraph_thread${thread}.svg"
+done
+```
diff --git a/src/tests/multi-server/scripts/profiling/generate_callgrind_report.py b/src/tests/multi-server/scripts/profiling/generate_callgrind_report.py
new file mode 100755 (executable)
index 0000000..eef0bca
--- /dev/null
@@ -0,0 +1,182 @@
+#!/usr/bin/env python3
+"""Generate text and markdown profiling reports from callgrind output files."""
+
+import argparse
+import os
+import re
+import subprocess
+import sys
+from collections import defaultdict
+
+
+def parse_thread_number(callgrind_file):
+    with open(callgrind_file) as f:
+        for line in f:
+            if line.startswith('thread:'):
+                return int(line.split()[1])
+    return 0
+
+
+def parse_summary_ir(callgrind_file):
+    with open(callgrind_file) as f:
+        for line in f:
+            if line.startswith('summary:'):
+                return int(line.split()[1])
+    return 0
+
+
+def run_callgrind_annotate(callgrind_file):
+    result = subprocess.run(
+        ['callgrind_annotate', '--auto=no', '--threshold=100', callgrind_file],
+        capture_output=True, text=True
+    )
+    return result.stdout
+
+
+def parse_module_entries(annotate_output):
+    """Extract rlm_* and proto_load function rows from callgrind_annotate output."""
+    entries = []
+    for line in annotate_output.splitlines():
+        if 'rlm_' not in line and 'proto_load' not in line:
+            continue
+        if not line.strip() or line.startswith('-') or line.startswith('='):
+            continue
+
+        ir_match = re.match(r'^\s*([\d,]+)\s+\(\s*([\d.]+)%\)', line)
+        if not ir_match:
+            continue
+
+        func_match = re.search(r'([^/\s]+):(\w+)\s+\[([^\]]+)\]', line)
+        if not func_match:
+            continue
+
+        entries.append({
+            'ir': int(ir_match.group(1).replace(',', '')),
+            'ir_pct': float(ir_match.group(2)),
+            'function': func_match.group(2),
+            'lib': os.path.basename(func_match.group(3)),
+        })
+    return entries
+
+
+def fmt_ir(n):
+    return f"{n:,}"
+
+
+def generate_markdown(results_dir, thread_data, title):
+    lines = []
+    lines.append(f"# {title}")
+    lines.append("")
+    lines.append(f"**Results:** `{results_dir}`")
+    lines.append("")
+
+    # Collect all unique function+lib pairs
+    lib_to_funcs = defaultdict(set)
+    for td in thread_data.values():
+        for e in td['entries']:
+            lib_to_funcs[e['lib']].add(e['function'])
+
+    lines.append("## Functions Found")
+    lines.append("")
+    lines.append("| Function | Library |")
+    lines.append("|---|---|")
+    for lib in sorted(lib_to_funcs):
+        funcs = ', '.join(f'`{f}`' for f in sorted(lib_to_funcs[lib]))
+        lines.append(f"| {funcs} | `{lib}` |")
+    lines.append("")
+
+    lines.append("## CPU Share (Ir = Instructions Retired)")
+    lines.append("")
+
+    for thread_num, td in sorted(thread_data.items()):
+        if not td['entries']:
+            continue
+
+        total_ir = td['total_ir']
+        lines.append(f"### Thread {thread_num:02d} — Total: {fmt_ir(total_ir)} Ir")
+        lines.append("")
+        lines.append("| Function | Library | Ir | % of Thread |")
+        lines.append("|---|---|---|---|")
+
+        module_total = 0
+        for e in sorted(td['entries'], key=lambda x: -x['ir']):
+            lines.append(f"| `{e['function']}` | `{e['lib']}` | {fmt_ir(e['ir'])} | {e['ir_pct']:.2f}% |")
+            module_total += e['ir']
+
+        module_pct = (module_total / total_ir * 100) if total_ir else 0
+        lines.append(f"| **Total** | | **{fmt_ir(module_total)}** | **{module_pct:.2f}%** |")
+        lines.append("")
+
+    all_module_ir = sum(e['ir'] for td in thread_data.values() for e in td['entries'])
+    all_total_ir = sum(td['total_ir'] for td in thread_data.values())
+    overall_pct = (all_module_ir / all_total_ir * 100) if all_total_ir else 0
+
+    lines.append("## Takeaway")
+    lines.append("")
+    lines.append(
+        f"`rlm_*` and `proto_load` combined account for **{overall_pct:.2f}% of total instructions** "
+        f"across all threads ({fmt_ir(all_module_ir)} of {fmt_ir(all_total_ir)} Ir total)."
+    )
+    lines.append("")
+
+    return "\n".join(lines)
+
+
+def main():
+    parser = argparse.ArgumentParser(description="Generate profiling reports from callgrind results")
+    parser.add_argument("results_dir", help="Directory containing callgrind.out.* files")
+    parser.add_argument("--title", default=None, help="Report title")
+    parser.add_argument("--text-output", default=None, help="Path for combined callgrind_annotate text report")
+    parser.add_argument("--md-output", default=None, help="Path for markdown summary report")
+    args = parser.parse_args()
+
+    results_dir = args.results_dir
+    if not os.path.isdir(results_dir):
+        print(f"error: {results_dir} is not a directory", file=sys.stderr)
+        sys.exit(1)
+
+    files = sorted([
+        os.path.join(results_dir, f)
+        for f in os.listdir(results_dir)
+        if re.match(r'callgrind\.out\.\d+(-\d+)?$', f) and os.path.getsize(os.path.join(results_dir, f)) > 0
+    ])
+
+    if not files:
+        print(f"error: no callgrind.out.* files found in {results_dir}", file=sys.stderr)
+        sys.exit(1)
+
+    title = args.title or f"FreeRADIUS Callgrind Profile: {os.path.basename(os.path.normpath(results_dir))}"
+
+    thread_data = {}
+    text_sections = []
+
+    for f in files:
+        thread_num = parse_thread_number(f)
+        total_ir = parse_summary_ir(f)
+        print(f"  {os.path.basename(f)}: thread {thread_num:02d}, {total_ir:,} Ir", file=sys.stderr)
+
+        annotate_output = run_callgrind_annotate(f)
+        text_sections.append(f"{'='*80}\n{os.path.basename(f)} (thread {thread_num:02d})\n{'='*80}\n{annotate_output}")
+
+        if thread_num not in thread_data:
+            thread_data[thread_num] = {'total_ir': 0, 'entries': []}
+        thread_data[thread_num]['total_ir'] += total_ir
+        thread_data[thread_num]['entries'].extend(parse_module_entries(annotate_output))
+
+    if args.text_output:
+        with open(args.text_output, 'w') as out:
+            out.write("\n\n".join(text_sections))
+        print(f"text report -> {args.text_output}", file=sys.stderr)
+
+    md = generate_markdown(results_dir, thread_data, title)
+
+    if args.md_output:
+        with open(args.md_output, 'w') as out:
+            out.write(md)
+        print(f"markdown report -> {args.md_output}", file=sys.stderr)
+    else:
+        print(md)
+
+
+if __name__ == '__main__':
+    main()
diff --git a/src/tests/multi-server/scripts/profiling/start_valgrind_profiling.sh b/src/tests/multi-server/scripts/profiling/start_valgrind_profiling.sh
new file mode 100755 (executable)
index 0000000..2a69c91
--- /dev/null
@@ -0,0 +1,92 @@
+#!/bin/bash
+
+# To be run inside the profiling container
+
+# Clear any stale marker from a previous run
+rm -f /etc/prof-results/.profiling_complete
+rm -f /etc/prof-results/valgrind_profiling.log
+
+exec > /etc/prof-results/valgrind_profiling.log 2>&1
+
+# Ignore SIGTERM — freeradius broadcasts it to the process group on shutdown,
+# which would otherwise kill this script before it can touch .profiling_complete
+trap '' SIGTERM
+
+# Load proto_load config to get packet settings
+source /etc/freeradius/proto_load_config.env
+
+# Calculate approximate send duration
+SEND_DURATION=$(( TEST_LOADGEN_NUM_MESSAGES / TEST_LOADGEN_START_PPS ))
+PROFILE_DURATION_BUFFER=15
+PROFILE_DURATION=$SEND_DURATION-$PROFILE_DURATION_BUFFER
+
+# Start freeradius under valgrind with instrumentation off
+valgrind \
+  --tool=callgrind \
+  --callgrind-out-file=/etc/prof-results/callgrind.out.%p \
+  --trace-children=yes \
+  --separate-threads=yes \
+  --dump-instr=yes \
+  --collect-jumps=yes \
+  --cache-sim=yes \
+  --branch-sim=yes \
+  --keep-debuginfo=yes \
+  --instr-atstart=no \
+  freeradius -f -l stdout -S resources.talloc_skip_cleanup=yes 2>&1 | \
+  tee /etc/prof-results/freeradius.log &
+VALGRIND_PID=$!
+
+# Wait for server ready (bail out if freeradius fails to start under valgrind)
+STARTUP_TIMEOUT=300
+STARTUP_ELAPSED=0
+until grep -q "Ready to process requests" /etc/prof-results/freeradius.log; do
+  sleep 1
+  STARTUP_ELAPSED=$(( STARTUP_ELAPSED + 1 ))
+  if [ ${STARTUP_ELAPSED} -ge ${STARTUP_TIMEOUT} ]; then
+    echo "ERROR: freeradius did not become ready within ${STARTUP_TIMEOUT}s, aborting"
+    kill -SIGKILL ${VALGRIND_PID} 2>/dev/null
+    exit 1
+  fi
+done
+
+# Enable instrumentation. callgrind_control auto-detects the running callgrind
+# instance and prints "PID <n>: freeradius ..." — capture that to get the PID
+# we need later for the graceful shutdown signal.
+echo "INFO: enabling callgrind instrumentation"
+CTRL_OUT=$(callgrind_control --instr=on)
+printf '%s\n' "$CTRL_OUT"
+FR_PID=$(printf '%s\n' "$CTRL_OUT" | grep -oP 'PID \K\d+(?=: freeradius)' | head -1)
+echo "Freeradius PID: ${FR_PID}"
+
+# Wait for approximate send duration
+sleep ${SEND_DURATION}
+
+# Stop instrumentation before shutdown so valgrind only flushes already-collected data
+echo "INFO: disabling callgrind instrumentation"
+CTRL_OUT=$(callgrind_control --instr=off 2>/dev/null || true)
+printf '%s\n' "$CTRL_OUT"
+
+# Graceful shutdown (equivalent to Ctrl+C)
+if [ -z "${FR_PID}" ]; then
+  echo "WARNING: could not determine freeradius PID from callgrind_control output, sending SIGINT to valgrind pipeline instead"
+  kill -SIGINT ${VALGRIND_PID} 2>/dev/null || true
+else
+  echo "INFO: killing freeradius process ${FR_PID} with SIGINT for graceful shutdown"
+  kill -SIGINT ${FR_PID}
+fi
+
+# Give valgrind time to write callgrind output after freeradius exits
+echo "INFO: sleeping for 5s"
+sleep 5
+
+# Signal that valgrind has finished writing all profiling data
+echo "INFO: Profiling complete at $(date)"
+
+echo "INFO: running callgrind_annotate to generate report"
+#callgrind_annotate $(find /etc/prof-results -name "callgrind.out.*" -size +0c | sort) > /etc/prof-results/callgrind_report.txt
+cmd='callgrind_annotate $(find /etc/prof-results -name "callgrind.out.*" -size +0c | sort) > /etc/prof-results/callgrind_report.txt'
+echo "$cmd"
+eval "$cmd"
+
+# Restore stdout/stderr
+exec > /dev/null 2>&1
diff --git a/src/tests/multi-server/tests/prof-accept/5min.test.yml b/src/tests/multi-server/tests/prof-accept/5min.test.yml
new file mode 100644 (file)
index 0000000..494dbd3
--- /dev/null
@@ -0,0 +1,17 @@
+# Topology - N/A for profiling test
+
+# Routing - N/A for profiling test
+
+# Load generator configuration
+loadgen:
+  start_pps: 500
+  max_pps: 500
+  duration: 300
+  step: 500
+  parallel: 1
+  max_backlog: 1000
+  repeat: "no"
+
+# Test framework
+test_timeout: 350
+test_verify_timeout: 340
diff --git a/src/tests/multi-server/tests/prof-accept/environment.yml.j2 b/src/tests/multi-server/tests/prof-accept/environment.yml.j2
new file mode 120000 (symlink)
index 0000000..1fdf2bc
--- /dev/null
@@ -0,0 +1 @@
+../../environments/profiling.yml.j2
\ No newline at end of file
diff --git a/src/tests/multi-server/tests/prof-accept/short.ci.test.yml b/src/tests/multi-server/tests/prof-accept/short.ci.test.yml
new file mode 100644 (file)
index 0000000..b009c3e
--- /dev/null
@@ -0,0 +1,18 @@
+# Topology - N/A for profiling test
+
+# Routing - N/A for profiling test
+
+# Load generator configuration
+loadgen:
+  start_pps: 100
+  max_pps: 100
+  duration: 60
+  step: 100
+  parallel: 1
+  max_backlog: 1000
+  repeat: "no"
+
+# Test framework
+test_timeout: 125
+test_state1_verify_timeout: 120
+test_state2_verify_timeout: 30
diff --git a/src/tests/multi-server/tests/prof-accept/template.yml.j2 b/src/tests/multi-server/tests/prof-accept/template.yml.j2
new file mode 100644 (file)
index 0000000..185bcd8
--- /dev/null
@@ -0,0 +1,38 @@
+timeout: {{ test_timeout }}
+state_order: sequence
+states:
+  state_1:
+    description: >
+      Baseline profiling test
+    host:
+      profiling-server:
+        actions:
+        - execute_command:
+            command: |
+              #
+              # proto_load configuration via environment variables
+              #
+              {%- for key, value in loadgen.items() %}
+              export TEST_LOADGEN_{{ key | upper }}="{{ value }}"
+              {%- endfor %}
+              TEST_LOADGEN_NUM_MESSAGES=0
+              for ((pps=$TEST_LOADGEN_START_PPS; pps<=$TEST_LOADGEN_MAX_PPS; pps+=$TEST_LOADGEN_STEP)); do
+                TEST_LOADGEN_NUM_MESSAGES=$((TEST_LOADGEN_NUM_MESSAGES + TEST_LOADGEN_DURATION * pps))
+              done
+              export TEST_LOADGEN_NUM_MESSAGES
+              #
+              # Starting load-generator server which will generate traffic based on env configuration
+              # from above.
+              #
+              printf "Starting load-generator with the following configuration:\n"
+              {%- for key, value in loadgen.items() %}
+              printf "  {{ key | upper }}:%s\n" "$TEST_LOADGEN_{{ key | upper }}"
+              {%- endfor %}
+              printf "  NUM_MESSAGES:     %s\n" "$TEST_LOADGEN_NUM_MESSAGES"
+
+              source /etc/freeradius/start_valgrind_profiling.sh
+
+            detach: true
+    verify:
+      timeout: {{ test_state1_verify_timeout }}
+      trigger_mode: unordered
diff --git a/src/tests/multi-server/tests/prof-ldap/5min.test.yml b/src/tests/multi-server/tests/prof-ldap/5min.test.yml
new file mode 100644 (file)
index 0000000..494dbd3
--- /dev/null
@@ -0,0 +1,17 @@
+# Topology - N/A for profiling test
+
+# Routing - N/A for profiling test
+
+# Load generator configuration
+loadgen:
+  start_pps: 500
+  max_pps: 500
+  duration: 300
+  step: 500
+  parallel: 1
+  max_backlog: 1000
+  repeat: "no"
+
+# Test framework
+test_timeout: 350
+test_verify_timeout: 340
diff --git a/src/tests/multi-server/tests/prof-ldap/environment.yml.j2 b/src/tests/multi-server/tests/prof-ldap/environment.yml.j2
new file mode 120000 (symlink)
index 0000000..6ffe47a
--- /dev/null
@@ -0,0 +1 @@
+../../environments/profiling-ldap.yml.j2
\ No newline at end of file
diff --git a/src/tests/multi-server/tests/prof-ldap/short.ci.test.yml b/src/tests/multi-server/tests/prof-ldap/short.ci.test.yml
new file mode 100644 (file)
index 0000000..b009c3e
--- /dev/null
@@ -0,0 +1,18 @@
+# Topology - N/A for profiling test
+
+# Routing - N/A for profiling test
+
+# Load generator configuration
+loadgen:
+  start_pps: 100
+  max_pps: 100
+  duration: 60
+  step: 100
+  parallel: 1
+  max_backlog: 1000
+  repeat: "no"
+
+# Test framework
+test_timeout: 125
+test_state1_verify_timeout: 120
+test_state2_verify_timeout: 30
diff --git a/src/tests/multi-server/tests/prof-ldap/template.yml.j2 b/src/tests/multi-server/tests/prof-ldap/template.yml.j2
new file mode 100644 (file)
index 0000000..185bcd8
--- /dev/null
@@ -0,0 +1,38 @@
+timeout: {{ test_timeout }}
+state_order: sequence
+states:
+  state_1:
+    description: >
+      Baseline profiling test
+    host:
+      profiling-server:
+        actions:
+        - execute_command:
+            command: |
+              #
+              # proto_load configuration via environment variables
+              #
+              {%- for key, value in loadgen.items() %}
+              export TEST_LOADGEN_{{ key | upper }}="{{ value }}"
+              {%- endfor %}
+              TEST_LOADGEN_NUM_MESSAGES=0
+              for ((pps=$TEST_LOADGEN_START_PPS; pps<=$TEST_LOADGEN_MAX_PPS; pps+=$TEST_LOADGEN_STEP)); do
+                TEST_LOADGEN_NUM_MESSAGES=$((TEST_LOADGEN_NUM_MESSAGES + TEST_LOADGEN_DURATION * pps))
+              done
+              export TEST_LOADGEN_NUM_MESSAGES
+              #
+              # Starting load-generator server which will generate traffic based on env configuration
+              # from above.
+              #
+              printf "Starting load-generator with the following configuration:\n"
+              {%- for key, value in loadgen.items() %}
+              printf "  {{ key | upper }}:%s\n" "$TEST_LOADGEN_{{ key | upper }}"
+              {%- endfor %}
+              printf "  NUM_MESSAGES:     %s\n" "$TEST_LOADGEN_NUM_MESSAGES"
+
+              source /etc/freeradius/start_valgrind_profiling.sh
+
+            detach: true
+    verify:
+      timeout: {{ test_state1_verify_timeout }}
+      trigger_mode: unordered
diff --git a/src/tests/multi-server/tests/prof-mysql/5min.test.yml b/src/tests/multi-server/tests/prof-mysql/5min.test.yml
new file mode 100644 (file)
index 0000000..494dbd3
--- /dev/null
@@ -0,0 +1,17 @@
+# Topology - N/A for profiling test
+
+# Routing - N/A for profiling test
+
+# Load generator configuration
+loadgen:
+  start_pps: 500
+  max_pps: 500
+  duration: 300
+  step: 500
+  parallel: 1
+  max_backlog: 1000
+  repeat: "no"
+
+# Test framework
+test_timeout: 350
+test_verify_timeout: 340
diff --git a/src/tests/multi-server/tests/prof-mysql/environment.yml.j2 b/src/tests/multi-server/tests/prof-mysql/environment.yml.j2
new file mode 120000 (symlink)
index 0000000..8b72097
--- /dev/null
@@ -0,0 +1 @@
+../../environments/profiling-mysql.yml.j2
\ No newline at end of file
diff --git a/src/tests/multi-server/tests/prof-mysql/short.ci.test.yml b/src/tests/multi-server/tests/prof-mysql/short.ci.test.yml
new file mode 100644 (file)
index 0000000..b009c3e
--- /dev/null
@@ -0,0 +1,18 @@
+# Topology - N/A for profiling test
+
+# Routing - N/A for profiling test
+
+# Load generator configuration
+loadgen:
+  start_pps: 100
+  max_pps: 100
+  duration: 60
+  step: 100
+  parallel: 1
+  max_backlog: 1000
+  repeat: "no"
+
+# Test framework
+test_timeout: 125
+test_state1_verify_timeout: 120
+test_state2_verify_timeout: 30
diff --git a/src/tests/multi-server/tests/prof-mysql/template.yml.j2 b/src/tests/multi-server/tests/prof-mysql/template.yml.j2
new file mode 100644 (file)
index 0000000..185bcd8
--- /dev/null
@@ -0,0 +1,38 @@
+timeout: {{ test_timeout }}
+state_order: sequence
+states:
+  state_1:
+    description: >
+      Baseline profiling test
+    host:
+      profiling-server:
+        actions:
+        - execute_command:
+            command: |
+              #
+              # proto_load configuration via environment variables
+              #
+              {%- for key, value in loadgen.items() %}
+              export TEST_LOADGEN_{{ key | upper }}="{{ value }}"
+              {%- endfor %}
+              TEST_LOADGEN_NUM_MESSAGES=0
+              for ((pps=$TEST_LOADGEN_START_PPS; pps<=$TEST_LOADGEN_MAX_PPS; pps+=$TEST_LOADGEN_STEP)); do
+                TEST_LOADGEN_NUM_MESSAGES=$((TEST_LOADGEN_NUM_MESSAGES + TEST_LOADGEN_DURATION * pps))
+              done
+              export TEST_LOADGEN_NUM_MESSAGES
+              #
+              # Starting load-generator server which will generate traffic based on env configuration
+              # from above.
+              #
+              printf "Starting load-generator with the following configuration:\n"
+              {%- for key, value in loadgen.items() %}
+              printf "  {{ key | upper }}:%s\n" "$TEST_LOADGEN_{{ key | upper }}"
+              {%- endfor %}
+              printf "  NUM_MESSAGES:     %s\n" "$TEST_LOADGEN_NUM_MESSAGES"
+
+              source /etc/freeradius/start_valgrind_profiling.sh
+
+            detach: true
+    verify:
+      timeout: {{ test_state1_verify_timeout }}
+      trigger_mode: unordered
diff --git a/src/tests/multi-server/tests/prof-pap-auth/5min.test.yml b/src/tests/multi-server/tests/prof-pap-auth/5min.test.yml
new file mode 100644 (file)
index 0000000..494dbd3
--- /dev/null
@@ -0,0 +1,17 @@
+# Topology - N/A for profiling test
+
+# Routing - N/A for profiling test
+
+# Load generator configuration
+loadgen:
+  start_pps: 500
+  max_pps: 500
+  duration: 300
+  step: 500
+  parallel: 1
+  max_backlog: 1000
+  repeat: "no"
+
+# Test framework
+test_timeout: 350
+test_verify_timeout: 340
diff --git a/src/tests/multi-server/tests/prof-pap-auth/environment.yml.j2 b/src/tests/multi-server/tests/prof-pap-auth/environment.yml.j2
new file mode 120000 (symlink)
index 0000000..ced5982
--- /dev/null
@@ -0,0 +1 @@
+../../environments/profiling-pap-auth.yml.j2
\ No newline at end of file
diff --git a/src/tests/multi-server/tests/prof-pap-auth/short.ci.test.yml b/src/tests/multi-server/tests/prof-pap-auth/short.ci.test.yml
new file mode 100644 (file)
index 0000000..b009c3e
--- /dev/null
@@ -0,0 +1,18 @@
+# Topology - N/A for profiling test
+
+# Routing - N/A for profiling test
+
+# Load generator configuration
+loadgen:
+  start_pps: 100
+  max_pps: 100
+  duration: 60
+  step: 100
+  parallel: 1
+  max_backlog: 1000
+  repeat: "no"
+
+# Test framework
+test_timeout: 125
+test_state1_verify_timeout: 120
+test_state2_verify_timeout: 30
diff --git a/src/tests/multi-server/tests/prof-pap-auth/template.yml.j2 b/src/tests/multi-server/tests/prof-pap-auth/template.yml.j2
new file mode 100644 (file)
index 0000000..185bcd8
--- /dev/null
@@ -0,0 +1,38 @@
+timeout: {{ test_timeout }}
+state_order: sequence
+states:
+  state_1:
+    description: >
+      Baseline profiling test
+    host:
+      profiling-server:
+        actions:
+        - execute_command:
+            command: |
+              #
+              # proto_load configuration via environment variables
+              #
+              {%- for key, value in loadgen.items() %}
+              export TEST_LOADGEN_{{ key | upper }}="{{ value }}"
+              {%- endfor %}
+              TEST_LOADGEN_NUM_MESSAGES=0
+              for ((pps=$TEST_LOADGEN_START_PPS; pps<=$TEST_LOADGEN_MAX_PPS; pps+=$TEST_LOADGEN_STEP)); do
+                TEST_LOADGEN_NUM_MESSAGES=$((TEST_LOADGEN_NUM_MESSAGES + TEST_LOADGEN_DURATION * pps))
+              done
+              export TEST_LOADGEN_NUM_MESSAGES
+              #
+              # Starting load-generator server which will generate traffic based on env configuration
+              # from above.
+              #
+              printf "Starting load-generator with the following configuration:\n"
+              {%- for key, value in loadgen.items() %}
+              printf "  {{ key | upper }}:%s\n" "$TEST_LOADGEN_{{ key | upper }}"
+              {%- endfor %}
+              printf "  NUM_MESSAGES:     %s\n" "$TEST_LOADGEN_NUM_MESSAGES"
+
+              source /etc/freeradius/start_valgrind_profiling.sh
+
+            detach: true
+    verify:
+      timeout: {{ test_state1_verify_timeout }}
+      trigger_mode: unordered