Skip to content

perf(resolve): enumerate imported members without walking their routes - #1348

Merged
HuiJun merged 3 commits into
developfrom
perf/enumerate-children-without-route-walks
Oct 21, 2026
Merged

HuiJun merged 3 commits into
developfrom
perf/enumerate-children-without-route-walks

Conversation

@devin-ai-integration

@devin-ai-integration devin-ai-integration Bot commented Oct 21, 2026 •

Copy link
Copy Markdown
Contributor

What and why

The 0.11.0 performance record (measured against v0.10.1) finds the language server's ReferencesCold at 44.5k → 6.28M allocs/op and RenameWarm at 14.9k → 12.4M. Bisected over the pull-request merges to #1062 (hidden colliding imports, 45k → 17.4M), later cut to 6.28M by #1158 and #1209. The allocation profile puts three quarters of what is left in Resolver.namespaceChildren, which for every index entry under an imported namespace:

  • built the entry's qualified name with Index.GetFQN to decide whether it is a re-export (reexportedUnder);
  • for a re-export, allocated a one-candidate slice per name it binds and handed it to withoutHiddenImports, which answered from the memoized hidesBelow that nothing on the route is hidden (hiddenOnRoute);
  • copied the route conditions gating the name into a [][]ElementFilter through Index.ReexportGates, which admitsUnderName then read once, accepting the first unfiltered route (gateRoutes).

This change keeps the decisions and drops the allocations: hiddenOnRoute asks hidesBelow first; reexportedUnder compares the owner chain against the prefix segment by segment with the new symbols.FQNIs; admitsUnderName walks the routes in place through the new Index.EachReexportGate (ReexportGates stays for its other caller and gateRoutes returns nil when no route qualifies). ReferencesWarm, WorkspaceUpdate and FormatEdits are unchanged.

What this does not recover: both rows still allocate about 1 GiB/op and take over a second, against 49 ms and 23 ms before #1062. The space profile puts that in elementList.add: each query re-enumerates the members every wildcard import brings (importedMembersInto, whose per-edge memo enumMembers is cleared when the outermost enumeration returns), per occurrence in ImportedBeside for a rename and per document for the references index, and the enumeration itself is what #1062 made expensive. Keeping that enumeration across a query is a design change in the same code the in-flight perf/name-first-recursive-import-matching branch reworks, so it is left to a follow-up rather than done here.

How it was verified

  • TestImportedMemberEnumerationDoesNotWalkRoutesPerMember (internal/semantic/resolve): a differential testing.AllocsPerRun bound — enumerating a member re-exported through a second wildcard import (R imports Q::*, Q imports L::*; namespaces of 2n and n members, per member) costs at most 3 allocations. On develop before this change it costs 6.23 and the test fails; with it 2.23.

  • TestFQNIsMatchesFQNOf and TestEachReexportGateWalksReexportGates (internal/semantic/symbols): the new helpers answer as FQNOf and ReexportGates do, stop at the first accepting visit, and allocate nothing.

  • Benchmark A/B, six interleaved rounds on the record's machine, pre-regression commit 0af9fcb36 (the parent of the fix(resolve): hide imported memberships that collide by name #1062 merge) in a scratch worktree | develop b60a18c20 | this branch:

    row allocs/op B/op sec/op
    ReferencesCold 44.5k | 6 279k | 1 945k 9.7 MiB | 1 010 MiB | 950 MiB 49 ms | 1 970 ms | 1 383 ms
    RenameWarm 14.9k | 12 410k | 3 740k 1.4 MiB | 1 668 MiB | 1 549 MiB 23 ms | 2 904 ms | 1 928 ms
    ReferencesWarm 1 017 | 1 017 | 1 017 339 KiB all within noise
    WorkspaceUpdate 710.6k all 57.2 MiB all within noise
  • go test ./internal/semantic/... ./internal/frontend/lsp ./internal/workspace/... ./tests/resolve ./internal/check/... pass (the two failures without the pilot and training corpora present pass with them), go build ./..., go vet on the touched packages, gofmt -l . empty, python3 scripts/changelog.py check.

Checklist

  • make test and make lint pass locally (the packages above; the full suite runs in CI)
  • Tests added or updated for the change
  • Documentation extended where it already covers the surface (no user-facing surface changes)
  • Changelog entry added as changes/unreleased/<slug>.<section>.md, not as an edit to CHANGELOG.md
  • baselines regenerated and make docs-counts run if a gate count moved (none moved)
  • No internal work-item labels (waves, slices, F4, K5) in the body, docs, or changelog

Link to Devin session: https://nasa-jpl-demo.devinenterprise.com/sessions/cf22d7a746814d20b2f0eaf11682fef9
Open in Devin Desktop: https://nasa-jpl-demo.devinenterprise.com/desktop/session/cf22d7a746814d20b2f0eaf11682fef9?variant=devin
Requested by: @HuiJun

Co-Authored-By: jason.han <hanhuijun@gmail.com>
@devin-ai-integration

Copy link
Copy Markdown
Contributor Author

I'll fix CI failures and address comments from users with write access. I'll skip comments containing "(aside)".

  • Disable automatic comment, CI, and merge conflict monitoring

devin-ai-integration[bot]

This comment was marked as resolved.

devin-ai-integration Bot and others added 2 commits October 21, 2026 07:34
Co-Authored-By: jason.han <hanhuijun@gmail.com>
…e walk

Co-Authored-By: jason.han <hanhuijun@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant