[RFC PATCH 2/3] app/test-mempool-perf: add perf test logic
Konstantin Ananyev
konstantin.v.ananyev at yandex.ru
Sun Oct 4 16:18:52 CEST 2026
> Add the performance test logic to do performance measurements for the
> mempool settings specified. Run-to-completion mode is assumed, and each
> thread does allocs and frees in a randomized pattern.
LGTM, in general, nice thing to have.
Few thoughts and comments below.
Some generic ones first:
I don't think there is a real need for interactive mode.
In fact, it is probably better to have it as part of our UT,
instead of separate app.
I do understand that there are a lot of testing parameters,
but probably we can pass them va env variable as some other tests do.
>
> Signed-off-by: Bruce Richardson <bruce.richardson at intel.com>
> ---
> app/test-mempool-perf/main.c <https://inbox.dpdk.org/dev/20260820142439.3684311-3-bruce.richardson@intel.com/#Z31app:test-mempool-perf:main.c> | 248 +++++++++++++++++++++++++++++-
> app/test-mempool-perf/meson.build <https://inbox.dpdk.org/dev/20260820142439.3684311-3-bruce.richardson@intel.com/#Z31app:test-mempool-perf:meson.build> | 2 +-
> doc/guides/tools/mempoolperf.rst <https://inbox.dpdk.org/dev/20260820142439.3684311-3-bruce.richardson@intel.com/#Z31doc:guides:tools:mempoolperf.rst> | 34 ++++
> 3 files changed, 282 insertions(+), 2 deletions(-)
>
> diff
> <https://inbox.dpdk.org/dev/20260820142439.3684311-3-bruce.richardson@intel.com/#iZ31app:test-mempool-perf:main.c>
> --git a/app/test-mempool-perf/main.c b/app/test-mempool-perf/main.c
> index 877abc2170..471631f3db 100644 --- a/app/test-mempool-perf/main.c
> +++ b/app/test-mempool-perf/main.c @@ -9,9 +9,14 @@ #include <inttypes.h>
>
> #include <rte_argparse.h>
> +#include <rte_cycles.h> #include <rte_eal.h>
> +#include <rte_errno.h> +#include <rte_launch.h> #include <rte_lcore.h>
> +#include <rte_mbuf.h> #include <rte_mempool.h>
> +#include <rte_random.h> #include <rte_string_fns.h>
>
> #define DEFAULT_CACHE_SIZE 512
> @@ -19,6 +24,11 @@ #define DEFAULT_BURST_SIZE 32
> #define DEFAULT_NB_BUFS_PER_LCORE 1024
>
> +/* match the element size used by rte_pktmbuf_pool_create with
> default dataroom */ +#define ELEM_SIZE (sizeof(struct rte_mbuf) +
> RTE_MBUF_DEFAULT_BUF_SIZE) +#define TEST_DURATION_SEC 5
I think test duration and elem size needs to be configurable test
parameters too.
> +#define RESHUFFLE_INTERVAL 100 + struct test_config {
> char mempool_type[RTE_MEMPOOL_NAMESIZE];
> uint32_t nb_bufs;
> @@ -210,9 +220,238 @@ run_interactive_mode(void) return 0;
> }
>
> +struct worker_stats { + uint64_t get_success; + uint64_t get_fail; +
> uint64_t put_count; +} __rte_cache_aligned; + +static struct
> worker_stats lcore_stats[RTE_MAX_LCORE]; static bool summary_only;
> +static RTE_ATOMIC(uint32_t) test_running; + +/* + * Shuffle delta[]
> (half +1, half -1) then rotate it so the running sum + * of held
> batches never goes negative. Works by finding the last index + * where
> the prefix sum reaches its minimum and rotating to start just + *
> after that point, which is guaranteed to keep all prefix sums >= 0. +
> */ +static void +shuffle_and_validate(int8_t *delta, int8_t *tmp,
> uint32_t rf) +{ + uint32_t i, j, rot; + int32_t running, min_val; +
> uint32_t min_idx; + + /* Fisher-Yates shuffle */ + for (i = rf - 1; i
> > 0; i--) { + j = (uint32_t)rte_rand_max(i + 1); + int8_t t =
> delta[i]; + delta[i] = delta[j]; + delta[j] = t; + } + + /* find last
> position where prefix sum reaches its minimum */ + running = 0; +
> min_val = 0; + min_idx = 0; + for (i = 0; i < rf; i++) { + running +=
> delta[i]; + if (running <= min_val) { /* <= keeps the last occurrence
> */ + min_val = running; + min_idx = i; + } + } + + if (min_val >= 0) +
> return; /* already valid, no rotation needed */ + + rot = (min_idx +
> 1) % rf; + if (rot == 0) + return; + + /* apply rotation via tmp
> buffer */ + memcpy(tmp, delta + rot, (rf - rot) * sizeof(*tmp)); +
> memcpy(tmp + rf - rot, delta, rot * sizeof(*tmp)); + memcpy(delta,
> tmp, rf * sizeof(*tmp)); +} + +static void +access_object(void *obj)
> +{ + volatile uint64_t *p = (volatile uint64_t *)obj; + uint64_t acc =
> 0; + uint32_t i; + + for (i = 0; i < ELEM_SIZE / sizeof(uint64_t);
> i++) + acc += p[i]; + for (i = 0; i < RTE_CACHE_LINE_SIZE /
> sizeof(uint64_t); i++) + p[i] = acc; +}
We can probably add here some sanity checking too, so this UT can
be used not only to measure performance, but also to make sure that our
mempool works correctly under heavy load.
Something like that:
free object contain some repdefine pattern (RTE_LCORE_MAX),
when we do get() we check hat allocated objects contain given pattern
and overwrite them with new one (let say current lcore id).
Then before put() we re-check our pattern and overwrite it again with
(RTE_LCORE_MAX).
> + +static int +worker_main(void *arg) +{ + struct rte_mempool *mp =
> arg; + unsigned int id = rte_lcore_id(); + struct worker_stats *stats
> = &lcore_stats[id]; + /* rf must be even so alloc count == free count;
> minimum 2 */ + uint32_t rf = RTE_MAX(2u, (cfg.rand_factor / 2) * 2); +
> uint32_t bs = cfg.burst_size; + uint32_t hold_count = 0; + uint32_t
> run = RESHUFFLE_INTERVAL; /* trigger shuffle on first entry */ + void
> **hold_objs; + int8_t *delta, *tmp; + uint32_t i, k; + + hold_objs =
> malloc(bs * (rf / 2) * sizeof(*hold_objs)); + delta = malloc(rf *
> sizeof(*delta)); + tmp = malloc(rf * sizeof(*tmp)); + if (hold_objs ==
> NULL || delta == NULL || tmp == NULL) { + free(hold_objs); +
> free(delta); + free(tmp); + return -ENOMEM; + } + + /* +1 = alloc a
> burst, -1 = free a burst */ + for (i = 0; i < rf / 2; i++) delta[i] =
> +1; + for (i = rf / 2; i < rf; i++) delta[i] = -1; + + while
> (rte_atomic_load_explicit(&test_running, rte_memory_order_relaxed)) {
> + if (run >= RESHUFFLE_INTERVAL) { + shuffle_and_validate(delta, tmp,
> rf); + run = 0; + } + + for (i = 0; i < rf; i++) { + if (delta[i] > 0)
> { + if (rte_mempool_get_bulk(mp, + hold_objs + hold_count, bs) != 0) {
> + stats->get_fail++; + continue; + } + if (cfg.access_on_alloc) { +
> for (k = 0; k < bs; k++) + access_object(hold_objs[hold_count + k]); +
> } + hold_count += bs; + stats->get_success += bs; + } else { +
> hold_count -= bs; + rte_mempool_put_bulk(mp, hold_objs + hold_count,
> bs); + stats->put_count += bs; + } + } + run++; + } + + if (hold_count
> > 0) + rte_mempool_put_bulk(mp, hold_objs, hold_count); + + free(tmp);
> + free(delta); + free(hold_objs); + return 0; +}
> -/* Used only in non-interactive mode to receive the --mempool-type
> string */ +static struct rte_mempool * +create_mempool(void) +{ +
> struct rte_mempool *mp; + int ret; + + mp =
> rte_mempool_create_empty("perf_pool", cfg.nb_bufs, ELEM_SIZE, +
> cfg.cache_size, 0, + rte_socket_id(), 0); + if (mp == NULL) { +
> fprintf(stderr, "Failed to create empty mempool: %s\n", +
> rte_strerror(rte_errno)); + return NULL; + } + + ret =
> rte_mempool_set_ops_byname(mp, cfg.mempool_type, NULL); + if (ret < 0)
> { + fprintf(stderr, "Failed to set ops '%s': %s\n", +
> cfg.mempool_type, rte_strerror(-ret)); + rte_mempool_free(mp); +
> return NULL; + } + + ret = rte_mempool_populate_default(mp); + if (ret
> < 0) { + fprintf(stderr, "Failed to populate mempool: %s\n", +
> rte_strerror(-ret)); + rte_mempool_free(mp); + return NULL; + } + +
> return mp; +} + +static void +print_results(double elapsed_secs) +{ +
> uint64_t total_get = 0, total_fail = 0; + unsigned int id; + +
> printf("\n%-8s %12s %12s %12s\n", + "lcore", "get (Mops/s)",
> "fail/burst", "put (Mops/s)"); + printf("%-8s %12s %12s %12s\n", +
> "------", "------------", "----------", "------------"); + +
> RTE_LCORE_FOREACH_WORKER(id) { + if (lcore_stats[id].get_success == 0
> && + lcore_stats[id].get_fail == 0) + continue; + if (!summary_only) +
> printf("%-8u %12.3f %12" PRIu64 " %12.3f\n", + id, +
> lcore_stats[id].get_success / elapsed_secs / 1e6, +
> lcore_stats[id].get_fail, + lcore_stats[id].put_count / elapsed_secs /
> 1e6); + total_get += lcore_stats[id].get_success; + total_fail +=
> lcore_stats[id].get_fail; + } + + printf("%-8s %12.3f %12" PRIu64
> "\n\n", + "Total", + total_get / elapsed_secs / 1e6, + total_fail); +}
> + +static void +run_test(struct rte_mempool *mp) +{ + uint64_t start,
> end; + unsigned int id; + uint32_t launched = 0; + +
> memset(lcore_stats, 0, sizeof(lcore_stats)); +
> rte_atomic_store_explicit(&test_running, 1, rte_memory_order_release);
> + + RTE_LCORE_FOREACH_WORKER(id) { + if (launched >= cfg.nb_threads) +
> break; + rte_eal_remote_launch(worker_main, mp, id); + launched++; + }
> + + printf("Running test for %d seconds with %" PRIu32 "
> worker(s)...\n", + TEST_DURATION_SEC, launched); + + start =
> rte_get_timer_cycles(); + rte_delay_ms((uint32_t)(TEST_DURATION_SEC *
> 1000)); + + rte_atomic_store_explicit(&test_running, 0,
> rte_memory_order_release); + end = rte_get_timer_cycles(); +
> rte_eal_mp_wait_lcore(); + + double elapsed_s = (double)(end - start)
> / rte_get_timer_hz(); + print_results(elapsed_s); +} + +/* receives
> the --mempool-type string from argparse */ static const char *mempool_type_arg;
>
> static int
> @@ -325,6 +564,13 @@ main(int argc, char **argv)
> print_config();
>
> + struct rte_mempool *mp = create_mempool(); + if (mp == NULL) +
> rte_exit(EXIT_FAILURE, "Failed to create mempool\n"); + +
> run_test(mp); + + rte_mempool_free(mp); rte_eal_cleanup();
> return 0;
> }
> diff
> <https://inbox.dpdk.org/dev/20260820142439.3684311-3-bruce.richardson@intel.com/#iZ31app:test-mempool-perf:meson.build>
> --git a/app/test-mempool-perf/meson.build
> b/app/test-mempool-perf/meson.build index 2d61ac62e9..37c6c14d56
> 100644 --- a/app/test-mempool-perf/meson.build +++
> b/app/test-mempool-perf/meson.build @@ -1,7 +1,7 @@ # SPDX-License-Identifier: BSD-3-Clause
> # Copyright(c) 2024 Intel Corporation
>
> -deps += ['mempool', 'argparse'] +deps += ['mbuf', 'mempool', 'argparse']
> sources = files(
> 'main.c',
> diff
> <https://inbox.dpdk.org/dev/20260820142439.3684311-3-bruce.richardson@intel.com/#iZ31doc:guides:tools:mempoolperf.rst>
> --git a/doc/guides/tools/mempoolperf.rst
> b/doc/guides/tools/mempoolperf.rst index e0ad859a29..31bd6e660e 100644
> --- a/doc/guides/tools/mempoolperf.rst +++
> b/doc/guides/tools/mempoolperf.rst @@ -8,6 +8,7 @@ The
> ``dpdk-test-mempool-perf`` tool measures the alloc/free throughput of
> DPDK m Worker threads repeatedly allocate and free objects in configurable burst sizes following a randomised pattern,
> exercising the pool under varying levels of occupancy.
> Any mempool driver registered with the DPDK mempool ops table can be tested.
> +Pool elements are sized to match ``rte_pktmbuf_pool_create()`` with
> default data room.
>
> Running the Application
> @@ -105,6 +106,39 @@ After configuration the tool prints an equivalent
> non-interactive command:: Append this output after the EAL options on subsequent runs to reproduce the exact same configuration without prompting.
>
>
> +Test Output +----------- + +Each run lasts a fixed 5 seconds. +On
> completion, a results table is printed showing per-worker throughput
> and an aggregate total: + +.. code-block:: console + + lcore get
> (Mops/s) fail/burst put (Mops/s) + ------ ------------ ----------
> ------------ + 1 23.871 0 23.871 + 2 24.012 0 24.012 + Total 47.883 0
> + +lcore + Worker lcore ID. + +get (Mops/s) + Successful allocation
> throughput in millions of operations per second. + +fail/burst +
> Number of allocation calls that failed because the pool had
> insufficient free objects. + A non-zero value indicates ``--nb-bufs``
> is too small for the configured workload. + +put (Mops/s) + Free
> throughput in millions of operations per second. + Under a balanced
> workload this matches the get rate. + +The ``Total`` row shows
> aggregate get throughput and failure count across all workers; +it
> does not include a put rate. +With ``--summary``, only the ``Total``
> row is printed. + + Examples
> --------
>
> -- 2.53.0
More information about the dev
mailing list