perf(curl): run immediate timeout work without delay
Summary
Libcurl uses a zero timeout to request work as soon as its timer callback returns. The native cURL hook turns that request into a 1 ms Swoole timer, so curl_exec() yields before starting the transfer.
This is visible on fast local requests, where the extra reactor wait can take longer than the transfer itself.
Change
Keep zero-timeout work pending in the existing selector and run it through curl_multi_socket_action() after libcurl's callback returns. The hook no longer suspends while that immediate work is waiting.
Positive timeouts still use Swoole timers. A multi handle already waiting in another coroutine still uses the existing deferred wakeup path.
The zero-timeout path also makes an existing queued-wakeup lifetime bug easy to hit: a multi handle can be released after its wakeup is queued but before that callback runs. Master already has the same lifetime bug through socket readiness and select timeouts. This PR keeps the object alive until its pending callback finishes, matching the 6.1 fix in #6303.
Performance
Measured on Linux with PHP 8.4 and libcurl 8.5 against a local Swoole HTTP server. Each result is the range of p50 values from five rounds of 500 warmed requests using a reused easy handle.
| Measurement | Unchanged master | With this change |
|---|---|---|
curl_exec() wall time | 1.261–1.293 ms | 0.066–0.086 ms |
| libcurl total time | 0.149–0.157 ms | 0.060–0.078 ms |
Tests
- Added a deterministic
file://regression that verifies immediate work starts before a queued reactor defer. It fails on unchangedmasterand passes with this change. - Added a local regression for cancellation and release after the zero-timeout path queues a wakeup. It crashes without the ownership handoff and passes with it, including under Valgrind.
- Built the PHP 8.4 extension with sockets and the native cURL hook.
- Ran the complete cURL multi-handle test directory and the affected easy-handle lifecycle paths.