Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
172 changes: 172 additions & 0 deletions memory_analysis.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
<?php
/**
* Memory usage analysis for JsonMapper ancestors
*
* This script demonstrates the memory impact of ancestor tracking
* and shows how optimization can help.
*/

require_once 'vendor/autoload.php';

echo "=== JsonMapper Ancestor Memory Analysis ===\n\n";

// Test 1: Memory usage without ancestors (baseline)
echo "1. Baseline memory usage (no ancestors):\n";
$jm = new JsonMapper();

$json = createLargeNestedJson(5, 50); // 5 levels deep, 50 chars per field
$target = new stdClass();

$memBefore = memory_get_usage(true);
$result = $jm->map($json, $target);
$memAfter = memory_get_usage(true);

echo " Memory used: " . formatBytes($memAfter - $memBefore) . "\n\n";

// Test 2: Memory usage with ancestor tracking
echo "2. Memory usage with ancestor tracking:\n";
$jm = new JsonMapper();
$memoryUsages = [];

$jm->classMap['DeepClass'] = function($class, $jvalue, $ancestors) use (&$memoryUsages) {
$memoryUsages[] = memory_get_usage(true);
return 'stdClass';
};

// Create target with typed property to trigger classMap
$target = new class {
/** @var DeepClass */
public $level1;
};

$json = createLargeNestedJsonWithClasses(5, 100);

$memBefore = memory_get_usage(true);
$result = $jm->map($json, $target);
$memAfter = memory_get_usage(true);

echo " Memory used: " . formatBytes($memAfter - $memBefore) . "\n";
echo " Peak memory during classMap calls: " . formatBytes(max($memoryUsages) - $memBefore) . "\n\n";

// Test 3: Memory optimization strategies
echo "3. Memory optimization strategies:\n\n";

echo " Strategy A: Depth Limiting\n";
$ancestors = createMockAncestors(10, 200); // 10 deep, 200 chars each
$memBefore = memory_get_usage(true);
$limited = limitAncestorDepth($ancestors, 3);
$memAfter = memory_get_usage(true);
echo " Original: " . count($ancestors) . " ancestors, " . formatBytes(calculateAncestorMemory($ancestors)) . "\n";
echo " Limited: " . count($limited) . " ancestors, " . formatBytes(calculateAncestorMemory($limited)) . "\n";
echo " Memory saved: " . formatBytes(calculateAncestorMemory($ancestors) - calculateAncestorMemory($limited)) . "\n\n";

echo " Strategy B: Field Filtering\n";
$ancestors = createMockAncestors(5, 500); // 5 levels, 500 chars of data each
$filtered = filterAncestorFields($ancestors, ['id', 'version', 'type']);
echo " Original: " . formatBytes(calculateAncestorMemory($ancestors)) . "\n";
echo " Filtered: " . formatBytes(calculateAncestorMemory($filtered)) . "\n";
echo " Memory saved: " . formatBytes(calculateAncestorMemory($ancestors) - calculateAncestorMemory($filtered)) . "\n\n";

echo " Strategy C: Combined Optimization\n";
$ancestors = createMockAncestors(15, 1000); // 15 deep, 1KB each
$optimized = filterAncestorFields(limitAncestorDepth($ancestors, 5), ['id', 'version']);
echo " Original: " . count($ancestors) . " ancestors, " . formatBytes(calculateAncestorMemory($ancestors)) . "\n";
echo " Optimized: " . count($optimized) . " ancestors, " . formatBytes(calculateAncestorMemory($optimized)) . "\n";
echo " Memory saved: " . formatBytes(calculateAncestorMemory($ancestors) - calculateAncestorMemory($optimized)) . "\n";
echo " Reduction: " . round((1 - calculateAncestorMemory($optimized) / calculateAncestorMemory($ancestors)) * 100, 1) . "%\n\n";

// Summary
echo "=== Summary ===\n";
echo "For deeply nested JSON structures, ancestor tracking can use significant memory.\n";
echo "Optimization strategies can reduce memory usage by 80-90% in extreme cases.\n";
echo "Most real-world JSON has < 10 levels, making the impact manageable.\n";
echo "Use MemoryOptimizedJsonMapper for memory-constrained environments.\n";

function createLargeNestedJson($depth, $stringSize) {
$data = str_repeat('x', $stringSize);
$json = new stdClass();
$json->data = $data;
$json->extra1 = $data;
$json->extra2 = $data;

$current = $json;
for ($i = 1; $i < $depth; $i++) {
$current->nested = new stdClass();
$current->nested->data = $data;
$current->nested->extra1 = $data;
$current->nested->extra2 = $data;
$current = $current->nested;
}

return $json;
}

function createLargeNestedJsonWithClasses($depth, $stringSize) {
$data = str_repeat('x', $stringSize);
$json = new stdClass();
$json->data = $data;
$json->extra1 = $data;
$json->extra2 = $data;

$current = $json;
for ($i = 1; $i < $depth; $i++) {
$current->level1 = new stdClass();
$current->level1->data = $data;
$current->level1->extra1 = $data;
$current->level1->extra2 = $data;
$current = $current->level1;
}

return $json;
}

function createMockAncestors($count, $dataSize) {
$ancestors = [];
$data = str_repeat('x', $dataSize);

for ($i = 0; $i < $count; $i++) {
$ancestor = new stdClass();
$ancestor->id = "id_$i";
$ancestor->version = "v$i";
$ancestor->type = "type_$i";
$ancestor->large_data = $data;
$ancestor->extra_field_1 = $data;
$ancestor->extra_field_2 = $data;
$ancestors[] = $ancestor;
}

return $ancestors;
}

function limitAncestorDepth($ancestors, $maxDepth) {
if (count($ancestors) <= $maxDepth) {
return $ancestors;
}
return array_slice($ancestors, -$maxDepth);
}

function filterAncestorFields($ancestors, $allowedFields) {
return array_map(function($ancestor) use ($allowedFields) {
$filtered = new stdClass();
foreach ($allowedFields as $field) {
if (isset($ancestor->$field)) {
$filtered->$field = $ancestor->$field;
}
}
return $filtered;
}, $ancestors);
}

function calculateAncestorMemory($ancestors) {
return strlen(serialize($ancestors));
}

function formatBytes($size, $precision = 2) {
$units = ['B', 'KB', 'MB', 'GB'];
$unit = 0;
while ($size >= 1024 && $unit < count($units) - 1) {
$size /= 1024;
$unit++;
}
return round($size, $precision) . ' ' . $units[$unit];
}
59 changes: 46 additions & 13 deletions src/JsonMapper.php
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,26 @@ class JsonMapper
* @see mapArray()
*/
public function map($json, $object)
{
return $this->mapWithAncestors($json, $object, array());
}

/**
* Map data all data in $json into the given $object instance with ancestor tracking.
*
* @param object|array $json JSON object structure from json_decode()
* @param object|class-string $object Object to map $json data into
* @param array $ancestors Array of ancestor JSON objects from root to current level
*
* @return mixed Mapped object is returned.
*
* @template T
* @phpstan-param class-string<T>|T $object
* @phpstan-return T
*
* @see map()
*/
protected function mapWithAncestors($json, $object, $ancestors)
{
if ($this->bEnforceMapType && !is_object($json)) {
throw new InvalidArgumentException(
Expand Down Expand Up @@ -240,7 +260,7 @@ public function map($json, $object)
}

$type = $this->getFullNamespace($type, $strNs);
$type = $this->getMappedType($type, $jvalue, $json);
$type = $this->getMappedType($type, $jvalue, $ancestors);

if ($type === null || $type === 'mixed') {
//no given type - simply set the json data
Expand Down Expand Up @@ -318,7 +338,7 @@ public function map($json, $object)
if ($subtypeNullable) {
$subtype = '?' . $subtype;
}
$child = $this->mapArray($jvalue, $array, $subtype, $key);
$child = $this->mapArray($jvalue, $array, $subtype, $key, $ancestors);

} else if ($this->isFlatType(gettype($jvalue))) {
//use constructor parameter if we have a class
Expand All @@ -332,7 +352,8 @@ public function map($json, $object)
$child = $this->createInstance($type, true, $jvalue);
} else {
$child = $this->createInstance($type, false, $jvalue);
$this->map($jvalue, $child);
// Add current parent JSON object to ancestors for nested mapping
$this->mapWithAncestors($jvalue, $child, array_merge($ancestors, [$json]));
}
$this->setProperty($object, $accessor, $child);
}
Expand Down Expand Up @@ -443,10 +464,11 @@ protected function removeUndefinedAttributes($object, $providedProperties)
* Pass "null" to not convert any values
* @param string $parent_key Defines the key this array belongs to
* in order to aid debugging.
* @param array $ancestors Array of ancestor JSON objects from root to current level
*
* @return mixed Mapped $array is returned
*/
public function mapArray($json, $array, $class = null, $parent_key = '')
public function mapArray($json, $array, $class = null, $parent_key = '', $ancestors = array())
{
$isNullable = $this->isNullable($class);
$class = $this->removeNullable($class);
Expand All @@ -461,14 +483,17 @@ public function mapArray($json, $array, $class = null, $parent_key = '')
);
}

$class = $this->getMappedType($originalClass, $jvalue, $json);
$class = $this->getMappedType($originalClass, $jvalue, $ancestors);
if ($class === null) {
$array[$key] = $jvalue;
} else if ($this->isArrayOfType($class)) {
// Add current JSON object to ancestors for nested array mapping
$array[$key] = $this->mapArray(
$jvalue,
array(),
substr($class, 0, -2)
substr($class, 0, -2),
'',
array_merge($ancestors, [$json])
);
} else if ($this->isFlatType(gettype($jvalue))) {
//use constructor parameter if we have a class
Expand Down Expand Up @@ -499,13 +524,20 @@ public function mapArray($json, $array, $class = null, $parent_key = '')
. ' "' . gettype($jvalue) . '"'
);
} else if (is_a($class, 'ArrayObject', true)) {
// Add current JSON object to ancestors for ArrayObject mapping
$array[$key] = $this->mapArray(
$jvalue,
$this->createInstance($class)
$this->createInstance($class),
null,
'',
array_merge($ancestors, [$json])
);
} else {
$array[$key] = $this->map(
$jvalue, $this->createInstance($class, false, $jvalue)
// Add current JSON object to ancestors for nested object mapping
$array[$key] = $this->mapWithAncestors(
$jvalue,
$this->createInstance($class, false, $jvalue),
array_merge($ancestors, [$json])
);
}
}
Expand Down Expand Up @@ -747,12 +779,13 @@ protected function createInstance(
*
* Lets you override class names via the $classMap property.
*
* @param string|null $type Type name to map
* @param mixed $jvalue Constructor parameter (the json value)
* @param string|null $type Type name to map
* @param mixed $jvalue Constructor parameter (the json value)
* @param array $ancestors Array of ancestor JSON objects from root to current level
*
* @return string|null The mapped type/class name
*/
protected function getMappedType($type, $jvalue = null, $pjson = null)
protected function getMappedType($type, $jvalue = null, $ancestors = array())
{
if (isset($this->classMap[$type])) {
$target = $this->classMap[$type];
Expand All @@ -766,7 +799,7 @@ protected function getMappedType($type, $jvalue = null, $pjson = null)

if ($target) {
if (is_callable($target)) {
$type = $target($type, $jvalue, $pjson);
$type = $target($type, $jvalue, $ancestors);
} else {
$type = $target;
}
Expand Down
Loading
Loading