libxev is a cross-platform, high-performance event loop that provides abstractions for non-blocking IO, timers, events, and more and works on Linux (io_uring or epoll), macOS (kqueue), and Wasm + WASI. Available as both a Zig and C API.
In the ping-udp1.zig benchmark, there is a potential issue that can arise depending on the order of execution of read/write callbacks.
If everything is fine, the write callback will run first, thus setting the associated Completion to a .dead state and ready to be used again.
However, if the read callback runs first, here is what happens:
During the read callback, we call the Pinger.write(...) method and enqueue a write operation using the Pinger.c_write field. This has the effect to put the completion in the list of completion to submit on the next tick
Then, when the write completion is executed, we set the state to the Completion to .dead
When the time has come to submit the awaiting completion, the c_write completion is discarded because its state is set to .dead
There is sort of a livelock, because the read operation is waiting for a write operation that will never happen
Context
I noticed that while working on the IOCP backend. Somehow, the read completion was handled before the write operation and it lead to the described behavior.
Potential solutions
Solution 1 - Simple but kind of misses the point of the bench
Instead of queuing the read operation at the same time as the write operation, we can wait for the write to be completed. However, this introduce artificial latency and despite making the behavior correct, it doesn't really bench anything as operation are serialized.
Solution 2
Another approach would be to wait for both callbacks to have been executed before enqueuing new operations. That would solve the issue of the c_write completion to be reused before its callback has been called.
What's the issue
In the
ping-udp1.zig
benchmark, there is a potential issue that can arise depending on the order of execution ofread
/write
callbacks.If everything is fine, the
write
callback will run first, thus setting the associatedCompletion
to a.dead
state and ready to be used again.However, if the
read
callback runs first, here is what happens:read
callback, we call thePinger.write(...)
method and enqueue awrite
operation using thePinger.c_write
field. This has the effect to put the completion in the list of completion to submit on the nexttick
write
completion is executed, we set the state to theCompletion
to.dead
c_write
completion is discarded because its state is set to.dead
read
operation is waiting for awrite
operation that will never happenContext
I noticed that while working on the IOCP backend. Somehow, the
read
completion was handled before thewrite
operation and it lead to the described behavior.Potential solutions
Solution 1 - Simple but kind of misses the point of the bench
Instead of queuing the
read
operation at the same time as thewrite
operation, we can wait for thewrite
to be completed. However, this introduce artificial latency and despite making the behavior correct, it doesn't really bench anything as operation are serialized.Solution 2
Another approach would be to wait for both callbacks to have been executed before enqueuing new operations. That would solve the issue of the
c_write
completion to be reused before its callback has been called.