From 3c56af99023b9eb82620e18f00ec37100aadd289 Mon Sep 17 00:00:00 2001 From: Arnaud Le Blanc Date: Wed, 21 Feb 2024 16:33:33 +0100 Subject: [PATCH] Allow fiber switching during destructor execution Fiber switching was disabled during destructor execution due to conflicts with the garbage collector. This unfortunately introduces a function color problem: destructors can not call functions that may switch Fibers. In this change we update the GC so that Fiber switching during GC is safe. In turn we allow Fiber switching during destrutor execution. The GC executes destructors in a dedicated Fiber. If a destructor suspends, the Fiber is owned by userland and a new dedicated Fiber is created to execute the remaining destructors. Destructor suspension results in a resurection of the object, which is handled as usual: The object is not considered garbage anymore, but may be collected in a later run. When the GC is executed in the main context (not in a Fiber), then destructors are executed in the main context as well because there is no risk of conflicting with GC in this case (main context can not suspend). Fixes GH-11389 Closes GH-13460 --- Zend/tests/bug69446.phpt | 8 +- Zend/tests/fibers/destructors_001.phpt | 53 +++++ Zend/tests/fibers/destructors_002.phpt | 35 +++ Zend/tests/fibers/destructors_003.phpt | 42 ++++ Zend/tests/fibers/destructors_004.phpt | 79 +++++++ Zend/tests/fibers/destructors_005.phpt | 60 +++++ Zend/tests/fibers/destructors_006.phpt | 52 +++++ Zend/tests/fibers/destructors_007.phpt | 54 +++++ Zend/tests/fibers/destructors_008.phpt | 39 ++++ Zend/tests/fibers/destructors_009.phpt | 39 ++++ Zend/tests/fibers/destructors_010.phpt | 40 ++++ Zend/tests/fibers/no-switch-dtor-resume.phpt | 30 --- Zend/tests/fibers/no-switch-dtor-start.phpt | 20 -- Zend/tests/fibers/no-switch-dtor-suspend.phpt | 24 -- Zend/tests/fibers/no-switch-dtor-throw.phpt | 30 --- .../fibers/no-switch-force-close-finally.phpt | 31 --- Zend/tests/fibers/no-switch-gc.phpt | 36 --- Zend/zend.c | 1 + Zend/zend_fibers.h | 4 + Zend/zend_gc.c | 214 +++++++++++++++--- Zend/zend_gc.h | 1 + Zend/zend_objects_API.c | 6 - 22 files changed, 688 insertions(+), 210 deletions(-) create mode 100644 Zend/tests/fibers/destructors_001.phpt create mode 100644 Zend/tests/fibers/destructors_002.phpt create mode 100644 Zend/tests/fibers/destructors_003.phpt create mode 100644 Zend/tests/fibers/destructors_004.phpt create mode 100644 Zend/tests/fibers/destructors_005.phpt create mode 100644 Zend/tests/fibers/destructors_006.phpt create mode 100644 Zend/tests/fibers/destructors_007.phpt create mode 100644 Zend/tests/fibers/destructors_008.phpt create mode 100644 Zend/tests/fibers/destructors_009.phpt create mode 100644 Zend/tests/fibers/destructors_010.phpt delete mode 100644 Zend/tests/fibers/no-switch-dtor-resume.phpt delete mode 100644 Zend/tests/fibers/no-switch-dtor-start.phpt delete mode 100644 Zend/tests/fibers/no-switch-dtor-suspend.phpt delete mode 100644 Zend/tests/fibers/no-switch-dtor-throw.phpt delete mode 100644 Zend/tests/fibers/no-switch-force-close-finally.phpt delete mode 100644 Zend/tests/fibers/no-switch-gc.phpt diff --git a/Zend/tests/bug69446.phpt b/Zend/tests/bug69446.phpt index 6bc3c700689..f1b3b9984cb 100644 --- a/Zend/tests/bug69446.phpt +++ b/Zend/tests/bug69446.phpt @@ -23,12 +23,12 @@ unset($foo); gc_collect_cycles(); var_dump($bar); ?> ---EXPECT-- -object(bad)#2 (2) { +--EXPECTF-- +object(bad)#%d (2) { ["x"]=> - object(stdClass)#3 (0) { + object(stdClass)#%d (0) { } ["y"]=> - object(stdClass)#4 (0) { + object(stdClass)#%d (0) { } } diff --git a/Zend/tests/fibers/destructors_001.phpt b/Zend/tests/fibers/destructors_001.phpt new file mode 100644 index 00000000000..7a5ef8c328d --- /dev/null +++ b/Zend/tests/fibers/destructors_001.phpt @@ -0,0 +1,53 @@ +--TEST-- +Fibers in destructors 001: Suspend in destructor +--FILE-- +self = $this; + } + public function __destruct() { + $id = self::$counter++; + printf("%d: Start destruct\n", $id); + if ($id === 0) { + global $f2; + $f2 = Fiber::getCurrent(); + Fiber::suspend(new stdClass); + } + printf("%d: End destruct\n", $id); + } +} + +$f = new Fiber(function () { + global $f2; + new Cycle(); + new Cycle(); + new Cycle(); + new Cycle(); + new Cycle(); + gc_collect_cycles(); + $f2->resume(); +}); + +$f->start(); + +?> +--EXPECT-- +0: Start destruct +1: Start destruct +1: End destruct +2: Start destruct +2: End destruct +3: Start destruct +3: End destruct +4: Start destruct +4: End destruct +0: End destruct +Shutdown diff --git a/Zend/tests/fibers/destructors_002.phpt b/Zend/tests/fibers/destructors_002.phpt new file mode 100644 index 00000000000..aca5cf5bfdd --- /dev/null +++ b/Zend/tests/fibers/destructors_002.phpt @@ -0,0 +1,35 @@ +--TEST-- +Fibers in destructors 002: Start in destructor +--FILE-- +self = $this; + } + public function __destruct() { + $id = self::$counter++; + printf("%d: Start destruct\n", $id); + $f = new Fiber(function () { }); + $f->start(); + printf("%d: End destruct\n", $id); + } +} + +new Cycle(); +new Cycle(); +gc_collect_cycles(); + +?> +--EXPECT-- +0: Start destruct +0: End destruct +1: Start destruct +1: End destruct +Shutdown diff --git a/Zend/tests/fibers/destructors_003.phpt b/Zend/tests/fibers/destructors_003.phpt new file mode 100644 index 00000000000..84832ae62ef --- /dev/null +++ b/Zend/tests/fibers/destructors_003.phpt @@ -0,0 +1,42 @@ +--TEST-- +Fibers in destructors 003: Resume in destructor +--FILE-- +self = $this; + } + public function __destruct() { + $id = self::$counter++; + printf("%d: Start destruct\n", $id); + global $f; + $f->resume(); + printf("%d: End destruct\n", $id); + } +} + +$f = new Fiber(function () { + while (true) { + Fiber::suspend(); + } +}); +$f->start(); + +new Cycle(); +new Cycle(); +gc_collect_cycles(); + +?> +--EXPECT-- +0: Start destruct +0: End destruct +1: Start destruct +1: End destruct +Shutdown diff --git a/Zend/tests/fibers/destructors_004.phpt b/Zend/tests/fibers/destructors_004.phpt new file mode 100644 index 00000000000..682d6a1243f --- /dev/null +++ b/Zend/tests/fibers/destructors_004.phpt @@ -0,0 +1,79 @@ +--TEST-- +Fibers in destructors 004: Suspend and throw in destructor +--FILE-- +self = $this; + } + public function __destruct() { + $id = self::$counter++; + printf("%d: Start destruct\n", $id); + if ($id === 0) { + global $f2; + $f2 = Fiber::getCurrent(); + Fiber::suspend(new stdClass); + } + printf("%d: End destruct\n", $id); + throw new \Exception(sprintf("%d exception", $id)); + } +} + +$f = new Fiber(function () { + global $f2; + new Cycle(); + new Cycle(); + new Cycle(); + try { + gc_collect_cycles(); + } catch (\Exception $e) { + echo $e, "\n"; + } + $f2->resume(); +}); + +$f->start(); + +?> +--EXPECTF-- +0: Start destruct +1: Start destruct +1: End destruct +2: Start destruct +2: End destruct +Exception: 1 exception in %s:%d +Stack trace: +#0 [internal function]: Cycle->__destruct() +#1 [internal function]: gc_destructor_fiber() +#2 %s(%d): gc_collect_cycles() +#3 [internal function]: {closure:%s:%d}() +#4 %s(%d): Fiber->start() +#5 {main} + +Next Exception: 2 exception in %s:%d +Stack trace: +#0 [internal function]: Cycle->__destruct() +#1 [internal function]: gc_destructor_fiber() +#2 %s(%d): gc_collect_cycles() +#3 [internal function]: {closure:%s:%d}() +#4 %s(%d): Fiber->start() +#5 {main} +0: End destruct + +Fatal error: Uncaught Exception: 0 exception in %s:%d +Stack trace: +#0 [internal function]: Cycle->__destruct() +#1 [internal function]: gc_destructor_fiber() +#2 %s(%d): Fiber->resume() +#3 [internal function]: {closure:%s:%d}() +#4 %s(%d): Fiber->start() +#5 {main} + thrown in %s on line %d +Shutdown diff --git a/Zend/tests/fibers/destructors_005.phpt b/Zend/tests/fibers/destructors_005.phpt new file mode 100644 index 00000000000..c08e82fc31a --- /dev/null +++ b/Zend/tests/fibers/destructors_005.phpt @@ -0,0 +1,60 @@ +--TEST-- +Fibers in destructors 005: Suspended and not resumed destructor +--FILE-- +self = $this; + } + public function __destruct() { + printf("%d: Start destruct\n", $this->id); + try { + if ($this->id === 0) { + /* Fiber will be collected by GC because it's not referenced */ + Fiber::suspend(new stdClass); + } else if ($this->id === 1) { + /* Fiber will be dtor during shutdown */ + global $f2; + $f2 = Fiber::getCurrent(); + Fiber::suspend(new stdClass); + } + } finally { + printf("%d: End destruct\n", $this->id); + } + } +} + +$refs = []; +$f = new Fiber(function () use (&$refs) { + $refs[] = WeakReference::create(new Cycle(0)); + $refs[] = WeakReference::create(new Cycle(1)); + $refs[] = WeakReference::create(new Cycle(2)); + gc_collect_cycles(); +}); + +$f->start(); + +gc_collect_cycles(); + +foreach ($refs as $id => $ref) { + printf("%d: %s\n", $id, $ref->get() ? 'Live' : 'Collected'); +} + +?> +--EXPECT-- +2: Start destruct +2: End destruct +0: Start destruct +0: End destruct +1: Start destruct +0: Collected +1: Live +2: Collected +Shutdown +1: End destruct diff --git a/Zend/tests/fibers/destructors_006.phpt b/Zend/tests/fibers/destructors_006.phpt new file mode 100644 index 00000000000..e2c30ed1a54 --- /dev/null +++ b/Zend/tests/fibers/destructors_006.phpt @@ -0,0 +1,52 @@ +--TEST-- +Fibers in destructors 006: multiple GC runs +--FILE-- +self = $this; + } + public function __destruct() { + $id = self::$counter++; + printf("%d: Start destruct\n", $id); + if ($id === 0) { + global $f2; + $f2 = Fiber::getCurrent(); + Fiber::suspend(new stdClass); + } + printf("%d: End destruct\n", $id); + } +} + +$f = new Fiber(function () { + new Cycle(); + new Cycle(); + gc_collect_cycles(); +}); + +$f->start(); + +new Cycle(); +new Cycle(); +gc_collect_cycles(); + +$f2->resume(); + +?> +--EXPECT-- +0: Start destruct +1: Start destruct +1: End destruct +2: Start destruct +2: End destruct +3: Start destruct +3: End destruct +0: End destruct +Shutdown diff --git a/Zend/tests/fibers/destructors_007.phpt b/Zend/tests/fibers/destructors_007.phpt new file mode 100644 index 00000000000..35e17728e7a --- /dev/null +++ b/Zend/tests/fibers/destructors_007.phpt @@ -0,0 +1,54 @@ +--TEST-- +Fibers in destructors 007: scope destructor +--FILE-- +start(); + break; + case 2: + global $f2; + $f2->resume(); + break; + } + printf("%d: End destruct\n", $id); + } +} + +$f = new Fiber(function () { + new Cycle(); +}); + +$f->start(); + +new Cycle(); +new Cycle(); + +?> +--EXPECT-- +0: Start destruct +1: Start destruct +1: Fiber +1: End destruct +2: Start destruct +0: End destruct +2: End destruct +Shutdown diff --git a/Zend/tests/fibers/destructors_008.phpt b/Zend/tests/fibers/destructors_008.phpt new file mode 100644 index 00000000000..35636a16d79 --- /dev/null +++ b/Zend/tests/fibers/destructors_008.phpt @@ -0,0 +1,39 @@ +--TEST-- +Fibers in destructors 008: Fibers in shutdown sequence +--FILE-- +start(); + $f->resume(); + // Can not suspend main fiber + Fiber::suspend(); + } +} + +C::$instance = new C(); + +?> +--EXPECTF-- +Shutdown +Started +Resumed + +Fatal error: Uncaught FiberError: Cannot suspend outside of a fiber in %s:%d +Stack trace: +#0 %s(%d): Fiber::suspend() +#1 [internal function]: C->__destruct() +#2 {main} + thrown in %s on line %d diff --git a/Zend/tests/fibers/destructors_009.phpt b/Zend/tests/fibers/destructors_009.phpt new file mode 100644 index 00000000000..5e46f934040 --- /dev/null +++ b/Zend/tests/fibers/destructors_009.phpt @@ -0,0 +1,39 @@ +--TEST-- +Fibers in destructors 009: Destructor resurrects object, suspends +--FILE-- +self = $this; + } + public function __destruct() { + global $ref, $f2; + $ref = $this; + $f2 = Fiber::getCurrent(); + Fiber::suspend(); + } +} + +$f = new Fiber(function () { + global $weakRef; + $weakRef = WeakReference::create(new Cycle()); + gc_collect_cycles(); +}); + +$f->start(); +var_dump((bool) $weakRef->get()); +gc_collect_cycles(); +$f2->resume(); +gc_collect_cycles(); +var_dump((bool) $weakRef->get()); +?> +--EXPECT-- +bool(true) +bool(true) +Shutdown diff --git a/Zend/tests/fibers/destructors_010.phpt b/Zend/tests/fibers/destructors_010.phpt new file mode 100644 index 00000000000..9839bf7e09c --- /dev/null +++ b/Zend/tests/fibers/destructors_010.phpt @@ -0,0 +1,40 @@ +--TEST-- +Fibers in destructors 010: Destructor resurrects object, suspends, unrefs +--FILE-- +self = $this; + } + public function __destruct() { + global $ref, $f2; + $ref = $this; + $f2 = Fiber::getCurrent(); + Fiber::suspend(); + $ref = null; + } +} + +$f = new Fiber(function () { + global $weakRef; + $weakRef = WeakReference::create(new Cycle()); + gc_collect_cycles(); +}); + +$f->start(); +var_dump((bool) $weakRef->get()); +gc_collect_cycles(); +$f2->resume(); +gc_collect_cycles(); +var_dump((bool) $weakRef->get()); +?> +--EXPECT-- +bool(true) +bool(false) +Shutdown diff --git a/Zend/tests/fibers/no-switch-dtor-resume.phpt b/Zend/tests/fibers/no-switch-dtor-resume.phpt deleted file mode 100644 index 48ae34300b3..00000000000 --- a/Zend/tests/fibers/no-switch-dtor-resume.phpt +++ /dev/null @@ -1,30 +0,0 @@ ---TEST-- -Cannot resume fiber within destructor ---FILE-- -start(); - -return new class ($fiber) { - private $fiber; - - public function __construct(Fiber $fiber) { - $this->fiber = $fiber; - } - - public function __destruct() { - $this->fiber->resume(1); - } -}; - -?> ---EXPECTF-- -Fatal error: Uncaught FiberError: Cannot switch fibers in current execution context in %sno-switch-dtor-resume.php:%d -Stack trace: -#0 %sno-switch-dtor-resume.php(%d): Fiber->resume(1) -#1 %sno-switch-dtor-resume.php(%d): class@anonymous->__destruct() -#2 {main} - thrown in %sno-switch-dtor-resume.php on line %d diff --git a/Zend/tests/fibers/no-switch-dtor-start.phpt b/Zend/tests/fibers/no-switch-dtor-start.phpt deleted file mode 100644 index 0d81981a1ec..00000000000 --- a/Zend/tests/fibers/no-switch-dtor-start.phpt +++ /dev/null @@ -1,20 +0,0 @@ ---TEST-- -Cannot start fiber within destructor ---FILE-- - null); - $fiber->start(); - } -}; - -?> ---EXPECTF-- -Fatal error: Uncaught FiberError: Cannot switch fibers in current execution context in %sno-switch-dtor-start.php:%d -Stack trace: -#0 %sno-switch-dtor-start.php(%d): Fiber->start() -#1 %sno-switch-dtor-start.php(%d): class@anonymous->__destruct() -#2 {main} - thrown in %sno-switch-dtor-start.php on line %d diff --git a/Zend/tests/fibers/no-switch-dtor-suspend.phpt b/Zend/tests/fibers/no-switch-dtor-suspend.phpt deleted file mode 100644 index 4d634f9a18e..00000000000 --- a/Zend/tests/fibers/no-switch-dtor-suspend.phpt +++ /dev/null @@ -1,24 +0,0 @@ ---TEST-- -Cannot suspend fiber within destructor ---FILE-- -start(); - -?> ---EXPECTF-- -Fatal error: Uncaught FiberError: Cannot switch fibers in current execution context in %sno-switch-dtor-suspend.php:%d -Stack trace: -#0 %sno-switch-dtor-suspend.php(%d): Fiber::suspend() -#1 [internal function]: class@anonymous->__destruct() -#2 %sno-switch-dtor-suspend.php(%d): Fiber->start() -#3 {main} - thrown in %sno-switch-dtor-suspend.php on line %d diff --git a/Zend/tests/fibers/no-switch-dtor-throw.phpt b/Zend/tests/fibers/no-switch-dtor-throw.phpt deleted file mode 100644 index 127d1a74d2b..00000000000 --- a/Zend/tests/fibers/no-switch-dtor-throw.phpt +++ /dev/null @@ -1,30 +0,0 @@ ---TEST-- -Cannot resume fiber within destructor ---FILE-- -start(); - -return new class ($fiber) { - private $fiber; - - public function __construct(Fiber $fiber) { - $this->fiber = $fiber; - } - - public function __destruct() { - $this->fiber->throw(new Error()); - } -}; - -?> ---EXPECTF-- -Fatal error: Uncaught FiberError: Cannot switch fibers in current execution context in %sno-switch-dtor-throw.php:%d -Stack trace: -#0 %sno-switch-dtor-throw.php(%d): Fiber->throw(Object(Error)) -#1 %sno-switch-dtor-throw.php(%d): class@anonymous->__destruct() -#2 {main} - thrown in %sno-switch-dtor-throw.php on line %d diff --git a/Zend/tests/fibers/no-switch-force-close-finally.phpt b/Zend/tests/fibers/no-switch-force-close-finally.phpt deleted file mode 100644 index 62dce4c779a..00000000000 --- a/Zend/tests/fibers/no-switch-force-close-finally.phpt +++ /dev/null @@ -1,31 +0,0 @@ ---TEST-- -Cannot start a new fiber in a finally block in a force-closed fiber ---FILE-- -start(); - } -}); - -$fiber->start(); - -?> ---EXPECTF-- -finally - -Fatal error: Uncaught FiberError: Cannot switch fibers in current execution context in %sno-switch-force-close-finally.php:%d -Stack trace: -#0 %sno-switch-force-close-finally.php(%d): Fiber->start() -#1 [internal function]: {closure:%s:%d}() -#2 {main} - thrown in %sno-switch-force-close-finally.php on line %d diff --git a/Zend/tests/fibers/no-switch-gc.phpt b/Zend/tests/fibers/no-switch-gc.phpt deleted file mode 100644 index 6773edc0ff2..00000000000 --- a/Zend/tests/fibers/no-switch-gc.phpt +++ /dev/null @@ -1,36 +0,0 @@ ---TEST-- -Context switches are prevented during GC collect cycles ---FILE-- -next = $b; - $b->next = $a; - }); - - gc_collect_cycles(); -}); - -$fiber->start(); - -?> ---EXPECTF-- -Fatal error: Uncaught FiberError: Cannot switch fibers in current execution context in %sno-switch-gc.php:%d -Stack trace: -#0 %sno-switch-gc.php(%d): Fiber::suspend() -#1 [internal function]: class@anonymous->__destruct() -#2 %sno-switch-gc.php(%d): gc_collect_cycles() -#3 [internal function]: {closure:%s:%d}() -#4 %sno-switch-gc.php(%d): Fiber->start() -#5 {main} - thrown in %sno-switch-gc.php on line %d diff --git a/Zend/zend.c b/Zend/zend.c index efff18b7b67..49b63e4bccc 100644 --- a/Zend/zend.c +++ b/Zend/zend.c @@ -1128,6 +1128,7 @@ zend_result zend_post_startup(void) /* {{{ */ #ifdef ZEND_CHECK_STACK_LIMIT zend_call_stack_init(); #endif + gc_init(); return SUCCESS; } diff --git a/Zend/zend_fibers.h b/Zend/zend_fibers.h index 5c81f44a642..9442019bfa2 100644 --- a/Zend/zend_fibers.h +++ b/Zend/zend_fibers.h @@ -132,6 +132,10 @@ struct _zend_fiber { zval result; }; +ZEND_API zend_result zend_fiber_start(zend_fiber *fiber, zval *return_value); +ZEND_API void zend_fiber_resume(zend_fiber *fiber, zval *value, zval *return_value); +ZEND_API void zend_fiber_suspend(zend_fiber *fiber, zval *value, zval *return_value); + /* These functions may be used to create custom fiber objects using the bundled fiber switching context. */ ZEND_API zend_result zend_fiber_init_context(zend_fiber_context *context, void *kind, zend_fiber_coroutine coroutine, size_t stack_size); ZEND_API void zend_fiber_destroy_context(zend_fiber_context *context); diff --git a/Zend/zend_gc.c b/Zend/zend_gc.c index 3016ff8a1af..30314a1e48c 100644 --- a/Zend/zend_gc.c +++ b/Zend/zend_gc.c @@ -68,9 +68,14 @@ */ #include "zend.h" #include "zend_API.h" +#include "zend_compile.h" +#include "zend_errors.h" #include "zend_fibers.h" #include "zend_hrtime.h" +#include "zend_portability.h" +#include "zend_types.h" #include "zend_weakrefs.h" +#include "zend_string.h" #ifndef GC_BENCH # define GC_BENCH 0 @@ -265,6 +270,11 @@ typedef struct _zend_gc_globals { zend_hrtime_t dtor_time; zend_hrtime_t free_time; + uint32_t dtor_idx; /* root buffer index */ + uint32_t dtor_end; + zend_fiber *dtor_fiber; + bool dtor_fiber_running; + #if GC_BENCH uint32_t root_buf_length; uint32_t root_buf_peak; @@ -489,6 +499,11 @@ static void gc_globals_ctor_ex(zend_gc_globals *gc_globals) gc_globals->free_time = 0; gc_globals->activated_at = 0; + gc_globals->dtor_idx = GC_FIRST_ROOT; + gc_globals->dtor_end = 0; + gc_globals->dtor_fiber = NULL; + gc_globals->dtor_fiber_running = false; + #if GC_BENCH gc_globals->root_buf_length = 0; gc_globals->root_buf_peak = 0; @@ -532,6 +547,11 @@ void gc_reset(void) GC_G(dtor_time) = 0; GC_G(free_time) = 0; + GC_G(dtor_idx) = GC_FIRST_ROOT; + GC_G(dtor_end) = 0; + GC_G(dtor_fiber) = NULL; + GC_G(dtor_fiber_running) = false; + #if GC_BENCH GC_G(root_buf_length) = 0; GC_G(root_buf_peak) = 0; @@ -1776,6 +1796,120 @@ static void zend_get_gc_buffer_release(void); static void zend_gc_check_root_tmpvars(void); static void zend_gc_remove_root_tmpvars(void); +static zend_internal_function gc_destructor_fiber; + +static ZEND_COLD ZEND_NORETURN void gc_create_destructor_fiber_error(void) +{ + zend_error_noreturn(E_ERROR, "Unable to create destructor fiber"); +} + +static ZEND_COLD ZEND_NORETURN void gc_start_destructor_fiber_error(void) +{ + zend_error_noreturn(E_ERROR, "Unable to start destructor fiber"); +} + +static zend_always_inline zend_result gc_call_destructors(uint32_t idx, uint32_t end, zend_fiber *fiber) +{ + gc_root_buffer *current; + zend_refcounted *p; + + /* The root buffer might be reallocated during destructors calls, + * make sure to reload pointers as necessary. */ + while (idx != end) { + current = GC_IDX2PTR(idx); + if (GC_IS_DTOR_GARBAGE(current->ref)) { + p = GC_GET_PTR(current->ref); + /* Mark this is as a normal root for the next GC run */ + current->ref = p; + /* Double check that the destructor hasn't been called yet. It + * could have already been invoked indirectly by some other + * destructor. */ + if (!(OBJ_FLAGS(p) & IS_OBJ_DESTRUCTOR_CALLED)) { + if (fiber != NULL) { + GC_G(dtor_idx) = idx; + } + zend_object *obj = (zend_object*)p; + GC_TRACE_REF(obj, "calling destructor"); + GC_ADD_FLAGS(obj, IS_OBJ_DESTRUCTOR_CALLED); + GC_ADDREF(obj); + obj->handlers->dtor_obj(obj); + GC_TRACE_REF(obj, "returned from destructor"); + GC_DELREF(obj); + if (UNEXPECTED(fiber != NULL && GC_G(dtor_fiber) != fiber)) { + /* We resumed after suspension */ + gc_check_possible_root((zend_refcounted*)&obj->gc); + return FAILURE; + } + } + } + idx++; + } + + return SUCCESS; +} + +static zend_fiber *gc_create_destructor_fiber(void) +{ + zval zobj; + zend_fiber *fiber; + + GC_TRACE("starting destructor fiber"); + + if (UNEXPECTED(object_init_ex(&zobj, zend_ce_fiber) == FAILURE)) { + gc_create_destructor_fiber_error(); + } + + fiber = (zend_fiber *)Z_OBJ(zobj); + fiber->fci.size = sizeof(fiber->fci); + fiber->fci_cache.function_handler = (zend_function*) &gc_destructor_fiber; + + GC_G(dtor_fiber) = fiber; + + if (UNEXPECTED(zend_fiber_start(fiber, NULL) == FAILURE)) { + gc_start_destructor_fiber_error(); + } + + return fiber; +} + +static zend_never_inline void gc_call_destructors_in_fiber(uint32_t end) +{ + ZEND_ASSERT(!GC_G(dtor_fiber_running)); + + zend_fiber *fiber = GC_G(dtor_fiber); + + GC_G(dtor_idx) = GC_FIRST_ROOT; + GC_G(dtor_end) = GC_G(first_unused); + + if (UNEXPECTED(!fiber)) { + fiber = gc_create_destructor_fiber(); + } else { + zend_fiber_resume(fiber, NULL, NULL); + } + + for (;;) { + /* At this point, fiber has executed until suspension */ + GC_TRACE("resumed from destructor fiber"); + + if (UNEXPECTED(GC_G(dtor_fiber_running))) { + /* Fiber was suspended by a destructor. Start a new one for the + * remaining destructors. */ + GC_TRACE("destructor fiber suspended by destructor"); + GC_G(dtor_fiber) = NULL; + GC_G(dtor_idx)++; + /* We do not own the fiber anymore. It may be collected if the + * application does not reference it. */ + zend_object_release(&fiber->std); + fiber = gc_create_destructor_fiber(); + continue; + } else { + /* Fiber suspended itself after calling all destructors */ + GC_TRACE("destructor fiber suspended itself"); + break; + } + } +} + ZEND_API int zend_gc_collect_cycles(void) { int total_count = 0; @@ -1824,8 +1958,6 @@ rerun_gc: goto finish; } - zend_fiber_switch_block(); - end = GC_G(first_unused); if (gc_flags & GC_HAS_DESTRUCTORS) { @@ -1876,38 +2008,18 @@ rerun_gc: idx++; } - /* Actually call destructors. - * - * The root buffer might be reallocated during destructors calls, - * make sure to reload pointers as necessary. */ + /* Actually call destructors. */ zend_hrtime_t dtor_start_time = zend_hrtime(); - idx = GC_FIRST_ROOT; - while (idx != end) { - current = GC_IDX2PTR(idx); - if (GC_IS_DTOR_GARBAGE(current->ref)) { - p = GC_GET_PTR(current->ref); - /* Mark this is as a normal root for the next GC run, - * it's no longer garbage for this run. */ - current->ref = p; - /* Double check that the destructor hasn't been called yet. It could have - * already been invoked indirectly by some other destructor. */ - if (!(OBJ_FLAGS(p) & IS_OBJ_DESTRUCTOR_CALLED)) { - zend_object *obj = (zend_object*)p; - GC_TRACE_REF(obj, "calling destructor"); - GC_ADD_FLAGS(obj, IS_OBJ_DESTRUCTOR_CALLED); - GC_ADDREF(obj); - obj->handlers->dtor_obj(obj); - GC_DELREF(obj); - } - } - idx++; + if (EXPECTED(!EG(active_fiber))) { + gc_call_destructors(GC_FIRST_ROOT, end, NULL); + } else { + gc_call_destructors_in_fiber(end); } GC_G(dtor_time) += zend_hrtime() - dtor_start_time; if (GC_G(gc_protected)) { /* something went wrong */ zend_get_gc_buffer_release(); - zend_fiber_switch_unblock(); GC_G(collector_time) += zend_hrtime() - start_time; return 0; } @@ -1970,8 +2082,6 @@ rerun_gc: GC_G(free_time) += zend_hrtime() - free_start_time; - zend_fiber_switch_unblock(); - GC_TRACE("Collection finished"); GC_G(collected) += count; total_count += count; @@ -2125,3 +2235,49 @@ size_t zend_gc_globals_size(void) return sizeof(zend_gc_globals); } #endif + +static ZEND_FUNCTION(gc_destructor_fiber) +{ + uint32_t idx, end; + + zend_fiber *fiber = GC_G(dtor_fiber); + ZEND_ASSERT(fiber != NULL); + ZEND_ASSERT(fiber == EG(active_fiber)); + + for (;;) { + GC_G(dtor_fiber_running) = true; + + idx = GC_G(dtor_idx); + end = GC_G(dtor_end); + if (UNEXPECTED(gc_call_destructors(idx, end, fiber) == FAILURE)) { + /* We resumed after being suspended by a destructor */ + return; + } + + /* We have called all destructors. Suspend fiber until the next GC run + */ + GC_G(dtor_fiber_running) = false; + zend_fiber_suspend(fiber, NULL, NULL); + + if (UNEXPECTED(fiber->flags & ZEND_FIBER_FLAG_DESTROYED)) { + /* Fiber is being destroyed by shutdown sequence */ + GC_DELREF(&fiber->std); + gc_check_possible_root((zend_refcounted*)&fiber->std.gc); + return; + } + } +} + +static zend_internal_function gc_destructor_fiber = { + .type = ZEND_INTERNAL_FUNCTION, + .fn_flags = ZEND_ACC_PUBLIC, + .handler = ZEND_FN(gc_destructor_fiber), +}; + +void gc_init(void) +{ + gc_destructor_fiber.function_name = zend_string_init_interned( + "gc_destructor_fiber", + strlen("gc_destructor_fiber"), + true); +} diff --git a/Zend/zend_gc.h b/Zend/zend_gc.h index 84519aa68ca..a52de1bfcfa 100644 --- a/Zend/zend_gc.h +++ b/Zend/zend_gc.h @@ -65,6 +65,7 @@ ZEND_API int zend_gc_collect_cycles(void); ZEND_API void zend_gc_get_status(zend_gc_status *status); +void gc_init(void); void gc_globals_ctor(void); void gc_globals_dtor(void); void gc_reset(void); diff --git a/Zend/zend_objects_API.c b/Zend/zend_objects_API.c index 80f5b747db7..8a6b714c8b3 100644 --- a/Zend/zend_objects_API.c +++ b/Zend/zend_objects_API.c @@ -44,8 +44,6 @@ ZEND_API void ZEND_FASTCALL zend_objects_store_call_destructors(zend_objects_sto { EG(flags) |= EG_FLAGS_OBJECT_STORE_NO_REUSE; if (objects->top > 1) { - zend_fiber_switch_block(); - uint32_t i; for (i = 1; i < objects->top; i++) { zend_object *obj = objects->object_buckets[i]; @@ -62,8 +60,6 @@ ZEND_API void ZEND_FASTCALL zend_objects_store_call_destructors(zend_objects_sto } } } - - zend_fiber_switch_unblock(); } } @@ -179,11 +175,9 @@ ZEND_API void ZEND_FASTCALL zend_objects_store_del(zend_object *object) /* {{{ * if (object->handlers->dtor_obj != zend_objects_destroy_object || object->ce->destructor) { - zend_fiber_switch_block(); GC_SET_REFCOUNT(object, 1); object->handlers->dtor_obj(object); GC_DELREF(object); - zend_fiber_switch_unblock(); } }