Summary
Namespace-navigation completion (#330) decides node-vs-inline by peeking one level into
each prefix-matched child namespace (NamespaceCatalog::childrenOf() per child). This
discovery work is not bounded by the result cap — the cap limits output, not how many
namespaces are inspected. On a large, fully-indexed workspace this can become the
per-keystroke hot path, and #339 (unqualified/imported-prefix navigation) makes qualified
completion — the common case — trigger it far more often, so it's worth tracking now.
Where the cost is
ComposerNamespaceSource / ReflectionNamespaceSource peeks are memoised by
CachedNamespaceCatalog, so vendor/built-in namespaces are inspected at most once per
session. The unbounded piece is WorkspaceNamespaceSource, which is deliberately not
cached (the workspace changes on every keystroke):
SymbolIndex::inNamespace($ns) is an O(1) hash lookup, but
SymbolIndex::namespaces() walks every namespace bucket (O(#distinct namespaces in
the index)), and childrenOf() calls it on each invocation.
So a bare new \ peeks each top-level namespace, and each peek re-scans the index:
roughly O(#top-level-namespaces × #namespaces-in-index) on the keystroke after \.
Why it's fine for v1 (and when it stops being fine)
The workspace SymbolIndex is currently open-files-only — DocumentIndexer populates
it on didOpen and there is no project-wide scan wired — so namespaces() iterates only
the handful of namespaces among open files. The quadratic term is bounded by open files
today. It becomes real once a full workspace indexer lands (the index holds the whole
project) — which is also roughly when #339 widens the trigger surface.
Remediation strategies (options, not a decision)
- Request-scoped memoisation. Cache
childrenOf() results for the duration of a single
completion resolution (one keystroke), since a completion may query the same namespaces
repeatedly. Cheapest change; keeps the "workspace uncached across keystrokes" invariant.
- Incremental namespace tree in
SymbolIndex. Maintain a parent→children map updated on
index add/remove, so namespaces()/childrenOf() are O(children) rather than O(index).
- Bound the peek. The inline peek only needs a count; cap it (e.g. stop after
INLINE_THRESHOLD + 1) so a peek is O(threshold), not O(namespace size).
- Skip inline for the workspace contribution. Inline only cached (vendor/built-in)
sources; offer workspace child namespaces as plain nodes. Trades the inline nicety on
workspace symbols for bounded cost.
Acceptance
- Navigation completion stays responsive with a large, fully-indexed workspace (add a guard
or benchmark for the bare-\ case).
- The
WorkspaceNamespaceSource remains correct under mid-edit index churn (no stale caching
across keystrokes).
Related
This issue body was written by AI at the maintainer's request, from a code review of #342,
and reviewed by a human.
Summary
Namespace-navigation completion (#330) decides node-vs-inline by peeking one level into
each prefix-matched child namespace (
NamespaceCatalog::childrenOf()per child). Thisdiscovery work is not bounded by the result cap — the cap limits output, not how many
namespaces are inspected. On a large, fully-indexed workspace this can become the
per-keystroke hot path, and #339 (unqualified/imported-prefix navigation) makes qualified
completion — the common case — trigger it far more often, so it's worth tracking now.
Where the cost is
ComposerNamespaceSource/ReflectionNamespaceSourcepeeks are memoised byCachedNamespaceCatalog, so vendor/built-in namespaces are inspected at most once persession. The unbounded piece is
WorkspaceNamespaceSource, which is deliberately notcached (the workspace changes on every keystroke):
SymbolIndex::inNamespace($ns)is an O(1) hash lookup, butSymbolIndex::namespaces()walks every namespace bucket (O(#distinct namespaces inthe index)), and
childrenOf()calls it on each invocation.So a bare
new \peeks each top-level namespace, and each peek re-scans the index:roughly O(#top-level-namespaces × #namespaces-in-index) on the keystroke after
\.Why it's fine for v1 (and when it stops being fine)
The workspace
SymbolIndexis currently open-files-only —DocumentIndexerpopulatesit on
didOpenand there is no project-wide scan wired — sonamespaces()iterates onlythe handful of namespaces among open files. The quadratic term is bounded by open files
today. It becomes real once a full workspace indexer lands (the index holds the whole
project) — which is also roughly when #339 widens the trigger surface.
Remediation strategies (options, not a decision)
childrenOf()results for the duration of a singlecompletion resolution (one keystroke), since a completion may query the same namespaces
repeatedly. Cheapest change; keeps the "workspace uncached across keystrokes" invariant.
SymbolIndex. Maintain a parent→children map updated onindex add/remove, so
namespaces()/childrenOf()are O(children) rather than O(index).INLINE_THRESHOLD + 1) so a peek is O(threshold), not O(namespace size).sources; offer workspace child namespaces as plain nodes. Trades the inline nicety on
workspace symbols for bounded cost.
Acceptance
or benchmark for the bare-
\case).WorkspaceNamespaceSourceremains correct under mid-edit index churn (no stale cachingacross keystrokes).
Related
completion routinely triggers navigation.
This issue body was written by AI at the maintainer's request, from a code review of #342,
and reviewed by a human.