[core] Replace scheduler pool vector with unbounded intrusive freelist

The fixed MAX_POOL_SIZE=5 cap was the source of the heap churn the pool was
meant to prevent: any device with more than 5 concurrent timers (e.g. a board
with 30+ LD2450 sensors) hit a steady-state oscillation of recycle->delete and
acquire->new on every loop iteration.

Replace std::vector<SchedulerItem*> with a singly-linked freelist threaded
through SchedulerItem::next_free, which shares storage with `component` via an
anonymous union (zero per-item overhead -- the component pointer is dead while
pooled). Drop the cap entirely: the freelist quiesces at the application's
natural concurrent-timer high-water mark, which is the working set the device
already needs while those timers are active.

No std::vector means no growth-doubling slack and no realloc copies during
warm-up. Caller of get_item_from_pool_locked_() must overwrite item->component
before unlocking (already true at the sole call site); nullptr remains a valid
live `component` value for SELF_POINTER items, so we cannot pre-clear it.
This commit is contained in:
J. Nick Koston
2026-04-30 10:42:46 -05:00
parent 2758aa5517
commit f9b87d0ede
5 changed files with 43 additions and 73 deletions
+5 -5
View File
@@ -101,8 +101,8 @@ static void Scheduler_SetTimeout(benchmark::State &state) {
Component dummy_component;
// Register 3 timeouts then call() — realistic worst case where multiple
// components schedule in the same loop iteration. Keeps item count within
// the recycling pool (MAX_POOL_SIZE=5) to avoid spurious malloc/free.
// components schedule in the same loop iteration. warm_pool fills the
// freelist so acquire/recycle never falls back to malloc.
static constexpr int kBatchSize = 3;
static_assert(kInnerIterations % kBatchSize == 0, "kInnerIterations must be divisible by kBatchSize");
warm_pool(scheduler, &dummy_component, kBatchSize, 1000);
@@ -209,9 +209,9 @@ static void Scheduler_SetTimeout_ExceedPool(benchmark::State &state) {
Scheduler scheduler;
Component dummy_component;
// Register 10 timeouts then call() — exceeds MAX_POOL_SIZE=5 to measure
// the performance cliff when the recycling pool is exhausted and items
// must be malloc'd/freed.
// Register 10 timeouts then call() — larger working set than the 3-item
// batches above. With the unbounded freelist, warm_pool preallocates 10
// items so this measures steady-state, not malloc cliff.
static constexpr int kBatchSize = 10;
static_assert(kInnerIterations % kBatchSize == 0, "kInnerIterations must be divisible by kBatchSize");
warm_pool(scheduler, &dummy_component, kBatchSize, 1000);
@@ -221,14 +221,10 @@ script:
- id: test_full_pool_reuse
then:
- lambda: |-
ESP_LOGI("test", "Phase 6: Testing pool size limits after Phase 5 items complete");
ESP_LOGI("test", "Phase 6: Testing pool reuse after Phase 5 items complete");
// At this point, all Phase 5 timeouts should have completed and been recycled.
// The pool should be at its maximum size (5).
// Creating 10 new items tests that:
// - First 5 items reuse from the pool
// - Remaining 5 items allocate new (pool empty)
// - Pool doesn't grow beyond MAX_POOL_SIZE of 5
// Phase 5 timeouts have completed and been recycled. The freelist is unbounded;
// creating 10 new items reuses from it and only allocates fresh when empty.
auto *component = id(test_sensor);
int full_reuse_count = 10;
+4 -10
View File
@@ -180,16 +180,10 @@ async def test_scheduler_pool(
# Verify pool behavior
assert pool_recycle_count > 0, "Should have recycled items to pool"
# Check pool metrics
if pool_recycle_count > 0:
max_pool_size = 0
for line in log_lines:
if match := recycle_pattern.search(line):
size = int(match.group(1))
max_pool_size = max(max_pool_size, size)
# Pool can grow up to its maximum of 5
assert max_pool_size <= 5, f"Pool grew beyond maximum ({max_pool_size})"
# Pool is unbounded; the cap was the source of the churn it was meant to prevent.
assert pool_full_count == 0, (
f"Pool should never report full (got {pool_full_count})"
)
# Log summary for debugging
print("\nScheduler Pool Test Summary (Python Orchestrated):")