Skip to content

Bound namespace-navigation discovery cost (uncached workspace source) #343

Description

@Firehed

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-onlyDocumentIndexer 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)

  1. 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.
  2. 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).
  3. 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).
  4. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions