diff --git a/async.c b/async.c index a0dacae..bb029bd 100644 --- a/async.c +++ b/async.c @@ -717,14 +717,37 @@ PHP_FUNCTION(Async_runtime_stats) { ZEND_PARSE_PARAMETERS_NONE(); + /* + * Before the scheduler is launched (e.g. runtime_stats() called at the top + * level, outside any coroutine), the queue buffers are not yet allocated. + * Report a zeroed snapshot rather than reading uninitialized state; the pool + * minimum and fiber stack size are static and still meaningful. + */ + if (ZEND_ASYNC_SCHEDULER == NULL) { + array_init(return_value); + add_assoc_long(return_value, "coroutines_total", 0); + add_assoc_long(return_value, "coroutines_active", 0); + add_assoc_long(return_value, "microtasks_queue", 0); + add_assoc_long(return_value, "coroutine_queue", 0); + add_assoc_long(return_value, "resumed_queue", 0); + add_assoc_long(return_value, "fiber_pool_count", 0); + add_assoc_long(return_value, "fiber_pool_capacity", 0); + add_assoc_long(return_value, "fiber_pool_min", (zend_long) ASYNC_FIBER_POOL_SIZE); + add_assoc_long(return_value, "fiber_stack_size", (zend_long) EG(fiber_stack_size)); + add_assoc_long(return_value, "fiber_pool_virtual_bytes", 0); + return; + } + const circular_buffer_t *const pool = &ASYNC_G(fiber_context_pool); const circular_buffer_t *const microtasks = &ASYNC_G(microtasks); const circular_buffer_t *const coro_queue = &ASYNC_G(coroutine_queue); const circular_buffer_t *const resumed_q = &ASYNC_G(resumed_coroutines); - const size_t pool_count = circular_buffer_count(pool); - /* circular_buffer_capacity does capacity-1, underflows to SIZE_MAX - * pre-init. Clamp. */ + /* Both reads guard against capacity 0: before the scheduler allocates these + * buffers, circular_buffer_capacity() would underflow (capacity-1 = SIZE_MAX) + * and circular_buffer_count() would read uninitialized head/tail. Clamp to 0. */ + const size_t pool_count = pool->capacity > 0 + ? circular_buffer_count(pool) : 0; const size_t pool_capacity = pool->capacity > 0 ? circular_buffer_capacity(pool) : 0; const size_t stack_size = EG(fiber_stack_size); @@ -737,11 +760,11 @@ PHP_FUNCTION(Async_runtime_stats) (zend_long) ZEND_ASYNC_ACTIVE_COROUTINE_COUNT); add_assoc_long(return_value, "microtasks_queue", - (zend_long) circular_buffer_count(microtasks)); + (zend_long) (microtasks->capacity > 0 ? circular_buffer_count(microtasks) : 0)); add_assoc_long(return_value, "coroutine_queue", - (zend_long) circular_buffer_count(coro_queue)); + (zend_long) (coro_queue->capacity > 0 ? circular_buffer_count(coro_queue) : 0)); add_assoc_long(return_value, "resumed_queue", - (zend_long) circular_buffer_count(resumed_q)); + (zend_long) (resumed_q->capacity > 0 ? circular_buffer_count(resumed_q) : 0)); add_assoc_long(return_value, "fiber_pool_count", (zend_long) pool_count); add_assoc_long(return_value, "fiber_pool_capacity", (zend_long) pool_capacity); diff --git a/internal/circular_buffer.c b/internal/circular_buffer.c index cdbdb98..91ff28a 100644 --- a/internal/circular_buffer.c +++ b/internal/circular_buffer.c @@ -520,6 +520,16 @@ bool circular_buffer_is_full(const circular_buffer_t *buffer) size_t circular_buffer_count(const circular_buffer_t *buffer) { ZEND_ASSERT(buffer != NULL && "Buffer cannot be NULL"); + + /* + * A buffer that was never initialized (capacity 0 — e.g. a scheduler queue + * read before the scheduler has allocated it) holds nothing. Return 0 rather + * than asserting the head/tail bounds, where 0 < 0 would fail. + */ + if (buffer->capacity == 0) { + return 0; + } + ZEND_ASSERT(buffer->head < buffer->capacity && "Head index out of bounds"); ZEND_ASSERT(buffer->tail < buffer->capacity && "Tail index out of bounds"); diff --git a/tests/common/runtime_stats.phpt b/tests/common/runtime_stats.phpt new file mode 100644 index 0000000..a059d16 --- /dev/null +++ b/tests/common/runtime_stats.phpt @@ -0,0 +1,42 @@ +--TEST-- +Async\runtime_stats(): safe to call before the scheduler starts (#164) +--FILE-- += 1); +})); + +echo "done\n"; +?> +--EXPECT-- +bool(true) +bool(true) +bool(true) +bool(true) +bool(true) +bool(true) +done