Repository navigation
Description
The POSIX cpp.net.Poll backend calls poll() on the entire prepared descriptor array for every events call. With thousands of mostly idle sockets, this makes each wait scale with all registrations rather than the ready set.
A Linux epoll backend owned by the runtime could keep registrations between calls while preserving the existing Haxe Poll API. No compiler or standard-library change is required to make the backend persistent.
Measured on fe67557, WSL Ubuntu x86_64, Haxe 4.3.3, g++ 13.3.0, default collector:
| Idle sockets | Current Poll.events(0), ms / 1,000 calls | Prepared native epoll_wait(0), ms / 1,000 calls |
|---|---|---|
| 64 | 4.2636 | 0.1184 |
| 1,024 | 66.9241 | 0.1218 |
| 4,096 | 297.1804 | 0.1175 |
| 8,192 | 603.4032 | 0.1175 |
A second fresh process measured 604.1253 ms versus 0.1172 ms at 8,192 sockets. Earlier independent runs of the idle-only comparison measured 628.9079 and 652.2511 ms for Poll at the same size.
Method:
- Use the same ordinary UDP sockets, bound to ephemeral ports on 127.0.0.1, in both prepared sets. No external network service is involved.
- Create sockets, call Poll.prepare, register epoll interests and warm both paths outside timing.
- Nine samples per path, 1,000 zero-time calls per sample, alternating execution order. Report the median.
- The native control enters and exits the same GC-free zone around epoll_wait. GC remains enabled.
- Verify both paths return no ready sockets during the idle measurement.
- After timing, send one datagram to the last registered socket. Both paths must report its correct index, then return no readiness after consuming it. This passed at every population size in both fresh processes.
The native control calls the OS API directly and does not implement a libhxcpp backend. These timings show the mostly idle polling opportunity, not a prediction of application-wide speedup or active-event performance. Registration cost is deliberately excluded because the proposed backend would retain registrations across events calls. Workloads that rebuild every registration set each iteration need separate measurement.
Possible implementation scope:
- Add a Linux epoll backend inside the existing native polldata lifecycle, with poll as a fallback. Consider kqueue separately where appropriate; it was not benchmarked here.
- Handle repeated prepare calls, ordering changes, read/write interests for the same socket and descriptor reuse. Return indexes in the current API's prepared-array order even if the OS returns events in another order.
- Preserve timeout and EINTR behavior, EOF and failed-connection reporting, explicit close behavior, GC-safe blocking, and close-on-exec for the backend descriptor.
- Retain registered wrappers as tracked separately in Prepared socket polls do not retain registered wrappers, allowing GC to close active connections #286. Benchmark sparse readiness, dense readiness and prepare churn before choosing backend policy.
Added a local conventional regression before implementation work, test/regression/PollRegistrationLifecycle. It passes on Windows x64 and Linux x86_64 and covers idle sets, multiple readable indexes, reordered registrations, removed registrations, write-only sets and one socket registered for both read and write. There are no timing assertions and no runtime implementation changes.
Activity
Add socket line reading and Poll benchmarks
Keep Poll registrations in the kernel with epoll and kqueue