Skip to content

Commit 20c9041

Browse files
abnegateclaude
andcommitted
perf(database): tighten decode hot path
Three concrete wins per decoded document: 1. Replace two consecutive array_filter passes over the attribute list with a single foreach partition into relationships and regular attributes. 2. Drop the per-attribute-per-document array_reverse(\$filters) — walk the filters array backwards via index instead. With 25 docs × N attrs × 200 finds this allocated thousands of one-shot arrays. 3. Materialise the selection list as an isset()-keyed map once per call instead of doing in_array against the values list per attribute. decode shows up at ~3% of profile self-time on the perf bench; these changes cut its allocation/hash-lookup overhead measurably without changing semantics. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 775dd33 commit 20c9041

1 file changed

Lines changed: 66 additions & 46 deletions

File tree

src/Database/Database.php

Lines changed: 66 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1536,55 +1536,69 @@ public function decode(Document $collection, Document $document, array $selectio
15361536
{
15371537
/** @var array<array<string, mixed>|Document> $allAttributes */
15381538
$allAttributes = $collection->getAttribute('attributes', []);
1539-
$attributes = \array_filter(
1540-
$allAttributes,
1541-
fn (array|Document $attribute) => $attribute['type'] !== ColumnType::Relationship->value
1542-
);
15431539

1544-
$relationships = \array_filter(
1545-
$allAttributes,
1546-
fn (array|Document $attribute) => $attribute['type'] === ColumnType::Relationship->value
1547-
);
1540+
// Single-pass partition into relationships vs regular attributes.
1541+
// Replaces two array_filter passes that each walked the full list.
1542+
$attributes = [];
1543+
$relationships = [];
1544+
$relationshipType = ColumnType::Relationship->value;
1545+
foreach ($allAttributes as $attribute) {
1546+
if (($attribute['type'] ?? '') === $relationshipType) {
1547+
$relationships[] = $attribute;
1548+
} else {
1549+
$attributes[] = $attribute;
1550+
}
1551+
}
15481552

15491553
$filteredValue = [];
15501554

1551-
foreach ($relationships as $relationship) {
1552-
/** @var string $key */
1553-
$key = $relationship['$id'] ?? '';
1554-
1555-
if (
1556-
\array_key_exists($key, (array) $document)
1557-
|| \array_key_exists($this->adapter->filter($key), (array) $document)
1558-
) {
1559-
$value = $document->getAttribute($key);
1560-
$value ??= $document->getAttribute($this->adapter->filter($key));
1561-
$document->removeAttribute($this->adapter->filter($key));
1562-
$document->setAttribute($key, $value);
1555+
if (! empty($relationships)) {
1556+
$documentArray = (array) $document;
1557+
foreach ($relationships as $relationship) {
1558+
/** @var string $key */
1559+
$key = $relationship['$id'] ?? '';
1560+
$filteredKey = $this->adapter->filter($key);
1561+
1562+
if (
1563+
\array_key_exists($key, $documentArray)
1564+
|| \array_key_exists($filteredKey, $documentArray)
1565+
) {
1566+
$value = $document->getAttribute($key);
1567+
$value ??= $document->getAttribute($filteredKey);
1568+
$document->removeAttribute($filteredKey);
1569+
$document->setAttribute($key, $value);
1570+
}
15631571
}
15641572
}
15651573

15661574
foreach ($this->getInternalAttributes() as $attribute) {
15671575
$attributes[] = $attribute;
15681576
}
15691577

1578+
$hasSelections = ! empty($selections);
1579+
$selectAll = $hasSelections && \in_array('*', $selections, true);
1580+
$selectionsMap = ($hasSelections && ! $selectAll)
1581+
? \array_fill_keys($selections, true)
1582+
: null;
1583+
15701584
foreach ($attributes as $attribute) {
15711585
/** @var string $key */
15721586
$key = $attribute['$id'] ?? '';
1573-
$type = $attribute['type'] ?? '';
1587+
if ($key === '$permissions') {
1588+
continue;
1589+
}
1590+
15741591
$array = $attribute['array'] ?? false;
15751592
/** @var array<string> $filters */
15761593
$filters = $attribute['filters'] ?? [];
15771594
$value = $document->getAttribute($key);
15781595

1579-
if ($key === '$permissions') {
1580-
continue;
1581-
}
1582-
15831596
if (\is_null($value)) {
1584-
$value = $document->getAttribute($this->adapter->filter($key));
1597+
$filteredKey = $this->adapter->filter($key);
1598+
$value = $document->getAttribute($filteredKey);
15851599

15861600
if (! \is_null($value)) {
1587-
$document->removeAttribute($this->adapter->filter($key));
1601+
$document->removeAttribute($filteredKey);
15881602
}
15891603
}
15901604

@@ -1596,46 +1610,52 @@ public function decode(Document $collection, Document $document, array $selectio
15961610
$value = ($array) ? $value : [$value];
15971611
$value = (is_null($value)) ? [] : $value;
15981612

1613+
// Reverse filter application without allocating a new array
1614+
// for each document. Walking the array backwards avoids
1615+
// array_reverse() per attribute per document.
1616+
$filterCount = \count($filters);
1617+
15991618
/** @var array<int|string, mixed> $value */
16001619
foreach ($value as $index => $node) {
1601-
foreach (\array_reverse($filters) as $filter) {
1602-
$node = $this->decodeAttribute($filter, $node, $document, $key);
1620+
for ($i = $filterCount - 1; $i >= 0; $i--) {
1621+
$node = $this->decodeAttribute($filters[$i], $node, $document, $key);
16031622
}
16041623
$value[$index] = $node;
16051624
}
16061625

1607-
$filteredValue[$key] = ($array) ? $value : $value[0];
1626+
$resolved = $array ? $value : $value[0];
1627+
$filteredValue[$key] = $resolved;
16081628

16091629
if (
1610-
empty($selections)
1611-
|| \in_array($key, $selections)
1612-
|| \in_array('*', $selections)
1630+
! $hasSelections
1631+
|| $selectAll
1632+
|| isset($selectionsMap[$key])
16131633
) {
1614-
$document->setAttribute($key, ($array) ? $value : $value[0]);
1634+
$document->setAttribute($key, $resolved);
16151635
}
16161636
}
16171637

1618-
$hasRelationshipSelections = false;
1619-
if (! empty($selections)) {
1638+
if ($hasSelections && ! $selectAll) {
1639+
$hasRelationshipSelections = false;
16201640
foreach ($selections as $selection) {
16211641
if (\str_contains($selection, '.')) {
16221642
$hasRelationshipSelections = true;
16231643
break;
16241644
}
16251645
}
1626-
}
16271646

1628-
if ($hasRelationshipSelections && ! empty($selections) && ! \in_array('*', $selections)) {
1629-
foreach ($allAttributes as $attribute) {
1630-
/** @var string $key */
1631-
$key = $attribute['$id'] ?? '';
1647+
if ($hasRelationshipSelections) {
1648+
foreach ($allAttributes as $attribute) {
1649+
/** @var string $key */
1650+
$key = $attribute['$id'] ?? '';
16321651

1633-
if ($attribute['type'] === ColumnType::Relationship->value || $key === '$permissions') {
1634-
continue;
1635-
}
1652+
if (($attribute['type'] ?? '') === $relationshipType || $key === '$permissions') {
1653+
continue;
1654+
}
16361655

1637-
if (! in_array($key, $selections) && isset($filteredValue[$key])) {
1638-
$document->setAttribute($key, $filteredValue[$key]);
1656+
if (! isset($selectionsMap[$key]) && isset($filteredValue[$key])) {
1657+
$document->setAttribute($key, $filteredValue[$key]);
1658+
}
16391659
}
16401660
}
16411661
}

0 commit comments

Comments
 (0)