Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 29 additions & 6 deletions async.c
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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);
Expand Down
10 changes: 10 additions & 0 deletions internal/circular_buffer.c
Original file line number Diff line number Diff line change
Expand Up @@ -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");

Expand Down
42 changes: 42 additions & 0 deletions tests/common/runtime_stats.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
--TEST--
Async\runtime_stats(): safe to call before the scheduler starts (#164)
--FILE--
<?php

use function Async\runtime_stats;
use function Async\spawn;
use function Async\await;

// Called at the top level: before any spawn/await the scheduler has not
// launched and the queue buffers are unallocated. This used to abort the
// process (assert head < capacity, i.e. 0 < 0) on a debug build; it must now
// return a zeroed snapshot with the full set of keys.
$stats = runtime_stats();
var_dump(is_array($stats));

$keys = ['coroutines_total', 'coroutines_active', 'microtasks_queue',
'coroutine_queue', 'resumed_queue', 'fiber_pool_count',
'fiber_pool_capacity', 'fiber_pool_min', 'fiber_stack_size',
'fiber_pool_virtual_bytes'];
var_dump(array_diff($keys, array_keys($stats)) === []);

// Live counters are zero before the scheduler runs; static fields still present.
var_dump($stats['coroutines_total'] === 0);
var_dump($stats['fiber_pool_virtual_bytes'] === 0);
var_dump(is_int($stats['fiber_pool_min']));

// Inside a coroutine the scheduler is running, so live values are reported.
await(spawn(function (): void {
var_dump(runtime_stats()['coroutines_total'] >= 1);
}));

echo "done\n";
?>
--EXPECT--
bool(true)
bool(true)
bool(true)
bool(true)
bool(true)
bool(true)
done
Loading