From 1f10fab294eb4090cf9a49f49ccc01b64fd14ca4 Mon Sep 17 00:00:00 2001 From: Jeff Ching Date: Tue, 3 Apr 2018 12:55:47 -0700 Subject: [PATCH] Debugger: De-dup arrays and string in VariableTable (#946) * If a variable's id (hash value) is defined, use it to dedup entries in the VariableTable * Fix phpcs --- Debugger/src/Breakpoint.php | 7 +- Debugger/src/VariableTable.php | 109 ++++++++++++---------- Debugger/tests/Unit/VariableTableTest.php | 41 +++++++- 3 files changed, 104 insertions(+), 53 deletions(-) diff --git a/Debugger/src/Breakpoint.php b/Debugger/src/Breakpoint.php index b8c7e42d15a6..7b156130248f 100644 --- a/Debugger/src/Breakpoint.php +++ b/Debugger/src/Breakpoint.php @@ -535,7 +535,8 @@ public function addStackFrame($stackFrameData) foreach ($stackFrameData['locals'] as $local) { $value = isset($local['value']) ? $local['value'] : null; - $variable = $this->addVariable($local['name'], $value); + $hash = isset($local['id']) ? $local['id'] : null; + $variable = $this->addVariable($local['name'], $value, $hash); $sf->addLocal($variable); } @@ -615,10 +616,10 @@ private function setError($type, $message, array $parameters = []) ); } - private function addVariable($name, $value) + private function addVariable($name, $value, $hash = null) { $this->variableTable = $this->variableTable ?: new VariableTable(); - return $this->variableTable->register($name, $value); + return $this->variableTable->register($name, $value, $hash); } private function ensureExtensionLoaded() diff --git a/Debugger/src/VariableTable.php b/Debugger/src/VariableTable.php index be2ef28ef1d9..26a8a09de811 100644 --- a/Debugger/src/VariableTable.php +++ b/Debugger/src/VariableTable.php @@ -56,11 +56,12 @@ public function __construct(array $initialVariables = []) * * @param string $name The name of the variable * @param mixed $value The value of the variable + * @param string|null $hash [optional] The object hash to use for deduping * @return Variable */ - public function register($name, $value) + public function register($name, $value, $hash = null) { - return $this->doRegister($name, $value, 0); + return $this->doRegister($name, $value, 0, $hash); } /** @@ -89,55 +90,29 @@ public function variables() return $this->variables; } - private function doRegister($name, $value, $depth, $index = null) + private function doRegister($name, $value, $depth, $hash) { - $name = (string)$name; + $type = $this->calcuateType($value); $members = []; - $shared = false; - $type = gettype($value); - $variableValue = null; - switch ($type) { + // If the variable already exists in the table (via object hash), then + // return a reference Variable to that VariableTable entry. + $hash = $hash ?: $this->calculateHash($value); + if ($hash && array_key_exists($hash, $this->sharedVariableIndex)) { + return new Variable($name, $type, [ + 'varTableIndex' => $this->sharedVariableIndex[$hash] + ]); + } + + switch (gettype($value)) { case 'object': - $type = get_class($value); - $hash = spl_object_hash($value); - - if (array_key_exists($hash, $this->sharedVariableIndex)) { - $index = $this->sharedVariableIndex[$hash]; - $shared = true; - } else { - $index = $this->nextIndex; - $this->sharedVariableIndex[$hash] = $index; - - $members = []; - if ($depth < self::MAX_MEMBER_DEPTH) { - foreach (get_object_vars($value) as $key => $member) { - array_push($members, $this->doRegister($key, $member, $depth + 1)); - } - } - - $this->nextIndex++; - array_push($this->variables, new Variable($name, $type, [ - 'value' => "$type ($hash)", - 'members' => $members - ])); - } - return new Variable($name, $type, [ - 'varTableIndex' => $index - ]); + $variableValue = "$type ($hash)"; + $members = $this->doRegisterMembers(get_object_vars($value), $depth); break; case 'array': $arraySize = count($value); - $members = []; - if ($depth < self::MAX_MEMBER_DEPTH) { - foreach ($value as $key => $member) { - array_push($members, $this->doRegister($key, $member, $depth + 1)); - } - } - return new Variable($name, $type, [ - 'value' => "array ($arraySize)", - 'members' => $members - ]); + $variableValue = "array ($arraySize)"; + $members = $this->doRegisterMembers($value, $depth); break; case 'NULL': $variableValue = 'NULL'; @@ -146,8 +121,48 @@ private function doRegister($name, $value, $depth, $index = null) $variableValue = (string)$value; } - return new Variable($name, $type, [ - 'value' => $variableValue - ]); + if ($hash) { + // If this variable has an object hash, then save it in the + // VariableTable and return a reference Variable to that entry. + $index = $this->nextIndex; + $this->sharedVariableIndex[$hash] = $index; + $this->nextIndex++; + + array_push($this->variables, new Variable($name, $type, [ + 'value' => $variableValue, + 'members' => $members + ])); + return new Variable($name, $type, [ + 'varTableIndex' => $index + ]); + } else { + return new Variable($name, $type, [ + 'value' => $variableValue, + 'members' => $members + ]); + } + } + + private function calcuateType($value) + { + return is_object($value) + ? get_class($value) + : gettype($value); + } + + private function calculateHash($value) + { + return is_object($value) ? spl_object_hash($value) : null; + } + + private function doRegisterMembers($array, $depth) + { + $members = []; + if ($depth < self::MAX_MEMBER_DEPTH) { + foreach ($array as $key => $member) { + $members[] = $this->doRegister($key, $member, $depth + 1, null); + } + } + return $members; } } diff --git a/Debugger/tests/Unit/VariableTableTest.php b/Debugger/tests/Unit/VariableTableTest.php index cb7b314efcc4..b828671c500a 100644 --- a/Debugger/tests/Unit/VariableTableTest.php +++ b/Debugger/tests/Unit/VariableTableTest.php @@ -91,12 +91,11 @@ public function testRegistersSimilarObjects() public function testRegistersArrayAsSameObjects() { - $this->markTestSkipped('Array deduping NYI'); $variableTable = new VariableTable(); $object = ['abc', 123]; - $variable1 = $variableTable->register('int', $object); - $variable2 = $variableTable->register('int2', $object); + $variable1 = $variableTable->register('int', $object, 'hashid'); + $variable2 = $variableTable->register('int2', $object, 'hashid'); $data1 = $variable1; $this->assertEquals(0, $data1->jsonSerialize()['varTableIndex']); @@ -135,4 +134,40 @@ public function primitiveValues() [NULL, 'NULL', 'NULL'] ]; } + + public function testRegisterObjectWithId() + { + $variableTable = new VariableTable(); + $object = new Int64('123'); + $object2 = new Int64('123'); + + $variable1 = $variableTable->register('int', $object, 'hashid'); + $variable2 = $variableTable->register('int2', $object2, 'hashid'); + + $data1 = $variable1; + $this->assertEquals(0, $data1->jsonSerialize()['varTableIndex']); + + $data2 = $variable2; + $this->assertEquals(0, $data2->jsonSerialize()['varTableIndex']); + + $this->assertCount(1, $variableTable->variables()); + } + + public function testRegisterSameStrings() + { + $variableTable = new VariableTable(); + $string = 'Hello world'; + $string2 = 'Hello world'; + + $variable1 = $variableTable->register('int', $string, 'hashid'); + $variable2 = $variableTable->register('int2', $string2, 'hashid'); + + $data1 = $variable1; + $this->assertEquals(0, $data1->jsonSerialize()['varTableIndex']); + + $data2 = $variable2; + $this->assertEquals(0, $data2->jsonSerialize()['varTableIndex']); + + $this->assertCount(1, $variableTable->variables()); + } }