diff --git a/libuv_reactor.c b/libuv_reactor.c index 855424f..774ee4c 100644 --- a/libuv_reactor.c +++ b/libuv_reactor.c @@ -7161,4 +7161,8 @@ void async_libuv_reactor_register(void) async_thread_transfer_zval, async_thread_load_zval, async_thread_release_transferred_zval, async_thread_xlat_put_ctx, async_thread_defer_release_ctx); + + /* Batched multi-root release (one visited set). Additive; set directly rather + * than widening zend_async_thread_pool_register's signature. */ + zend_async_thread_release_transferred_zvals_fn = async_thread_release_transferred_zvals; } diff --git a/thread.c b/thread.c index 569c631..379a6fa 100644 --- a/thread.c +++ b/thread.c @@ -1401,6 +1401,20 @@ void async_thread_release_transferred_zval(zval *z) ZVAL_UNDEF(z); } +/* One release ctx (one visited set) across all roots — unlike the single-zval call, + * which builds a fresh set each time — so an object xlat-deduped into more than one + * root is freed once. Contract and rationale: the typedef in zend_async_API.h. */ +void async_thread_release_transferred_zvals(zval **roots, size_t count) +{ + thread_release_ctx_t ctx; + thread_release_ctx_init(&ctx); + for (size_t i = 0; i < count; i++) { + thread_release_transferred_zval(&ctx, roots[i]); + ZVAL_UNDEF(roots[i]); + } + thread_release_ctx_destroy(&ctx); +} + /* Partial-release for build-error paths: refcount-based so xlat-shared * sub-pieces still owned by the half-built snapshot survive; the snapshot's * full release frees them shortly afterwards. */ diff --git a/thread.h b/thread.h index 1ea4257..c9e995d 100644 --- a/thread.h +++ b/thread.h @@ -176,6 +176,7 @@ void async_thread_load_zval(zval *dst, const zval *src); * Free a persistent zval created by async_thread_transfer_zval(). */ void async_thread_release_transferred_zval(zval *z); +void async_thread_release_transferred_zvals(zval **roots, size_t count); /* Recursion helpers passed to zend_async_thread_pool_register() — expose * thread_transfer_zval_inner / thread_load_zval_inner / xlat_put / a lazy