## Summary
Each TURN permission and channel carried its own libevent timer, torn
down and recreated (`IOA_EVENT_DEL` + `set_ioa_timer`) on **every**
refresh. At high allocation counts this is the dominant scheduling and
memory cost: ~200k+ live libevent events at 100k allocations (≥2 per
allocation), each ~228 B of resident heap (a `timer_event`, a libevent
`struct event`, and a `strdup` of the handler name) plus a min-heap
node.
This replaces per-object timers with a single **per-thread sweep**.
`timer_timeout_handler` already runs once a second per relay thread on
that thread's own engine; after refreshing `ctime` it now walks the
thread-local `sessions_map` and reaps permissions/channels whose
`expiration_time` has passed, **reusing the existing
`client_ss_perm_timeout_handler` / `client_ss_channel_timeout_handler`**
so teardown (including `mp_deregister_permission_peers` in
multiplex-peer mode) is unchanged. `update_turn_permission_lifetime` /
`update_channel_lifetime` now just push `expiration_time` forward.
The now-dead `lifetime_ev` fields are removed from
`turn_permission_info` (648→640 B) and `ch_info` (64→56 B), along with
the spurious "strange permission" error log that would otherwise fire on
every sweep-driven expiry.
## Why
For workloads with many concurrent allocations (e.g. 100k VoIP
sessions), the per-object timer model puts ~200k+ nodes in libevent's
min-heap and burns a free/malloc/strdup cycle on every refresh. The
expiry deadline is already stored in `expiration_time`; a periodic sweep
makes the timer redundant.
## Trade-offs
- Expiry latency rises to **≤1s** (negligible vs 300–600s
permission/channel TTLs).
- The sweep is a bounded per-thread array walk — ~39k trivial
comparisons/thread/s at 100k allocations over 128 threads.
## Performance (measured)
Faithful libevent 2.1.12 microbenchmark of the create/refresh/destroy
path:
| Metric | Before | After |
|---|---|---|
| Resident heap per timer | ~228 B | 0 |
| Refresh op | 110 ns (del+create at 100k heap depth) | 0.28 ns (field
store) |
| `sizeof(turn_permission_info)` | 648 B | 640 B |
| `sizeof(ch_info)` | 64 B | 56 B |
At 100k allocations (≥2 timers each): **~46 MB** of libevent objects
freed and ~200k fewer min-heap nodes.
## Testing
- Unit (`ctest`), `run_tests.sh`, `run_tests_conf.sh`,
`run_tests_multiplex_peer.sh` — pass on macOS and in a clean Ubuntu
24.04 Docker build.
- New `examples/run_tests_expiry.sh`: forces 4s server-side
permission/channel lifetimes so the sweep must reap mid-session, and
asserts the verbose log shows reaping while the server stays healthy.
**Verified to FAIL when the sweep call is disabled** (negative control),
confirming it catches the regression rather than passing trivially.