@@ -251,6 +251,39 @@ typedef struct _gc_root_buffer {
251251 zend_refcounted * ref ;
252252} gc_root_buffer ;
253253
254+ typedef struct _gc_stack gc_stack ;
255+
256+ #define GC_STACK_SEGMENT_SIZE (((4096 - ZEND_MM_OVERHEAD) / sizeof(void*)) - 2)
257+
258+ struct _gc_stack {
259+ gc_stack * prev ;
260+ gc_stack * next ;
261+ zend_refcounted * data [GC_STACK_SEGMENT_SIZE ];
262+ };
263+
264+ #ifdef PHP_ASYNC_API
265+
266+ typedef enum {
267+ GC_ASYNC_STATE_NONE = 0 ,
268+ GC_ASYNC_STATE_INIT , // initial state
269+ GC_ASYNC_STATE_RUNNING , // GC is running
270+ GC_ASYNC_STATE_CONTINUE // GC is called from GC coroutine
271+ } gc_async_state_t ;
272+
273+ typedef struct {
274+ gc_async_state_t state ; // state
275+ int total_count ; // total freed objects in this GC run
276+ int count ; // freed objects in this GC run
277+
278+ uint32_t gc_flags ; // GC_HAS_DESTRUCTORS, etc.
279+ bool should_rerun_gc ; // run GC again after destructors
280+ bool did_rerun_gc ; // guard against endless reruns
281+
282+ zend_hrtime_t start_time ; // start of full GC pass
283+ zend_hrtime_t dtor_start_time ; // start of dtor phase
284+ } gc_async_context_t ;
285+ #endif
286+
254287typedef struct _zend_gc_globals {
255288 gc_root_buffer * buf ; /* preallocated arrays of buffers */
256289
@@ -278,9 +311,11 @@ typedef struct _zend_gc_globals {
278311 zend_fiber * dtor_fiber ;
279312 bool dtor_fiber_running ;
280313#ifdef PHP_ASYNC_API
281- zend_coroutine_t * dtor_coroutine ;
282- zend_async_scope_t * dtor_scope ;
283- zend_async_microtask_t * microtask ;
314+ gc_async_context_t async_context ; /* async context for gc */
315+ gc_stack * gc_stack ; /* local mark/scan stack */
316+ zend_coroutine_t * dtor_coroutine ;
317+ zend_async_scope_t * dtor_scope ;
318+ zend_async_microtask_t * microtask ;
284319#endif
285320
286321#if GC_BENCH
@@ -316,17 +351,6 @@ static zend_gc_globals gc_globals;
316351# define GC_BENCH_PEAK (peak , counter )
317352#endif
318353
319-
320- #define GC_STACK_SEGMENT_SIZE (((4096 - ZEND_MM_OVERHEAD) / sizeof(void*)) - 2)
321-
322- typedef struct _gc_stack gc_stack ;
323-
324- struct _gc_stack {
325- gc_stack * prev ;
326- gc_stack * next ;
327- zend_refcounted * data [GC_STACK_SEGMENT_SIZE ];
328- };
329-
330354#define GC_STACK_DCL (init ) \
331355 gc_stack *_stack = init; \
332356 size_t _top = 0;
@@ -516,6 +540,8 @@ static void gc_globals_ctor_ex(zend_gc_globals *gc_globals)
516540 gc_globals -> dtor_coroutine = NULL ;
517541 gc_globals -> dtor_scope = NULL ;
518542 gc_globals -> microtask = NULL ;
543+ gc_globals -> async_context .state = GC_ASYNC_STATE_NONE ;
544+ gc_globals -> gc_stack = NULL ;
519545#endif
520546
521547#if GC_BENCH
@@ -542,6 +568,12 @@ void gc_globals_dtor(void)
542568#ifndef ZTS
543569 root_buffer_dtor (& gc_globals );
544570#endif
571+
572+ #ifdef PHP_ASYNC_API
573+ if (GC_G (dtor_scope )) {
574+ GC_G (dtor_scope ) = NULL ;
575+ }
576+ #endif
545577}
546578
547579void gc_reset (void )
@@ -1973,9 +2005,34 @@ static void zend_gc_collect_cycles_coroutine(void)
19732005 GC_G (microtask ) = task ;
19742006 }
19752007
2008+ if (GC_ASYNC_STATE_NONE == GC_G (async_context ).state ) {
2009+ gc_async_context_t * context = & GC_G (async_context );
2010+ context -> state = GC_ASYNC_STATE_INIT ;
2011+ context -> total_count = 0 ;
2012+ context -> count = 0 ;
2013+ context -> start_time = zend_hrtime ();
2014+ context -> should_rerun_gc = 0 ;
2015+ context -> did_rerun_gc = 0 ;
2016+ context -> gc_flags = 0 ;
2017+ } else if (GC_G (async_context ).state == GC_ASYNC_STATE_RUNNING ) {
2018+ GC_G (async_context ).state = GC_ASYNC_STATE_CONTINUE ;
2019+ }
2020+
2021+ if (GC_G (gc_stack ) == NULL ) {
2022+ gc_stack * stack = ecalloc (1 , sizeof (gc_stack ));
2023+ stack -> prev = NULL ;
2024+ stack -> next = NULL ;
2025+ GC_G (gc_stack ) = stack ;
2026+ }
2027+
19762028 ZEND_ASYNC_ADD_MICROTASK (GC_G (microtask ));
19772029 zend_gc_collect_cycles ();
19782030
2031+ // Coroutines were separated.
2032+ if (GC_G (dtor_coroutine ) != ZEND_ASYNC_CURRENT_COROUTINE ) {
2033+ return ;
2034+ }
2035+
19792036 if (GC_G (microtask ) != NULL ) {
19802037 GC_G (microtask )-> is_cancelled = true;
19812038 ZEND_ASYNC_MICROTASK_RELEASE (GC_G (microtask ));
@@ -2006,11 +2063,30 @@ static void coroutine_dispose(zend_coroutine_t *coroutine)
20062063 if (coroutine == GC_G (dtor_coroutine )) {
20072064 GC_TRACE ("GC coroutine finished" );
20082065 GC_G (dtor_coroutine ) = NULL ;
2066+ GC_G (dtor_scope ) = NULL ;
20092067
20102068 if (GC_G (microtask ) != NULL ) {
20112069 GC_G (microtask )-> is_cancelled = true;
20122070 zend_gc_collect_cycles_microtask_dtor (GC_G (microtask ));
20132071 }
2072+
2073+ if (GC_G (gc_stack ) != NULL ) {
2074+ gc_stack * stack = GC_G (gc_stack );
2075+ GC_G (gc_stack ) = NULL ;
2076+ gc_stack_free (stack );
2077+ efree (stack );
2078+ }
2079+
2080+ if (GC_G (async_context .state )) {
2081+ gc_async_context_t * context = & GC_G (async_context );
2082+ context -> state = GC_ASYNC_STATE_NONE ;
2083+ context -> total_count = 0 ;
2084+ context -> count = 0 ;
2085+ context -> start_time = 0 ;
2086+ context -> should_rerun_gc = 0 ;
2087+ context -> did_rerun_gc = 0 ;
2088+ context -> gc_flags = 0 ;
2089+ }
20142090 }
20152091}
20162092
@@ -2074,48 +2150,115 @@ ZEND_API int zend_gc_collect_cycles(void)
20742150
20752151#endif
20762152
2153+ #ifdef PHP_ASYNC_API
2154+ #define GC_COLLECT_TOTAL_COUNT (context->total_count)
2155+ #define GC_COLLECT_COUNT (context->count)
2156+ #define GC_COLLECT_START_TIME (context->start_time)
2157+ #define GC_COLLECT_SHOULD_RERUN_GC (context->should_rerun_gc)
2158+ #define GC_COLLECT_DID_RERUN_GC (context->did_rerun_gc)
2159+ #define GC_COLLECT_GC_FLAGS (context->gc_flags)
2160+ #define GC_COLLECT_STACK (stack)
2161+ #define GC_COLLECT_FREE_STACK GC_G(gc_stack) = NULL;\
2162+ gc_stack_free(stack); \
2163+ efree(stack)
2164+ #define GC_COLLECT_FINISH_0 GC_G(async_context).state = GC_ASYNC_STATE_NONE; \
2165+ return 0
2166+
2167+ // Someone is trying to invoke GC from within a destructor?
2168+ // We don’t know what that is, but we have nothing to do here.
2169+ if (UNEXPECTED (GC_G (async_context ).state == GC_ASYNC_STATE_RUNNING )) {
2170+ return 0 ;
2171+ }
2172+
2173+ //
2174+ // We might enter this context from different coroutines, so we don’t initialize anything here.
2175+ //
2176+ gc_async_context_t * context = & GC_G (async_context );
2177+ gc_stack * stack = GC_G (gc_stack );
2178+
2179+ if (UNEXPECTED (context -> state == GC_ASYNC_STATE_CONTINUE )) {
2180+ // If we reach this point, it means the destructor call was interrupted by a suspend() operation,
2181+ // so we continue the process from the next element.
2182+ GC_G (async_context ).state = GC_ASYNC_STATE_RUNNING ;
2183+ GC_G (dtor_idx )++ ;
2184+ goto continue_calling_destructors ;
2185+ } else {
2186+ context -> state = GC_ASYNC_STATE_INIT ;
2187+ context -> total_count = 0 ;
2188+ context -> count = 0 ;
2189+ context -> start_time = zend_hrtime ();
2190+ context -> should_rerun_gc = 0 ;
2191+ context -> did_rerun_gc = 0 ;
2192+ context -> gc_flags = 0 ;
2193+
2194+ if (GC_G (gc_stack ) == NULL ) {
2195+ stack = ecalloc (1 , sizeof (gc_stack ));
2196+ stack -> prev = NULL ;
2197+ stack -> next = NULL ;
2198+ GC_G (gc_stack ) = stack ;
2199+ } else {
2200+ stack = GC_G (gc_stack );
2201+ }
2202+ }
2203+
2204+ context -> state = GC_ASYNC_STATE_RUNNING ;
2205+
2206+ #else
2207+ #define GC_COLLECT_COUNT count
2208+ #define GC_COLLECT_TOTAL_COUNT total_count
2209+ #define GC_COLLECT_START_TIME start_time
2210+ #define GC_COLLECT_SHOULD_RERUN_GC should_rerun_gc
2211+ #define GC_COLLECT_DID_RERUN_GC did_rerun_gc
2212+ #define GC_COLLECT_GC_FLAGS gc_flags
2213+ #define GC_COLLECT_STACK (&stack)
2214+ #define GC_COLLECT_FREE_STACK gc_stack_free(&stack);
2215+ #define GC_COLLECT_FINISH_0 return 0
2216+
20772217 int total_count = 0 ;
20782218 bool should_rerun_gc = 0 ;
20792219 bool did_rerun_gc = 0 ;
20802220
20812221 zend_hrtime_t start_time = zend_hrtime ();
2222+ #endif
2223+
20822224 if (GC_G (num_roots ) && !GC_G (gc_active )) {
20832225 zend_gc_remove_root_tmpvars ();
20842226 }
20852227
20862228rerun_gc :
20872229 if (GC_G (num_roots )) {
2088- int count ;
20892230 gc_root_buffer * current , * last ;
20902231 zend_refcounted * p ;
20912232 uint32_t gc_flags = 0 ;
20922233 uint32_t idx , end ;
2234+ #ifndef PHP_ASYNC_API
2235+ int count ;
20932236 gc_stack stack ;
2094-
20952237 stack .prev = NULL ;
20962238 stack .next = NULL ;
2239+ #endif
20972240
20982241 if (GC_G (gc_active )) {
2099- GC_G (collector_time ) += zend_hrtime () - start_time ;
2100- return 0 ;
2242+ GC_G (collector_time ) += zend_hrtime () - GC_COLLECT_START_TIME ;
2243+ GC_COLLECT_FINISH_0 ;
21012244 }
21022245
21032246 GC_TRACE ("Collecting cycles" );
21042247 GC_G (gc_runs )++ ;
21052248 GC_G (gc_active ) = 1 ;
21062249
21072250 GC_TRACE ("Marking roots" );
2108- gc_mark_roots (& stack );
2251+ gc_mark_roots (GC_COLLECT_STACK );
21092252 GC_TRACE ("Scanning roots" );
2110- gc_scan_roots (& stack );
2253+ gc_scan_roots (GC_COLLECT_STACK );
21112254
21122255 GC_TRACE ("Collecting roots" );
2113- count = gc_collect_roots (& gc_flags , & stack );
2256+ GC_COLLECT_COUNT = gc_collect_roots (& gc_flags , GC_COLLECT_STACK );
21142257
21152258 if (!GC_G (num_roots )) {
21162259 /* nothing to free */
21172260 GC_TRACE ("Nothing to free" );
2118- gc_stack_free ( & stack ) ;
2261+ GC_COLLECT_FREE_STACK ;
21192262 GC_G (gc_active ) = 0 ;
21202263 goto finish ;
21212264 }
@@ -2130,7 +2273,7 @@ ZEND_API int zend_gc_collect_cycles(void)
21302273 * modify any refcounts, so we have no real way to detect this situation
21312274 * short of rerunning full GC tracing. What we do instead is to only run
21322275 * destructors at this point and automatically re-run GC afterwards. */
2133- should_rerun_gc = 1 ;
2276+ GC_COLLECT_SHOULD_RERUN_GC = 1 ;
21342277
21352278 /* Mark all roots for which a dtor will be invoked as DTOR_GARBAGE. Additionally
21362279 * color them purple. This serves a double purpose: First, they should be
@@ -2164,7 +2307,7 @@ ZEND_API int zend_gc_collect_cycles(void)
21642307 while (idx != end ) {
21652308 if (GC_IS_DTOR_GARBAGE (current -> ref )) {
21662309 p = GC_GET_PTR (current -> ref );
2167- count -= gc_remove_nested_data_from_buffer (p , current , & stack );
2310+ GC_COLLECT_COUNT -= gc_remove_nested_data_from_buffer (p , current , GC_COLLECT_STACK );
21682311 }
21692312 current ++ ;
21702313 idx ++ ;
@@ -2173,7 +2316,21 @@ ZEND_API int zend_gc_collect_cycles(void)
21732316 /* Actually call destructors. */
21742317 zend_hrtime_t dtor_start_time = zend_hrtime ();
21752318 if (EXPECTED (!EG (active_fiber ))) {
2319+ #ifdef PHP_ASYNC_API
2320+ continue_calling_destructors :
2321+ if (UNEXPECTED (FAILURE == gc_call_destructors (GC_G (dtor_idx ), GC_G (first_unused ), NULL ))) {
2322+ //
2323+ // gc_call_destructors returns FAILURE when a destructor interrupts execution,
2324+ // i.e. calls suspend().
2325+ // At this point, we are inside a foreign coroutine from which we must immediately exit,
2326+ // because the process continues elsewhere.
2327+ //
2328+ GC_G (dtor_time ) += zend_hrtime () - dtor_start_time ;
2329+ return GC_COLLECT_TOTAL_COUNT ;
2330+ }
2331+ #else
21762332 gc_call_destructors (GC_FIRST_ROOT , end , NULL );
2333+ #endif
21772334 } else {
21782335 gc_call_destructors_in_fiber (end );
21792336 }
@@ -2182,12 +2339,16 @@ ZEND_API int zend_gc_collect_cycles(void)
21822339 if (GC_G (gc_protected )) {
21832340 /* something went wrong */
21842341 zend_get_gc_buffer_release ();
2185- GC_G (collector_time ) += zend_hrtime () - start_time ;
2186- return 0 ;
2342+ GC_G (collector_time ) += zend_hrtime () - GC_COLLECT_START_TIME ;
2343+ GC_COLLECT_FINISH_0 ;
21872344 }
21882345 }
21892346
2190- gc_stack_free (& stack );
2347+ GC_COLLECT_FREE_STACK ;
2348+
2349+ #ifdef PHP_ASYNC_API
2350+ end = GC_G (first_unused );
2351+ #endif
21912352
21922353 /* Destroy zvals. The root buffer may be reallocated. */
21932354 GC_TRACE ("Destroying zvals" );
@@ -2245,8 +2406,8 @@ ZEND_API int zend_gc_collect_cycles(void)
22452406 GC_G (free_time ) += zend_hrtime () - free_start_time ;
22462407
22472408 GC_TRACE ("Collection finished" );
2248- GC_G (collected ) += count ;
2249- total_count += count ;
2409+ GC_G (collected ) += GC_COLLECT_COUNT ;
2410+ GC_COLLECT_TOTAL_COUNT += GC_COLLECT_COUNT ;
22502411 GC_G (gc_active ) = 0 ;
22512412 }
22522413
@@ -2255,8 +2416,8 @@ ZEND_API int zend_gc_collect_cycles(void)
22552416 /* Objects with destructors were removed from this GC run. Rerun GC right away to clean them
22562417 * up. We do this only once: If we encounter more destructors on the second run, we'll not
22572418 * run GC another time. */
2258- if (should_rerun_gc && !did_rerun_gc ) {
2259- did_rerun_gc = 1 ;
2419+ if (GC_COLLECT_SHOULD_RERUN_GC && !GC_COLLECT_DID_RERUN_GC ) {
2420+ GC_COLLECT_DID_RERUN_GC = 1 ;
22602421 goto rerun_gc ;
22612422 }
22622423
@@ -2269,8 +2430,14 @@ ZEND_API int zend_gc_collect_cycles(void)
22692430 zend_gc_check_root_tmpvars ();
22702431 GC_G (gc_active ) = 0 ;
22712432
2272- GC_G (collector_time ) += zend_hrtime () - start_time ;
2273- return total_count ;
2433+ GC_G (collector_time ) += zend_hrtime () - GC_COLLECT_START_TIME ;
2434+ #ifdef PHP_ASYNC_API
2435+ GC_G (async_context ).state = GC_ASYNC_STATE_NONE ;
2436+ if (GC_G (gc_stack ) != NULL ) {
2437+ GC_COLLECT_FREE_STACK ;
2438+ }
2439+ #endif
2440+ return GC_COLLECT_TOTAL_COUNT ;
22742441}
22752442
22762443ZEND_API void zend_gc_get_status (zend_gc_status * status )
0 commit comments