From 42a4098a8d83a98ee83b3903b0ad560ee5761b83 Mon Sep 17 00:00:00 2001 From: Sebastian Mendel Date: Thu, 22 Jan 2026 02:12:55 +0100 Subject: [PATCH 1/2] refactor: use file path comparison instead of object identity for DocumentEntryNode Replace object identity comparison (===) with file path comparison when matching DocumentEntryNode instances. The file path is the natural unique identifier for a document entry, making identity comparison fragile and unnecessarily restrictive. This change: - Makes the code more robust to refactoring that may create new instances - Aligns with how SectionEntryNode comparison already works (uses getId()) - Enables future optimizations that may cache or recreate entry objects Changed locations: - RenderContext::getDocumentNodeForEntry() - GlobalMenuPass (root document lookup) - DocumentTreeIterator::current() No behavior change - documents are still matched by their file path, which was always the intent. --- packages/guides/src/Compiler/Passes/GlobalMenuPass.php | 3 ++- packages/guides/src/RenderContext.php | 3 ++- packages/guides/src/Renderer/DocumentTreeIterator.php | 3 ++- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/packages/guides/src/Compiler/Passes/GlobalMenuPass.php b/packages/guides/src/Compiler/Passes/GlobalMenuPass.php index dac74b564..cbe9f4b18 100644 --- a/packages/guides/src/Compiler/Passes/GlobalMenuPass.php +++ b/packages/guides/src/Compiler/Passes/GlobalMenuPass.php @@ -62,8 +62,9 @@ public function run(array $documents, CompilerContextInterface $compilerContext) } $rootDocument = null; + $rootFile = $rootDocumentEntry->getFile(); foreach ($documents as $document) { - if ($document->getDocumentEntry() === $rootDocumentEntry) { + if ($document->getDocumentEntry()->getFile() === $rootFile) { $rootDocument = $document; break; } diff --git a/packages/guides/src/RenderContext.php b/packages/guides/src/RenderContext.php index bc8e52b64..5d797667e 100644 --- a/packages/guides/src/RenderContext.php +++ b/packages/guides/src/RenderContext.php @@ -222,8 +222,9 @@ public function getProjectNode(): ProjectNode public function getDocumentNodeForEntry(DocumentEntryNode $entryNode): DocumentNode { + $file = $entryNode->getFile(); foreach ($this->allDocuments as $child) { - if ($child->getDocumentEntry() === $entryNode) { + if ($child->getDocumentEntry()->getFile() === $file) { return $child; } } diff --git a/packages/guides/src/Renderer/DocumentTreeIterator.php b/packages/guides/src/Renderer/DocumentTreeIterator.php index 30266415d..d2904dc5c 100644 --- a/packages/guides/src/Renderer/DocumentTreeIterator.php +++ b/packages/guides/src/Renderer/DocumentTreeIterator.php @@ -41,8 +41,9 @@ public function __construct( public function current(): DocumentNode { + $file = $this->levelNodes[$this->position]->getFile(); foreach ($this->documents as $document) { - if ($document->getDocumentEntry() === $this->levelNodes[$this->position]) { + if ($document->getDocumentEntry()->getFile() === $file) { return $document; } } From 0c2f2b696357d86bd564a1e6f51d2551b92a4ccb Mon Sep 17 00:00:00 2001 From: Sebastian Mendel Date: Thu, 22 Jan 2026 02:19:47 +0100 Subject: [PATCH 2/2] perf: add O(1) lookup caching for ProjectNode document entries - Cache root document entry for instant retrieval - Use direct hash lookup in getDocumentEntry() with fallback for numerically-indexed arrays from setDocumentEntries() - Invalidate cache in setDocumentEntries() and reset() This optimization is enabled by the previous commit's refactoring from object identity to file path comparison. --- .../Compiler/Passes/ToctreeValidationPass.php | 3 ++- packages/guides/src/Nodes/ProjectNode.php | 22 +++++++++++++++++++ 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/packages/guides/src/Compiler/Passes/ToctreeValidationPass.php b/packages/guides/src/Compiler/Passes/ToctreeValidationPass.php index 75a69bb9b..ec93f9774 100644 --- a/packages/guides/src/Compiler/Passes/ToctreeValidationPass.php +++ b/packages/guides/src/Compiler/Passes/ToctreeValidationPass.php @@ -68,6 +68,7 @@ public function run(array $documents, CompilerContextInterface $compilerContext) public function isMissingInToctree(DocumentEntryNode $documentEntry, ProjectNode $projectNode): bool { - return $documentEntry->getParent() === null && $documentEntry !== $projectNode->getRootDocumentEntry(); + return $documentEntry->getParent() === null + && $documentEntry->getFile() !== $projectNode->getRootDocumentEntry()->getFile(); } } diff --git a/packages/guides/src/Nodes/ProjectNode.php b/packages/guides/src/Nodes/ProjectNode.php index 7690c434c..84946f998 100644 --- a/packages/guides/src/Nodes/ProjectNode.php +++ b/packages/guides/src/Nodes/ProjectNode.php @@ -47,6 +47,9 @@ final class ProjectNode extends CompoundNode /** @var array> */ private array $internalLinkTargets = []; + /** Cached root document entry for O(1) lookup */ + private DocumentEntryNode|null $rootDocumentEntry = null; + /** @var DocumentEntryNode[] */ private array $documentEntries = []; private DateTimeImmutable $lastRendered; @@ -182,6 +185,10 @@ public function getAllInternalTargets(): array public function addDocumentEntry(DocumentEntryNode $documentEntry): void { + if ($documentEntry->isRoot()) { + $this->rootDocumentEntry = $documentEntry; + } + $this->documentEntries[$documentEntry->getFile()] = $documentEntry; } @@ -193,8 +200,15 @@ public function getAllDocumentEntries(): array public function getRootDocumentEntry(): DocumentEntryNode { + if ($this->rootDocumentEntry !== null) { + return $this->rootDocumentEntry; + } + + // Fallback: scan entries (handles setDocumentEntries with numeric keys) foreach ($this->documentEntries as $documentEntry) { if ($documentEntry->isRoot()) { + $this->rootDocumentEntry = $documentEntry; + return $documentEntry; } } @@ -205,6 +219,12 @@ public function getRootDocumentEntry(): DocumentEntryNode /** @throws DocumentEntryNotFound */ public function getDocumentEntry(string $file): DocumentEntryNode { + // O(1) lookup - documentEntries is keyed by file path (from addDocumentEntry) + if (isset($this->documentEntries[$file])) { + return $this->documentEntries[$file]; + } + + // Fallback: iterate for numerically-indexed arrays (from setDocumentEntries) foreach ($this->documentEntries as $documentEntry) { if ($documentEntry->getFile() === $file) { return $documentEntry; @@ -218,6 +238,7 @@ public function getDocumentEntry(string $file): DocumentEntryNode public function setDocumentEntries(array $documentEntries): void { $this->documentEntries = $documentEntries; + $this->rootDocumentEntry = null; // Invalidate cache } public function findDocumentEntry(string $filePath): DocumentEntryNode|null @@ -228,6 +249,7 @@ public function findDocumentEntry(string $filePath): DocumentEntryNode|null public function reset(): void { $this->documentEntries = []; + $this->rootDocumentEntry = null; } public function getLastRendered(): DateTimeImmutable