bug #23644 [VarDumper] Dont use Stub objects for arrays - lower GC pressure (nicolas-grekas)

This PR was merged into the 3.3 branch.

Discussion
----------

[VarDumper] Dont use Stub objects for arrays - lower GC pressure

| Q             | A
| ------------- | ---
| Branch?       | 3.3
| Bug fix?      | yes
| New feature?  | no
| BC breaks?    | no
| Deprecations? | no
| Tests pass?   | yes
| Fixed tickets | #23644
| License       | MIT
| Doc PR        | -

Several recent profiles have shown that VarDumper triggers the garbage collector quite often, leading to high CPU usage. The reason for this is that internally, VarCloner creates one Stub object per array+object+resource.

This PR removes the need for Stub objects for each arrays, replacing them with stub arrays. This should almost remove the GC pressure, since the number of Stub objects now has the same magnitude than the number of dumped objects.

Meanwhile, this PR removes any use of the `symfony_debug` extension, which is mostly useless anyway. This helps make the code simpler (really :) ), thus helps maintenance (eg merging up to master.)

I also changed the values of the constants defined in the Stub class, and removed the corresponding Data::mapStubConsts() method. Since the serialized format has changed (and we have to do it as there is no other way to fix this GC issue), there is no need to keep any sort of compat mapping there.

Commits
-------

0d5012d20e [VarDumper] Dont use Stub objects for arrays
This commit is contained in:
Fabien Potencier 2017-07-26 09:04:37 +02:00
commit 21d98d43c4
5 changed files with 162 additions and 241 deletions

View File

@ -213,7 +213,7 @@ abstract class AbstractCloner implements ClonerInterface
gc_disable();
}
try {
$data = $this->doClone($var);
return new Data($this->doClone($var));
} finally {
if ($gc) {
gc_enable();
@ -221,8 +221,6 @@ abstract class AbstractCloner implements ClonerInterface
restore_error_handler();
$this->prevErrorHandler = null;
}
return new Data($data);
}
/**

View File

@ -16,7 +16,7 @@ use Symfony\Component\VarDumper\Caster\Caster;
/**
* @author Nicolas Grekas <p@tchwork.com>
*/
class Data implements \ArrayAccess, \Countable, \IteratorAggregate, \Serializable
class Data implements \ArrayAccess, \Countable, \IteratorAggregate
{
private $data;
private $position = 0;
@ -72,7 +72,7 @@ class Data implements \ArrayAccess, \Countable, \IteratorAggregate, \Serializabl
if ($item instanceof Stub && Stub::TYPE_REF === $item->type && !$item->position) {
$item = $item->value;
}
if (!$item instanceof Stub) {
if (!($item = $this->getStub($item)) instanceof Stub) {
return $item;
}
if (Stub::TYPE_STRING === $item->type) {
@ -82,7 +82,7 @@ class Data implements \ArrayAccess, \Countable, \IteratorAggregate, \Serializabl
$children = $item->position ? $this->data[$item->position] : array();
foreach ($children as $k => $v) {
if ($recursive && !$v instanceof Stub) {
if ($recursive && !($v = $this->getStub($v)) instanceof Stub) {
continue;
}
$children[$k] = clone $this;
@ -90,12 +90,12 @@ class Data implements \ArrayAccess, \Countable, \IteratorAggregate, \Serializabl
$children[$k]->position = $item->position;
if ($recursive) {
if ($v instanceof Stub && Stub::TYPE_REF === $v->type && $v->value instanceof Stub) {
if (Stub::TYPE_REF === $v->type && ($v = $this->getStub($v->value)) instanceof Stub) {
$recursive = (array) $recursive;
if (isset($recursive[$v->value->position])) {
if (isset($recursive[$v->position])) {
continue;
}
$recursive[$v->value->position] = true;
$recursive[$v->position] = true;
}
$children[$k] = $children[$k]->getValue($recursive);
}
@ -123,7 +123,7 @@ class Data implements \ArrayAccess, \Countable, \IteratorAggregate, \Serializabl
public function __get($key)
{
if (null !== $data = $this->seek($key)) {
$item = $data->data[$data->position][$data->key];
$item = $this->getStub($data->data[$data->position][$data->key]);
return $item instanceof Stub || array() === $item ? $data : $item;
}
@ -236,7 +236,7 @@ class Data implements \ArrayAccess, \Countable, \IteratorAggregate, \Serializabl
if ($item instanceof Stub && Stub::TYPE_REF === $item->type && !$item->position) {
$item = $item->value;
}
if (!$item instanceof Stub || !$item->position) {
if (!($item = $this->getStub($item)) instanceof Stub || !$item->position) {
return;
}
$keys = array($key);
@ -278,57 +278,6 @@ class Data implements \ArrayAccess, \Countable, \IteratorAggregate, \Serializabl
$this->dumpItem($dumper, new Cursor(), $refs, $this->data[$this->position][$this->key]);
}
/**
* @internal
*/
public function serialize()
{
$data = $this->data;
foreach ($data as $i => $values) {
foreach ($values as $k => $v) {
if ($v instanceof Stub) {
if (Stub::TYPE_ARRAY === $v->type) {
$v = self::mapStubConsts($v, false);
$data[$i][$k] = array($v->class, $v->position, $v->cut);
} else {
$v = self::mapStubConsts($v, false);
$data[$i][$k] = array($v->class, $v->position, $v->cut, $v->type, $v->value, $v->handle, $v->refCount, $v->attr);
}
}
}
}
return serialize(array($data, $this->position, $this->key, $this->maxDepth, $this->maxItemsPerDepth, $this->useRefHandles));
}
/**
* @internal
*/
public function unserialize($serialized)
{
list($data, $this->position, $this->key, $this->maxDepth, $this->maxItemsPerDepth, $this->useRefHandles) = unserialize($serialized);
foreach ($data as $i => $values) {
foreach ($values as $k => $v) {
if ($v && is_array($v)) {
$s = new Stub();
if (3 === count($v)) {
$s->type = Stub::TYPE_ARRAY;
$s = self::mapStubConsts($s, false);
list($s->class, $s->position, $s->cut) = $v;
$s->value = $s->cut + count($data[$s->position]);
} else {
list($s->class, $s->position, $s->cut, $s->type, $s->value, $s->handle, $s->refCount, $s->attr) = $v;
}
$data[$i][$k] = self::mapStubConsts($s, true);
}
}
}
$this->data = $data;
}
/**
* Depth-first dumping of items.
*
@ -346,7 +295,10 @@ class Data implements \ArrayAccess, \Countable, \IteratorAggregate, \Serializabl
if (!$item instanceof Stub) {
$cursor->attr = array();
$type = gettype($item);
$type = \gettype($item);
if ($item && 'array' === $type) {
$item = $this->getStub($item);
}
} elseif (Stub::TYPE_REF === $item->type) {
if ($item->handle) {
if (!isset($refs[$r = $item->handle - (PHP_INT_MAX >> 1)])) {
@ -360,7 +312,7 @@ class Data implements \ArrayAccess, \Countable, \IteratorAggregate, \Serializabl
}
$cursor->attr = $item->attr;
$type = $item->class ?: gettype($item->value);
$item = $item->value;
$item = $this->getStub($item->value);
}
if ($item instanceof Stub) {
if ($item->refCount) {
@ -458,21 +410,20 @@ class Data implements \ArrayAccess, \Countable, \IteratorAggregate, \Serializabl
return $hashCut;
}
private static function mapStubConsts(Stub $stub, $resolve)
private function getStub($item)
{
static $stubConstIndexes, $stubConstValues;
if (null === $stubConstIndexes) {
$r = new \ReflectionClass(Stub::class);
$stubConstIndexes = array_flip(array_values($r->getConstants()));
$stubConstValues = array_flip($stubConstIndexes);
if (!$item || !\is_array($item)) {
return $item;
}
$map = $resolve ? $stubConstValues : $stubConstIndexes;
$stub = clone $stub;
$stub->type = $map[$stub->type];
$stub->class = isset($map[$stub->class]) ? $map[$stub->class] : $stub->class;
$stub = new Stub();
$stub->type = Stub::TYPE_ARRAY;
foreach ($item as $stub->class => $stub->position) {
}
if (isset($item[0])) {
$stub->cut = $item[0];
}
$stub->value = $stub->cut + \count($this->data[$stub->position]);
return $stub;
}

View File

@ -16,19 +16,19 @@ namespace Symfony\Component\VarDumper\Cloner;
*
* @author Nicolas Grekas <p@tchwork.com>
*/
class Stub
class Stub implements \Serializable
{
const TYPE_REF = 'ref';
const TYPE_STRING = 'string';
const TYPE_ARRAY = 'array';
const TYPE_OBJECT = 'object';
const TYPE_RESOURCE = 'resource';
const TYPE_REF = 1;
const TYPE_STRING = 2;
const TYPE_ARRAY = 3;
const TYPE_OBJECT = 4;
const TYPE_RESOURCE = 5;
const STRING_BINARY = 'bin';
const STRING_UTF8 = 'utf8';
const STRING_BINARY = 1;
const STRING_UTF8 = 2;
const ARRAY_ASSOC = 'assoc';
const ARRAY_INDEXED = 'indexed';
const ARRAY_ASSOC = 1;
const ARRAY_INDEXED = 2;
public $type = self::TYPE_REF;
public $class = '';
@ -38,4 +38,20 @@ class Stub
public $refCount = 0;
public $position = 0;
public $attr = array();
/**
* @internal
*/
public function serialize()
{
return \serialize(array($this->class, $this->position, $this->cut, $this->type, $this->value, $this->handle, $this->refCount, $this->attr));
}
/**
* @internal
*/
public function unserialize($serialized)
{
list($this->class, $this->position, $this->cut, $this->type, $this->value, $this->handle, $this->refCount, $this->attr) = \unserialize($serialized);
}
}

View File

@ -24,12 +24,11 @@ class VarCloner extends AbstractCloner
*/
protected function doClone($var)
{
$useExt = $this->useExt;
$len = 1; // Length of $queue
$pos = 0; // Number of cloned items past the first level
$refsCounter = 0; // Hard references counter
$queue = array(array($var)); // This breadth-first queue is the return value
$arrayRefs = array(); // Map of queue indexes to stub array objects
$indexedArrays = array(); // Map of queue indexes that hold numerically indexed arrays
$hardRefs = array(); // Map of original zval hashes to stub objects
$objRefs = array(); // Map of original object handles to their stub object couterpart
$resRefs = array(); // Map of original resource handles to their stub object couterpart
@ -41,92 +40,99 @@ class VarCloner extends AbstractCloner
$a = null; // Array cast for nested structures
$stub = null; // Stub capturing the main properties of an original item value
// or null if the original value is used directly
$zval = array( // Main properties of the current value
'type' => null,
'zval_isref' => null,
'zval_hash' => null,
'array_count' => null,
'object_class' => null,
'object_handle' => null,
'resource_type' => null,
);
if (!self::$hashMask) {
self::initHashMask();
}
$hashMask = self::$hashMask;
$hashOffset = self::$hashOffset;
$arrayStub = new Stub();
$arrayStub->type = Stub::TYPE_ARRAY;
$fromObjCast = false;
for ($i = 0; $i < $len; ++$i) {
$indexed = true; // Whether the currently iterated array is numerically indexed or not
$j = -1; // Position in the currently iterated array
$fromObjCast = array_keys($queue[$i]);
$fromObjCast = array_keys(array_flip($fromObjCast)) !== $fromObjCast;
$refs = $vals = $fromObjCast ? array_values($queue[$i]) : $queue[$i];
foreach ($queue[$i] as $k => $v) {
// $k is the original key
// $v is the original value or a stub object in case of hard references
if ($k !== ++$j) {
$indexed = false;
}
if ($fromObjCast) {
$k = $j;
}
if ($useExt) {
$zval = symfony_zval_info($k, $refs);
} else {
$refs[$k] = $cookie;
if ($zval['zval_isref'] = $vals[$k] === $cookie) {
$zval['zval_hash'] = $v instanceof Stub ? spl_object_hash($v) : null;
$refs = $vals = $queue[$i];
if (\PHP_VERSION_ID < 70200 && empty($indexedArrays[$i])) {
// see https://wiki.php.net/rfc/convert_numeric_keys_in_object_array_casts
foreach ($vals as $k => $v) {
if (\is_int($k)) {
continue;
}
foreach (array($k => true) as $j => $v) {
}
if ($k !== $j) {
$fromObjCast = true;
$refs = $vals = \array_values($queue[$i]);
break;
}
$zval['type'] = gettype($v);
}
if ($zval['zval_isref']) {
}
foreach ($vals as $k => $v) {
// $v is the original value or a stub object in case of hard references
$refs[$k] = $cookie;
if ($zvalIsRef = $vals[$k] === $cookie) {
$vals[$k] = &$stub; // Break hard references to make $queue completely
unset($stub); // independent from the original structure
if (isset($hardRefs[$zval['zval_hash']])) {
$vals[$k] = $useExt ? ($v = $hardRefs[$zval['zval_hash']]) : ($refs[$k] = $v);
if ($v instanceof Stub && isset($hardRefs[\spl_object_hash($v)])) {
$vals[$k] = $refs[$k] = $v;
if ($v->value instanceof Stub && (Stub::TYPE_OBJECT === $v->value->type || Stub::TYPE_RESOURCE === $v->value->type)) {
++$v->value->refCount;
}
++$v->refCount;
continue;
}
$refs[$k] = $vals[$k] = new Stub();
$refs[$k]->value = $v;
$h = \spl_object_hash($refs[$k]);
$hardRefs[$h] = &$refs[$k];
$values[$h] = $v;
$vals[$k]->handle = ++$refsCounter;
}
// Create $stub when the original value $v can not be used directly
// If $v is a nested structure, put that structure in array $a
switch ($zval['type']) {
case 'string':
if (isset($v[0]) && !preg_match('//u', $v)) {
switch (true) {
case empty($v):
case true === $v:
case \is_int($v):
case \is_float($v):
break;
case \is_string($v):
if (!\preg_match('//u', $v)) {
$stub = new Stub();
$stub->type = Stub::TYPE_STRING;
$stub->class = Stub::STRING_BINARY;
if (0 <= $maxString && 0 < $cut = strlen($v) - $maxString) {
if (0 <= $maxString && 0 < $cut = \strlen($v) - $maxString) {
$stub->cut = $cut;
$stub->value = substr($v, 0, -$cut);
$stub->value = \substr($v, 0, -$cut);
} else {
$stub->value = $v;
}
} elseif (0 <= $maxString && isset($v[1 + ($maxString >> 2)]) && 0 < $cut = mb_strlen($v, 'UTF-8') - $maxString) {
} elseif (0 <= $maxString && isset($v[1 + ($maxString >> 2)]) && 0 < $cut = \mb_strlen($v, 'UTF-8') - $maxString) {
$stub = new Stub();
$stub->type = Stub::TYPE_STRING;
$stub->class = Stub::STRING_UTF8;
$stub->cut = $cut;
$stub->value = mb_substr($v, 0, $maxString, 'UTF-8');
$stub->value = \mb_substr($v, 0, $maxString, 'UTF-8');
}
break;
case 'integer':
break;
case \is_array($v):
$stub = $arrayStub;
$stub->class = Stub::ARRAY_INDEXED;
case 'array':
if ($v) {
$stub = $arrayRefs[$len] = new Stub();
$stub->type = Stub::TYPE_ARRAY;
$stub->class = Stub::ARRAY_ASSOC;
$j = -1;
foreach ($v as $gk => $gv) {
if ($gk !== ++$j) {
$stub->class = Stub::ARRAY_ASSOC;
break;
}
}
$a = $v;
if (Stub::ARRAY_ASSOC === $stub->class) {
// Copies of $GLOBALS have very strange behavior,
// let's detect them with some black magic
$a = $v;
$a[$gid] = true;
// Happens with copies of $GLOBALS
@ -136,19 +142,23 @@ class VarCloner extends AbstractCloner
foreach ($v as $gk => &$gv) {
$a[$gk] = &$gv;
}
unset($gv);
} else {
$a = $v;
}
$stub->value = $zval['array_count'] ?: count($a);
} else {
$indexedArrays[$len] = true;
}
$stub->value = \count($a);
break;
case 'object':
if (empty($objRefs[$h = $zval['object_handle'] ?: ($hashMask ^ hexdec(substr(spl_object_hash($v), $hashOffset, PHP_INT_SIZE)))])) {
case \is_object($v):
case $v instanceof \__PHP_Incomplete_Class:
if (empty($objRefs[$h = $hashMask ^ \hexdec(\substr(\spl_object_hash($v), $hashOffset, \PHP_INT_SIZE))])) {
$stub = new Stub();
$stub->type = Stub::TYPE_OBJECT;
$stub->class = $zval['object_class'] ?: get_class($v);
$stub->class = \get_class($v);
$stub->value = $v;
$stub->handle = $h;
$a = $this->castObject($stub, 0 < $i);
@ -156,18 +166,12 @@ class VarCloner extends AbstractCloner
if (Stub::TYPE_OBJECT !== $stub->type || null === $stub->value) {
break;
}
if ($useExt) {
$zval['type'] = $stub->value;
$zval = symfony_zval_info('type', $zval);
$h = $zval['object_handle'];
} else {
$h = $hashMask ^ hexdec(substr(spl_object_hash($stub->value), $hashOffset, PHP_INT_SIZE));
}
$h = $hashMask ^ \hexdec(\substr(\spl_object_hash($stub->value), $hashOffset, \PHP_INT_SIZE));
$stub->handle = $h;
}
$stub->value = null;
if (0 <= $maxItems && $maxItems <= $pos) {
$stub->cut = count($a);
$stub->cut = \count($a);
$a = null;
}
}
@ -180,13 +184,11 @@ class VarCloner extends AbstractCloner
}
break;
case 'resource':
case 'unknown type':
case 'resource (closed)':
default: // resource
if (empty($resRefs[$h = (int) $v])) {
$stub = new Stub();
$stub->type = Stub::TYPE_RESOURCE;
if ('Unknown' === $stub->class = $zval['resource_type'] ?: @get_resource_type($v)) {
if ('Unknown' === $stub->class = @\get_resource_type($v)) {
$stub->class = 'Closed';
}
$stub->value = $v;
@ -194,7 +196,7 @@ class VarCloner extends AbstractCloner
$a = $this->castResource($stub, 0 < $i);
$stub->value = null;
if (0 <= $maxItems && $maxItems <= $pos) {
$stub->cut = count($a);
$stub->cut = \count($a);
$a = null;
}
}
@ -209,66 +211,51 @@ class VarCloner extends AbstractCloner
}
if (isset($stub)) {
if ($zval['zval_isref']) {
if ($useExt) {
$vals[$k] = $hardRefs[$zval['zval_hash']] = $v = new Stub();
$v->value = $stub;
} else {
$refs[$k] = new Stub();
$refs[$k]->value = $stub;
$h = spl_object_hash($refs[$k]);
$vals[$k] = $hardRefs[$h] = &$refs[$k];
$values[$h] = $v;
if ($a) {
if (!$i || 0 > $maxItems) {
$queue[$len] = $a;
$stub->position = $len++;
} elseif ($pos < $maxItems) {
if ($maxItems < $pos += \count($a)) {
$a = \array_slice($a, 0, $maxItems - $pos);
if ($stub->cut >= 0) {
$stub->cut += $pos - $maxItems;
}
}
$queue[$len] = $a;
$stub->position = $len++;
} elseif ($stub->cut >= 0) {
$stub->cut += \count($a);
$stub->position = 0;
}
$vals[$k]->handle = ++$refsCounter;
}
if ($arrayStub === $stub) {
if ($arrayStub->cut) {
$stub = array($arrayStub->cut, $arrayStub->class => $arrayStub->position);
$arrayStub->cut = 0;
} else {
$stub = array($arrayStub->class => $arrayStub->position);
}
}
if ($zvalIsRef) {
$refs[$k]->value = $stub;
} else {
$vals[$k] = $stub;
}
if ($a) {
if ($i && 0 <= $maxItems) {
$k = count($a);
if ($pos < $maxItems) {
if ($maxItems < $pos += $k) {
$a = array_slice($a, 0, $maxItems - $pos);
if ($stub->cut >= 0) {
$stub->cut += $pos - $maxItems;
}
}
} else {
if ($stub->cut >= 0) {
$stub->cut += $k;
}
$stub = $a = null;
unset($arrayRefs[$len]);
continue;
}
}
$queue[$len] = $a;
$stub->position = $len++;
}
$stub = $a = null;
} elseif ($zval['zval_isref']) {
if ($useExt) {
$vals[$k] = $hardRefs[$zval['zval_hash']] = new Stub();
$vals[$k]->value = $v;
} else {
$refs[$k] = $vals[$k] = new Stub();
$refs[$k]->value = $v;
$h = spl_object_hash($refs[$k]);
$hardRefs[$h] = &$refs[$k];
$values[$h] = $v;
}
$vals[$k]->handle = ++$refsCounter;
}
}
if ($fromObjCast) {
$fromObjCast = false;
$refs = $vals;
$vals = array();
$j = -1;
foreach ($queue[$i] as $k => $v) {
foreach (array($k => $v) as $a => $v) {
foreach (array($k => true) as $a => $v) {
}
if ($a !== $k) {
$vals = (object) $vals;
@ -281,13 +268,7 @@ class VarCloner extends AbstractCloner
}
$queue[$i] = $vals;
if (isset($arrayRefs[$i])) {
if ($indexed) {
$arrayRefs[$i]->class = Stub::ARRAY_INDEXED;
}
unset($arrayRefs[$i]);
}
unset($indexedArrays[$i]);
}
foreach ($values as $h => $v) {

View File

@ -33,19 +33,9 @@ Symfony\Component\VarDumper\Cloner\Data Object
(
[0] => Array
(
[0] => Symfony\Component\VarDumper\Cloner\Stub Object
[0] => Array
(
[type] => array
[class] => assoc
[value] => 1
[cut] => 0
[handle] => 0
[refCount] => 0
[position] => 1
[attr] => Array
(
)
[1] => 1
)
)
@ -84,7 +74,7 @@ Symfony\Component\VarDumper\Cloner\Data Object
(
[0] => Symfony\Component\VarDumper\Cloner\Stub Object
(
[type] => object
[type] => 4
[class] => stdClass
[value] =>
[cut] => 0
@ -103,7 +93,7 @@ Symfony\Component\VarDumper\Cloner\Data Object
(
[\000+\0001] => Symfony\Component\VarDumper\Cloner\Stub Object
(
[type] => object
[type] => 4
[class] => stdClass
[value] =>
[cut] => 0
@ -118,7 +108,7 @@ Symfony\Component\VarDumper\Cloner\Data Object
[\000+\0002] => Symfony\Component\VarDumper\Cloner\Stub Object
(
[type] => object
[type] => 4
[class] => stdClass
[value] =>
[cut] => 0
@ -174,24 +164,9 @@ object(Symfony\Component\VarDumper\Cloner\Data)#%i (6) {
[0]=>
array(1) {
[0]=>
object(Symfony\Component\VarDumper\Cloner\Stub)#%i (8) {
["type"]=>
string(5) "array"
["class"]=>
string(5) "assoc"
["value"]=>
array(1) {
[1]=>
int(1)
["cut"]=>
int(0)
["handle"]=>
int(0)
["refCount"]=>
int(0)
["position"]=>
int(1)
["attr"]=>
array(0) {
}
}
}
[1]=>
@ -199,7 +174,7 @@ object(Symfony\Component\VarDumper\Cloner\Data)#%i (6) {
["1"]=>
object(Symfony\Component\VarDumper\Cloner\Stub)#%i (8) {
["type"]=>
string(6) "object"
int(4)
["class"]=>
string(8) "stdClass"
["value"]=>
@ -259,7 +234,7 @@ Symfony\Component\VarDumper\Cloner\Data Object
(
[0] => Symfony\Component\VarDumper\Cloner\Stub Object
(
[type] => object
[type] => 4
[class] => %s
[value] =>
[cut] => 0