Skip to content

Remove WeakMap entries whose key is only reachable through the entry value #10932

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 10 commits into from
Jul 16, 2023

Conversation

arnaud-lb
Copy link
Member

@arnaud-lb arnaud-lb commented Mar 25, 2023

Quoting the WeakMaps RFC:

Weak maps allow creating a map from objects to arbitrary values (similar to SplObjectStorage) without preventing the objects that are used as keys from being garbage collected. If an object key is garbage collected, it will simply be removed from the map.

Unfortunately, as described in #10043, keys that are referenced by a WeakMap value will never be automatically removed. This causes memory leaks, and can be difficult to avoid in a sufficiently complex application. Since the goal of WeakMaps is to avoid memory leaks and manual memory management, it would be beneficial to improve this.

This PR changes the behavior of WeakMaps such that if a key is only reachable through the corresponding value in a WeakMap, it's considered unreachable and the entry can be removed.

Consider the following example:

$map = new WeakMap();

$key = new stdClass;

$value = new stdClass;
$value->key = $key;

$map[$key] = $value;

unset($key, $value);

After the last statement, $key is only reachable through a WeakMap value, but $map[$key] is not automatically removed.

This PR updates the cycle collector to detect this, so that $key can be removed in this case. The exact changes are documented in a comment bellow: #10932 (comment).

Caveats and breaking changes

Predictability

Unreachable WeakMap entries are removed at an unpredictable time. This affects entries whose key is part of a garbage cycle (this is existing behavior), as well as entries concerned by this PR (this is new behavior, since these was not removed at all before).

Consider the following example:

$map = new WeakMap();
$key = new stdClass;
$map[$key] = $key;
unset($key);
  • Before this PR, $map[$key] is never removed automatically because the reference count of $key is non-zero, and the cycle is not detected by the GC
  • After this PR, $map[$key] is removed during the next GC run (at an unpredictable time)

Note that this behavior is not entirely new. In the example bellow, the behavior is the same before and after this PR: $map[$key] is removed at an unpredictable time, during the next GC run:

$map = new WeakMap();
$key = new stdClass;
$key->cycle = $key;
$map[$key] = 1;
unset($key);

Entries whose key is not part of a cycle are unaffected. In the example bellow, $map[$key] is removed immediately during the execution of the unset($key):

$map = new WeakMap();
$key = new stdClass;
$map[$key] = 1;
unset($key);
Iteration

All keys are in fact reachable by iterating a WeakMap. This PR considers that this reachability is weak, so that it does not retain keys that are only reachable through iteration.

Applications that iterate over WeakMap may break due to the changes in this PR, since keys that are only reachable like this may be removed from the map.

Removal during iteration

Currently, a WeakMap entry may be removed during iteration if one of the keys is collected. Due to existing iteration behaviors, this may cause some entries to be skipped:

$map = new WeakMap();
$key = new stdClass;
$key->cycle = $key;
$map[$key] = 1;
$key2 = new stdClass;
$map[$key2] = 1;

foreach ($map as $key => $value) {
    var_dump($key);
    unset($key);
    gc_collect_cycle(); // Collects $key. Due to existing iteration behaviors, foreach will skip the next entry
}

This is not new behavior, but this may now also happen when collecting a key that is only reachable through a WeakMap value:

$map = new WeakMap();
$key = new stdClass;
$map[$key] = $key;
$key2 = new stdClass;
$map[$key2] = 1;

foreach ($map as $key => $value) {
    var_dump($key);
    unset($key);
    gc_collect_cycle(); // Collects $key. Due to existing iteration behaviors, foreach will skip the next entry
}

Previous version of this PR

The original approach used in this PR had flaws, which have now be fixed. This description represents the current state of the PR.

@arnaud-lb arnaud-lb changed the title [wip] Remove WeakMap entries whose key is only reacheable through the entry value [wip] Remove WeakMap entries whose key is only racheable through the entry value Mar 31, 2023
@arnaud-lb arnaud-lb changed the title [wip] Remove WeakMap entries whose key is only racheable through the entry value Remove WeakMap entries whose key is only racheable through the entry value Mar 31, 2023
@arnaud-lb arnaud-lb marked this pull request as ready for review March 31, 2023 10:14
@arnaud-lb arnaud-lb changed the title Remove WeakMap entries whose key is only racheable through the entry value Remove WeakMap entries whose key is only reachable through the entry value Mar 31, 2023
@arnaud-lb
Copy link
Member Author

Ping @dstogov @iluuu1994

Copy link
Member

@dstogov dstogov left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The implementation looks strange to me, but it seems work.
I can't analyse all the possible consequences.
I think we may accept this and revert in case we get any related problems.

@arnaud-lb
Copy link
Member Author

Thank you for looking at this. I wasn't able to find papers or implementations of cycle GCs that solved this problem, so I've had to come up with this. It's possibly a bit strange, but maybe I can clarify it or change something?

@dstogov
Copy link
Member

dstogov commented May 22, 2023

Thank you for looking at this. I wasn't able to find papers or implementations of cycle GCs that solved this problem, so I've had to come up with this. It's possibly a bit strange, but maybe I can clarify it or change something?

The weird thing, that while mark-and-sweep GC stops tracing weak references our backup GC starts do this.

I think I understood your changes to original backup GC, because I knew the original algorithm well, and because I have to analyse only the added changes. Once this is committed, it's going to be hard to reconstruct the ideas behind this patch. It would be great to make a formal definition of the algorithm in some README or wiki.php.net page.

I tried to cheat the patch, but without success :)
@iluuu1994 could you also think about possible problems

@iluuu1994
Copy link
Member

iluuu1994 commented May 30, 2023

I'll look at this in detail today, which might take a while because I don't have any knowledge on the GC. However, note that this is a behavioral change because elements only referenced from WeakMap are now removed. However, you can currently still access these elements through iteration. https://3v4l.org/mG5L7 Since the GC runs non-deterministically this could also lead to race conditions. We should document this change accordingly.

@arnaud-lb
Copy link
Member Author

arnaud-lb commented Jun 16, 2023

It would be great to make a formal definition of the algorithm in some README or wiki.php.net page.

Agreed. I will write something after this is merged. In the meantime, here is a draft, as well as an attempt at a better explanation of what I did in this PR.

I will start by reminding us how the cycle GC works.

But before that, I think that some context helps to understand it:

  • The primary GC mechanism is refcounting, but this can not reclaim reference cycles
  • So we use a cycle collector, whose goal is to reclaim garbage cycles (cycles of values referencing each other, but not reachable from outside)
  • The cycle collector has similarities with a tracing GC, but it does not need to scan the full object graph. As a result, it is not fully redundant with refcounting, and also cheaper than a full tracing GC.

The cycle collector algorithm

The cycle collector algorithm is based on the synchronous variant described in "Concurrent Cycle Collection in Reference Counted Systems (David F. Bacon and V.T. Rajan)".

The paper makes these two fundamental observations:

The first observation is that garbage cycles can only be created when a reference count is decremented to a non-zero value [...]
The second observation is that in a garbage cycle, all the reference counts are internal; therefore, if those internal counts can be subtracted, the garbage cycle will be
discovered.

Together, these observations imply that we do not need to scan all nodes in order to find garbage cycles. We can discover a garbage cycle by only scanning the nodes reachable from any of its member. Furthermore, not all nodes are a possible member of a garbage cycle: Only the ones whose refcount was decremented to a non-zero value may be a member of a garbage cycle.

The discovery is done by trial deletion: If we subtract the reference counts contributed by internal references, the remaining nodes with a non-zero reference count are the only ones with an external reference. If we revert this process recursively from these nodes, the remaining nodes with a zero reference count are garbage.

Bellow is the pseudo code, with explanations.

Nodes are any refcounted value (objects, arrays, strings, ...). Buffer is a set of nodes. Nodes have a color, a refcount, and a gc status, denoted color(S), RC(S), and gc_status(S), respectively.

Colors have the following meaning:

  • GREY: Already scanned (in MarkRoots())
  • BLACK: Not garbage (after ScanRoots())
  • WHITE: Garbage (after ScanRoots())
  • PURPLE: Potential member of a garbage cycle
Decrement() and Increment()

These routines are used to decrement and increment the reference count of nodes during program execution. We know that a node whose reference count was decremented to a non-zero value is possibly a member of a garbage cycle. Such nodes are added to Buffer and their color is set to PURPLE. The algorithm will use these nodes as starting point.

Decrement(S):
  RC(S) = RC(S) - 1
  if RC(S) > 0:
    add S to Buffer
    color(S) = PURPLE

Increment(S):
  RC(S) = RC(S) + 1

This is implemented as zval_ptr_dtor() and Z_ADDREF().

CollectCycles()

CollectCycles() does the actual cycle collection. It is called when the size of Buffer reaches some threshold. At the beginning, Buffer contains at least one node per each possibly created garbage cycle since the last run. All nodes in Buffer are PURPLE, and other nodes are BLACK.

The pseudo code and explanations of subroutines are given bellow.

Note: I'm omitting destructors handling for brevity.

CollectCycles:
    MarkRoots()
    ScanRoots()
    CollectRoots()
    FreeGarbage()

This is implemented in zend_gc_collect_cycles().

MarkRoots()

MarkRoots() subtracts reference counts contributed by internal references. This is done by scanning the graph starting at all nodes in Buffer, and for each reference, decrementing the reference count of the target.

Before MarkRoots(), all nodes in Buffer are PURPLE

After MarkRoots(), all nodes reachable from Buffer are GREY. The ones with an external reference have a non-zero refcount, and others have a zero refcount:

With R the set of nodes reachable from Buffer:

  • Every node in R is GREY
  • Reference count of every node in R is decremented by one for each predecessor of the node in R

At this point, reference counts represent references external to R. Nodes with a non-zero reference count are referenced by a node outside of R.

MarkRoots():
  for S in Buffer:
    if color(S) == PURPLE:
      color(S) = GREY
      MarkGrey(S)

MarkGrey(S):
  for T in children(S):
    RC(T) = RC(T) - 1
    if color(T) != GREY:
      color(T) = GREY
      MarkGrey(T)

This is implemented in gc_mark_roots().

ScanRoots()

ScanRoots reverts the process of MarkRoots() for externally reachable nodes. This is done by scanning the graph starting at Buffer, marking all nodes GREY nodes as WHITE along the way. When a nodes with a non-zero refcount is encountered, it is scanned with ScanBlack(). ScanBlack() marks scanned nodes as BLACK, and restores refcounts.

After ScanRoots(), externally reachable nodes are BLACK (in-use). Other nodes are marked WHITE (garbage). The reference counts contributed by externally reachable nodes is re-added.

ScanRoots():
  for S in Buffer:
    if color(S) == GREY:
      color(S) = WHITE
      Scan(S)

Scan(S):
  if color(S) == WHITE
    if RC(S) > 0
      if color(S) != BLACK:
        color(S) = BLACK
        ScanBlack(S)
    else
      for T in children(S):
        if color(S) == GREY:
          color(S) = WHITE
          Scan(S)

ScanBlack(S):
  for T in children(S):
    RC(T) = RC(T) + 1
    if color(T) != BLACK:
      color(T) = BLACK
      ScanBlack(S)

This is implemented in gc_scan_roots().

CollectRoots()

CollectRoots() reverts the process of MarkRoots() for non-reachable nodes, and stages them for deletion.

After CollectRoots(), Buffer is a set of garbage nodes. Any reachable nodes have been removed from the set, and garbage ones have been added.

CollectRoots():
  for S in Buffer:
    if gc_status(S) == ROOT:
      if color(S) == BLACK:
        remove S from Buffer
       
  for S in Buffer:
    gc_status(S) = GARBAGE
    if color(S) == WHITE:
      color(S) = BLACK
      CollectWhite(S)

CollectWhite(S):
  if color(S) == BLACK:
    append S to Buffer
    gc_status(S) = BLACK
    
  for T in children(S):
    RC(T) = RC(T) + 1
    if color(T) == WHITE:
      color(T) = BLACK
      CollectWhite(S) 

This is implemented in gc_collect_roots().

FreeGarbage

In FreeGarbage(), we can free all nodes in Buffer. Freeing a node may remove its successors from Buffer, or add new possible roots. Therefor, we have to check gc_status(S).

FreeGarbage(S):
  for S in Buffer:
    if gc_status(S) == GARBAGE:
      free_node(s)
  for S in Buffer:
    if gc_status(S) == GARBAGE:
      free(s)
Example run

Bellow is an example run with the following initial state:

image

After MarkRoots():

image

We can see that node D has an external reference. D and E are externally reachable.

After ScanRoots():

image

The refcount of E has been restored. The refcount of D will be restored by CollectWhite().

After CollectWhite():

image

All reference counts have been restored. Not shown on the visualisation is that A, B, C are in Buffer and have a gc_status of GARBAGE.

During FreeGarbage, A is freed first, which frees and removes B and C from Buffer.

WeakMaps

Quoting the WeakMaps RFC:

Weak maps allow creating a map from objects to arbitrary values (similar to SplObjectStorage) without preventing the objects that are used as keys from being garbage collected. If an object key is garbage collected, it will simply be removed from the map.

The implementation works as follows:

  • WeakMaps do not contribute to the refcount of their keys: When an object is used as key in a WeakMap, its refcount is not incremented so its not retained by the WeakMap.
  • When the refcount of an object is decremented to zero, WeakMaps in which the object is used as key are notified and the corresponding entries are removed.

This achieves the goal of a WeakMap: We can map objects to arbitrary values without preventing the objects from being collected, and if an object is collected, the corresponding value is removed from the map.

However, if an object used as key is referenced from the value, it will never be automatically collected because its reference count will never reach zero.

This PR tries to improve that.

Consider a WeakMap initialized like this:

<?php

$m = new WeakMap();

$k = new stdClass;

$v = new stdClass;
$v->k = $k;

$m[$k] = $v;

unset($k, $v);

If we represent the graph of references of this program as seen by the cycle collector after unset(), we can see that M retains V, which in turns retains K:

Initialimage After MarkRoots()image After ScanRoots()image

MarkRoots() discovers that M has external references, which makes V and K externally reachable. ScanRoots() will revert their refcount to 1, and nothing will be collected.

To allow K and V to be collected, we have to recognize that deleting either of M or K would decrement the refcount of V:

  • Deleting M would decrement the refcount of all its values, including V
  • Deleting K would remove the entry pointing to V, thus decrementing its refcount

Both M and K contribute to the refcount of V.

If we recognize this, we can see that V and K form a cycle:

image

However, while deleting either of M or K will decrement the refcount of V by 1, deleting both will not decrement it by 2. Therefore we have to only consider the contribution of either M or V to the refcount of K, but not both at the same time.

Based on that, we can adjust the algorithm as follows:

  • In MarkRoots(), when scanning any node K, we should scan M(K) for every weakmap M in which K is used as a key. It should also decrement the refcount of M(K), but only if M(K) was not reached from M already. When scanning the values of a weakmap M, the refcount of every value V should be decremented only if it was not already reached from M(K).
  • ScanRoots() should mark a value V of a weakmap M as BLACK only if both M and the corresponding key K are BLACK. Otherwise, V should be marked WHITE.

Bellow is the adjusted pseudo code, with explanations.

The set of weakmaps in which a node is used as key is denoted weakmaps(S). We add two flags, from_key and from_weakmap on weakmap entries, meaning that the entry is was reached from Buffer by following the virtual reference from a weakmap key, or from a weakmap, respectively. These are denoted from_key(weakmap, key) and from_weakmap(weakmap, key).

MarkRoots()

As before, MarkRoots() cancels the contribution of internal references to refcounts.

We follow the reference from a weakmap to its values (as before) as well as the virtual reference from a node to the values mapped to it through all weakmaps. We decrement the refcount only once if we follow both references.

MarkRoots():
  for S in Buffer:
    if color(S) == PURPLE:
      color(S) = GREY
      MarkGrey(S)

MarkGrey(S):
  for M in weakmaps(S):
    from_key(M, S) = True
    V = M(S) 
    if not from_weakmap(M, S):
      RC(V) = RC(V) - 1
    if color(V) != GREY:
        color(V) = GREY
        MarkGrey(V)

  if S is a weakmap:
    for K in keys(S):
      from_weakmap(M, S) = True
      V = M(K)
      if not from_key(M, S):
        RC(V) = RC(V) - 1
        if color(V) != GREY:
          color(V) = GREY
          MarkGrey(V)

  for T in children(S):
    RC(T) = RC(T) - 1
    if color(T) != GREY:
      color(T) = GREY
      MarkGrey(T)

Implementation details:

  • The weakrefs component maintains a hashtable from objects to weakmaps. As a result, the implementation of weakmaps(S) is efficient
  • Objects used as weakmap key are flagged with IS_OBJ_WEAKLY_REFERENCED, so we can check this flag before calling weakmaps(S)
  • The flags from_key and from_weakmap are stored the in zval backing the entry in the WeakMap hashtable (in the u1.v.extra field)
  • We use custom get_gc functions for different reasons:
    • The get_gc function must return a pointer the zval backing the entry, and not a copy, so that we can set the flags
    • For effiency, we need weakmaps(S) to return pairs of weakmaps and corresponding values, so that we do not have to lookup M(S) for each weakmap
ScanRoots()

As before, ScanRoots() restores the refcounts of reachable nodes and marks them BLACK. Other nodes are marked WHITE.

In the subroutine ScanBlack(), we must mark a weakmap M value V as BLACK only if both M and the corresponding key K are BLACK. Also, we must increment the refcount of V only once if we reach V both from M and M(K).

We achieve this with the help of the from_key and from_weakmap flags:

  • When scanning every value V of a weakmap M, we remove the from_weakmap flag
  • When scanning a node K, we remove the from_key flag from M(K)
  • In both cases, if after removing the flag, none of from_key or from_weakmap is set, it implies that:
    • Both V and K are BLACK (we have either removed the flag in ScanBlack, or the node was not reached in MarkRoots, and therefore is BLACK)
    • We are scanning either V or K, and the other one has already been scanned or will never be scanned
    • Therefore, we can scan V or M(K), and increment its refcount

If either of the from_key or from_weakmap flags are set, we mark the node WHITE and Scan() it.

ScanRoots():
  for S in Buffer:
    if color(S) == GREY:
      color(S) = WHITE
      Scan(S)

Scan(S):
  if color(S) == WHITE
    if RC(S) > 0
      if color(S) != BLACK:
        color(S) = BLACK
        ScanBlack(S)
    else
      for T in children(S):
        if color(S) == GREY:
          color(S) = WHITE
          Scan(S)

ScanBlack(S):
  for M in weakmaps(S):
    from_key(M, S) = False
    V = M(S) 
    if not from_weakmap(M, S):
      RC(V) = RC(V) + 1
      if color(V) != BLACK:
        color(V) = BLACK
        ScanBlack(V)
    else:
      if color(V) != WHITE:
        color(V) = WHITE
        Scan(V)

  if S is a weakmap:
    for K in keys(S):
      from_weakmap(M, K) = False
      V = M(K)
      if color(K) == GREY and S not in Buffer:
        add K to Buffer
        gc_status(K) = ROOT
      if not from_key(M, K):
        RC(V) = RC(V) + 1
        if color(V) != BLACK:
          color(V) = BLACK
          ScanBlack(V)
      else:
        if color(V) != WHITE:
          color(V) = WHITE
          Scan(V)

  for T in children(S):
    RC(T) = RC(T) + 1
    if color(T) != BLACK:
      color(T) = BLACK
      ScanBlack(S)

Implementation details:

The implementation is non-recusive, so we can not just call Scan(V) in ScanBlack().

Instead, we could add V to Buffer so that ScanRoots() eventually scans it. However, in the worse case we will add all weakmap values to the Buffer.

We can reduce the number of nodes added to Buffer by adding the key or the weakmap instead, and only if they are GREY.

Here is the reasoning:

Given any weakmap M and key K:

  • M or K being non-GREY imply that we do not need to add V to Buffer, because it was already scanned or should not be scanned
  • Adding M to Buffer will cause values(M) to be scanned. Therefore, it achieves the same goal as adding values(M) to Buffer
  • Adding K to Buffer will cause M(K) to be scanned. Therefore, it achieves the same goal as adding M(K) to Buffer
  • M being GREY in ScanBlack implies that it was scanned in MarkRoots, and that it must be scanned in ScanRoots. Therefore, adding M to Buffer does not cause extra nodes to be scanned
  • The same applies for K.

Note: We could decide when to increment the refcount of V by looking at the color or M and K only (without using the flags), but not in the non-recursive implementation:

  • Due to the non-recursive nature of the implementation, colors are assigned before actually scanning a node, so both a weakmap and key may be BLACK at the same time when scanning any of them
  • We may scan the weakmap and the key in any order
  • We may scan only one of the key and the weakmap
CollectRootS()

As before, CollectRoots() restores the refcount of unreachable nodes, and marks them BLACK.

CollectRoots() scans a weakmap value if any of its weakmap and key are scanned, but increments its refcount only once. This is done when either from_weakmap or from_key is set. We reset both flags are reset in this case.

Conveniently, after ScanRoots() and CollectWhite() all from_weakmap and from_key flags have been reset.

CollectRoots():
  for S in Buffer:
    if gc_status(S) == ROOT:
      if color(S) == BLACK:
        remove S from Buffer
       
  for S in Buffer:
    gc_status(S) = GARBAGE
    if color(S) == WHITE:
      color(S) = BLACK
      CollectWhite(S)

CollectWhite(S):
  for M in weakmaps(S):
    if from_key(M, S):
      from_key(M, S) = False
      from_weakmap(M, S) = False
      V = M(S)
      RC(V) = RC(V) + 1
      if color(V) == WHITE:
        color(V) = BLACK
        ColletWhite(V)

  if S is a weakmap:
    for K in keys(S):
      if from_weakmap(M, K):
        from_key(M, K) = False
        from_weakmap(M, K) = False
        V = M(K)
        RC(V) = RC(V) + 1
        if color(V) == WHITE:
          color(V) = BLACK
          ColletWhite(V)
    
  for T in children(S):
    RC(T) = RC(T) + 1
    if color(T) == WHITE:
      color(T) = BLACK
      CollectWhite(S) 
Complexity

The algorithm has a complexity of O(N+E), with N the number of nodes and E the number of references. This PR increases N by the number of nodes reachable from WeakMap values whose corresponding key is reached during scanning (if a node is used as key in a WeakMap, and this node is reached during scanning, we scan the corresponding value, so N is increased by the number of nodes reachable from the value), and E by the number of references from these nodes.

Prior work

I was not able to find prior work solving this problem with reference counting GCs.

"Ephemerons: a new finalization mechanism" (Barry Hayes) solves it with a tracing GC. Ephemerons are an abstraction for a WeakMap entry.

"Eliminating Cycles in Weak Tables (Alexandra Barros, Roberto Ierusalimschy)" builds upon this in the context of Lua, with an other flavor of a tracing GC.

In the ephemerons paper, the problem is solved by adding an additional tracing phase:

When an ephemeron is encountered during first phase of a trace, the collector does not immediately trace either of the key or the value, but puts it on a queue.

In a second phase, the value of queued ephemerons whose key was reached are scanned. This may reach the key of other ephemerons, so the queued ephemerons are scanned again until the queue contains only ephemerons whose key has not been reached.

Whilst it seems odd at first to scan WeakMap values, it's actually necessary in all tracing flavors: A full tracing GC needs to scan values whose key is reachable, and a cycle collector needs to scan values whose key may be unreachable.

Edit: My original approach was flawed, as it was dependent on the traversing order. This flaw has been fixed and this comment reflects the current state of the PR.

@dstogov
Copy link
Member

dstogov commented Jun 19, 2023

This works as intended as long as the WeakMap value is not reachable from the key.

Values from the keys, or keys from the values?

@arnaud-lb
Copy link
Member Author

Key from the value, thank you. I'll update the comment

The normal code path will fetch `obj->handlers->get_gc`, so it's much cheaper to
identify weakmaps by looking at `obj->handlers->get_gc`.
* up/master: (571 commits)
  Expose time spent collecting cycles in gc_status() (php#11523)
  Warn when fpm socket was not registered on the expected path
  Implement DOMElement::id
  Fix ?
  Implement DOMParentNode::replaceChildren()
  Implement DOMElement::className
  RFC: Deprecate remains of string evaluated code assertions (php#11671)
  Prevent decimal int precision loss in number_format()
  Implement DOMNode::getRootNode()
  Implement DOMElement::getAttributeNames()
  Refactor dom_node_node_name_read() to avoid double allocation
  Handle fragments consisting out of multiple children without a single root correctly
  Avoid allocations in DOMElement::getAttribute()
  Avoid string allocation in dom_get_dom1_attribute() for as long as possible
  Fix use-of-uninitialized-value when calling php_posix_stream_get_fd (php#11694)
  Reorder list construction in the function php_intpow10 (php#11683)
  proc_open: Use posix_spawn(3) interface on systems where it is profitable
  zend_gdb disable gdb detection for FreeBSD < 11.
  Fix iface const visibility variance check
  Fix missing iface class const inheritance type check
  ...
@arnaud-lb arnaud-lb merged commit cbf67e4 into php:master Jul 16, 2023
iluuu1994 added a commit to iluuu1994/php-src that referenced this pull request Dec 16, 2024
* Actually skip non-cyclic objects in GC.
* Add MAY_BE_CYCLIC to more internal classes
* Fix dynamic property creation trigger

And also breaking phpGH-10932 for now. :) I will try to fix this later.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants