|WARNING| [RFC, 3/3] app/test-mempool-perf: add testing in pipeline model

dpdklab at iol.unh.edu dpdklab at iol.unh.edu
Thu Aug 20 16:57:18 CEST 2026


Test-Label: ai-code-review
Test-Status: WARNING
https://dpdk.org/patch/168123

_AI Code Review_

=== Patch Review: series.patch (via Claude) ===
Review date: 2026-08-20

# Review of DPDK test-mempool-perf Patch Series

## Patch 1/3: Skeleton of New Test App

### Errors

**Resource leak on error path in `run_interactive_mode()`**

In `run_interactive_mode()`, if `prompt_uint32()` or the subsequent `fgets()` calls return an error after `apply_defaults()` has been called, the function returns `-1` without cleaning up any resources. While this particular function doesn't allocate dynamic memory, the error return causes `main()` to call `rte_exit()` which terminates the process, so this is acceptable. However, the pattern should be noted for consistency.

**Missing error propagation in `main()`**

```c
ret = rte_eal_init(argc, argv);
if (ret < 0)
    rte_exit(EXIT_FAILURE, "Invalid EAL arguments\n");
```

The error message "Invalid EAL arguments" is generic and doesn't preserve the specific error information from `rte_eal_init()`. This is acceptable since `rte_eal_init()` typically prints its own error messages, but worth noting for clarity.

### Warnings

**Missing release notes**

This patch adds a new application (`test-mempool-perf`) but does not include updates to the release notes. According to the guidelines, new applications require release notes documentation.

Add a release notes entry in `doc/guides/rel_notes/release_<version>.rst` documenting the new test-mempool-perf application.

**Structure field initialization style**

In the `cfg` structure initialization:

```c
static struct test_config cfg = {
	.mempool_type   = "",
	.nb_bufs        = 0,  /* 0 means: compute from lcore count */
	.cache_size     = DEFAULT_CACHE_SIZE,
	...
};
```

The zero-initialization of `.nb_bufs` and `.nb_threads` is unnecessary since structure members are zero-initialized by default in C. The comments explaining the zero values are valuable, but the explicit `= 0` assignments can be omitted.

**Boolean return type preferred for `is_valid_mempool_type()`**

```c
static bool
is_valid_mempool_type(const char *name)
```

Good use of `bool` for a predicate function.

### Info

**Code organization and clarity**

The skeleton implementation is well-structured with clear separation of interactive and non-interactive modes. The `trim_newline()` helper and `prompt_uint32()` functions are clean and reusable.

---

## Patch 2/3: Add Performance Test Logic

### Errors

**Potential memory leak on worker thread allocation failure**

In `worker_main()`:

```c
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;
}
```

This is correct. All three pointers are freed regardless of which allocation failed, and `free(NULL)` is safe. Well done.

**No error checking on worker launch failure**

In `run_test()`:

```c
RTE_LCORE_FOREACH_WORKER(id) {
    if (launched >= cfg.nb_threads)
        break;
    rte_eal_remote_launch(worker_main, mp, id);
    launched++;
}
```

The return value of `rte_eal_remote_launch()` is not checked. If the launch fails, `launched` is still incremented and the test proceeds with fewer workers than expected. This could lead to incorrect results or misleading output.

Check the return value:
```c
if (rte_eal_remote_launch(worker_main, mp, id) < 0) {
    fprintf(stderr, "Failed to launch worker on lcore %u\n", id);
    rte_atomic_store_explicit(&test_running, 0, rte_memory_order_release);
    rte_eal_mp_wait_lcore();
    return;
}
```

**Use of `volatile` instead of atomic operations**

In `access_object()`:

```c
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;
```

The `volatile` qualifier is used to prevent the compiler from optimizing away the memory accesses. However, this function is called in a single-threaded context (each worker accesses only objects it has allocated from the pool), so `volatile` is appropriate here to force memory accesses for benchmarking purposes. This is acceptable usage -- the intent is to simulate real workload memory bandwidth, not to provide thread-safe synchronization.

### Warnings

**Missing release notes for new application functionality**

The patch adds the core performance testing functionality but does not update release notes to describe what the application does and how to use it. The documentation in `mempoolperf.rst` is comprehensive, but a release notes entry is still required.

**Statistics accumulation uses correct `+=` operator**

The statistics updates in `worker_main()` correctly use `+=`:

```c
stats->get_success += bs;
stats->put_count += bs;
```

This is correct.

**Fisher-Yates shuffle implementation**

The shuffle implementation in `shuffle_and_validate()` is correct. The rotation logic to ensure non-negative prefix sums is a clever solution to guarantee the worker never tries to free more objects than it holds.

### Info

**Test duration hardcoded**

```c
#define TEST_DURATION_SEC   5
```

The test duration is hardcoded to 5 seconds. While this is documented, making it configurable via command line would increase flexibility. This is a minor enhancement suggestion, not a requirement.

**Performance measurement approach**

The randomized alloc/free pattern with configurable `rand_factor` is a good approach to exercise the pool under varying pressure. The shuffle-and-rotate algorithm ensures balanced alloc/free counts while maintaining realism.

---

## Patch 3/3: Add Testing in Pipeline Model

### Errors

**Potential memory leak in `run_pipeline_test()` on early exit**

In `run_pipeline_test()`:

```c
pargs = malloc(nb_pairs * sizeof(*pargs));
rings = malloc(nb_pairs * sizeof(*rings));
if (pargs == NULL || rings == NULL)
    rte_exit(EXIT_FAILURE, "Failed to allocate pipeline resources\n");

for (i = 0; i < nb_pairs; i++) {
    snprintf(ring_name, sizeof(ring_name), "pipe_ring_%u", i);
    rings[i] = rte_ring_create(ring_name, PIPELINE_RING_SIZE,
                               rte_socket_id(),
                               RING_F_SP_ENQ | RING_F_SC_DEQ);
    if (rings[i] == NULL)
        rte_exit(EXIT_FAILURE,
                 "Failed to create pipeline ring %u: %s\n",
                 i, rte_strerror(rte_errno));
    pargs[i].mp   = mp;
    pargs[i].ring = rings[i];
}
```

If `rte_ring_create()` fails for ring `i`, the function calls `rte_exit()` without freeing the successfully created rings `[0..i-1]` or the allocated `pargs` and `rings` arrays. While `rte_exit()` terminates the process (so the leak doesn't persist), it would be cleaner to free resources before exiting or use a goto cleanup pattern.

Suggested fix:
```c
for (i = 0; i < nb_pairs; i++) {
    snprintf(ring_name, sizeof(ring_name), "pipe_ring_%u", i);
    rings[i] = rte_ring_create(ring_name, PIPELINE_RING_SIZE,
                               rte_socket_id(),
                               RING_F_SP_ENQ | RING_F_SC_DEQ);
    if (rings[i] == NULL) {
        fprintf(stderr, "Failed to create pipeline ring %u: %s\n",
                i, rte_strerror(rte_errno));
        /* cleanup already-created rings */
        for (uint32_t j = 0; j < i; j++)
            rte_ring_free(rings[j]);
        free(pargs);
        free(rings);
        rte_exit(EXIT_FAILURE, "Ring creation failed\n");
    }
    pargs[i].mp   = mp;
    pargs[i].ring = rings[i];
}
```

**No error checking on worker launch in `run_pipeline_test()`**

Similar to the issue in `run_test()`, the return values of `rte_eal_remote_launch()` are not checked in the pipeline test:

```c
for (i = 0; i < nb_pairs; i++) {
    id = worker_lcores[i];
    pipeline_producer[id] = true;
    rte_eal_remote_launch(producer_main, &pargs[i], id);
}
for (i = 0; i < nb_pairs; i++) {
    id = worker_lcores[nb_pairs + i];
    rte_eal_remote_launch(consumer_main, &pargs[i], id);
}
```

If a launch fails, the test proceeds with incomplete worker sets, producing invalid results.

**Resource leak in `producer_main()` and `consumer_main()` on malloc failure**

In `producer_main()` and `consumer_main()`:

```c
objs = malloc(bs * sizeof(*objs));
if (objs == NULL)
    return -ENOMEM;
```

If malloc fails, the function returns `-ENOMEM`. The main thread calls `rte_eal_mp_wait_lcore()` which waits for all launched workers, but a worker that returns an error is not handled specially. The error code is silently lost and the test continues. This should at least be logged or cause the test to abort.

Suggested approach: After `rte_eal_mp_wait_lcore()`, check worker return codes and report failures:

```c
rte_eal_mp_wait_lcore();

RTE_LCORE_FOREACH_WORKER(id) {
    if (rte_eal_wait_lcore(id) != 0)
        fprintf(stderr, "Warning: worker on lcore %u failed\n", id);
}
```

Note: `rte_eal_wait_lcore()` can only be called after the lcore has already been waited on by `rte_eal_mp_wait_lcore()`, so you would need to store the return codes separately during the wait. Alternatively, use a global error flag that workers set on failure.

### Warnings

**Pipeline mode invalidates `--rand-factor` but doesn't reject it**

In `run_interactive_mode()`, if the user selects pipeline mode, the `rand_factor` prompt is skipped. However, in non-interactive mode, the user can specify both `--pipeline` and `--rand-factor`, and the `rand_factor` value is silently ignored.

This should either:
1. Print a warning if both are specified, or
2. Return an error rejecting the conflicting options

**Missing validation for minimum thread count in pipeline mode**

The pipeline mode requires at least 2 worker lcores (to form one producer/consumer pair). The code checks this in `run_pipeline_test()`:

```c
nb_pairs = nb_workers / 2;
if (nb_pairs == 0)
    rte_exit(EXIT_FAILURE,
             "Pipeline mode needs at least 2 worker lcores\n");
```

However, this check should be performed earlier, ideally during argument parsing or configuration validation, before attempting to create the mempool and rings. This would fail faster and provide clearer error messages to the user.

**Odd number of threads in pipeline mode**

If `cfg.nb_threads` is odd and greater than 1, the last lcore is silently unused:

```c
nb_pairs = nb_workers / 2;
```

The documentation mentions this ("if `--nb-threads` is odd, the last lcore is unused"), but it would be user-friendly to print a warning when this happens, e.g.:

```c
if (nb_workers % 2 != 0)
    printf("Warning: odd worker count (%u), last lcore unused\n", nb_workers);
```

**Missing release notes**

As with the previous patches, this significant new feature (pipeline mode) should be documented in the release notes.

### Info

**Pipeline mode design**

The pipeline mode design is sound. Using single-producer/single-consumer rings (`RING_F_SP_ENQ | RING_F_SC_DEQ`) is appropriate for paired producer/consumer threads and provides better performance than multi-producer/multi-consumer rings.

The draining logic at the end of `run_pipeline_test()` correctly cleans up any objects left in the rings after workers exit:

```c
for (i = 0; i < nb_pairs; i++) {
    while ((n = rte_ring_dequeue_burst(rings[i], drain, RTE_DIM(drain), NULL)) > 0)
        rte_mempool_put_bulk(mp, drain, n);
    rte_ring_free(rings[i]);
}
```

This prevents mempool object leaks.

---

## Summary

### Critical Issues (Errors)

1. **Patch 2**: `rte_eal_remote_launch()` return value not checked in `run_test()` -- could silently fail to launch workers
2. **Patch 3**: `rte_eal_remote_launch()` return value not checked in `run_pipeline_test()` -- same issue
3. **Patch 3**: Resource leak in `run_pipeline_test()` if ring creation fails -- rings and allocated memory not freed before `rte_exit()`
4. **Patch 3**: Worker malloc failures in `producer_main()`/`consumer_main()` return error codes that are silently ignored

### Warnings

1. **All patches**: Missing release notes entries for new application and features
2. **Patch 1**: Unnecessary zero-initialization of structure members with explanatory comments
3. **Patch 3**: No warning when `--rand-factor` and `--pipeline` are both specified (conflicting options)
4. **Patch 3**: Minimum thread count validation for pipeline mode should happen during config validation, not in `run_pipeline_test()`
5. **Patch 3**: No warning when odd thread count causes last worker to be unused in pipeline mode

### Positive Observations

- Good use of `bool` for predicate functions and flags
- Correct resource cleanup in `worker_main()` (all three pointers freed on any allocation failure)
- Correct use of `+=` for statistics accumulation
- Appropriate use of `volatile` in `access_object()` for forcing memory accesses (not for synchronization)
- Clever shuffle-and-rotate algorithm to ensure valid alloc/free sequences
- Proper ring draining cleanup at end of pipeline test
- Comprehensive documentation in RST files
- Clean code organization and separation of modes

The patches are generally well-written with good structure and documentation. The main issues are around error handling for worker launch failures and resource cleanup on error paths.


More information about the test-report mailing list