Skip to content

Reduced CPU usage - #11

Merged
g2px1 merged 7 commits into
mainfrom
optimizations
Apr 10, 2026
Merged

Reduced CPU usage#11
g2px1 merged 7 commits into
mainfrom
optimizations

Conversation

@g2px1

@g2px1 g2px1 commented Apr 10, 2026

Copy link
Copy Markdown
Contributor

Summary

Performance and correctness fixes across the timer wheel, accept path, and coroutine dispatch.


Changes

perf(timer): replace std::list buckets with std::vector, fix O(n) scan in updateNextExpiryTime

Timer wheel buckets used std::list<Timer*>, causing pointer chasing on every tick iteration.
Replaced with std::vector<Timer*> and swap-and-pop removal — O(1), order within a bucket is irrelevant.

updateNextExpiryTime was called after every advance() — up to N times per tick, each doing a full
scan of all 1024 buckets. Fixed by maintaining nextExpiryTime_ incrementally in addTimerToWheel
and invalidating it to 0 in removeTimerFromWheel. Full rescan now runs only when the minimum is
unknown (after removal or expiry of the minimum timer). Added early return for elapsed == 0.

Flamegraph before: ~85% CPU in TimerWheel::tick
Flamegraph after:  ~1.4%, on par with epoll and coroutine dispatch

feat(net): add async_accept(F, Args...) overload, deprecate async_accept()

The zero-argument async_accept() accepted one connection per ET wakeup and returned. In
edge-triggered mode (epoll ET / kqueue EV_CLEAR) this silently drops all connections buffered
since the last edge.

New overload drains the accept queue in a tight loop until EAGAIN before suspending — one suspend
per batch of connections instead of one per socket. F may be a plain void callback or a
coroutine function; coroutines are automatically spawned via co_spawn. Extra arguments are
forwarded to F on each call via variadic Args....

Implemented independently per backend:

Backend Strategy
Linux epoll Drain loop + AwaiterAccept on EAGAIN
Linux io_uring Try accept4 first, fall back to AcceptAwaiter SQE
BSD / macOS kqueue Same drain strategy, manual fcntl(O_NONBLOCK|FD_CLOEXEC)
Windows IOCP Full AcceptEx loop inlined, no recursive async_accept() call

Old overload preserved with [[deprecated]] for backwards compatibility.


fix(system): correct promise cast in co_spawn_static overloads

Template overload cast promise to AwaitableFrameBase by value, slicing the object —
set_thread_id was never called on the original. Fixed with static_cast<AwaitableFrameBase*>.

Handle overload had a malformed cast on coroutine_handle<> — erased handles cannot be cast
directly. Fixed by restoring the typed handle via from_address before accessing the promise.


Testing

Profiled under wrk load (-t8 -c500 -d30s) with perf record -F 999. Flamegraph attached.
No regressions in existing sync primitives, channels, or timer tests.

@g2px1
g2px1 merged commit 76d7672 into main Apr 10, 2026
3 checks passed
@g2px1
g2px1 deleted the optimizations branch May 4, 2026 21:44
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant