Publish NavigableContainer's cases - #20529
xperiandri wants to merge 3 commits into
Conversation
✅ Release notes checked
|
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This comment has been minimized.
This comment has been minimized.
T-Gro
left a comment
There was a problem hiding this comment.
🤖 🕵️ AI review — verify independently.
| and NavigableContainer = | ||
| | File of fileName: string | ||
| | Container of containerType: NavigableContainerType * nameParts: string list * parent: NavigableContainer | ||
| | Container of info: NavigableContainerInfo |
There was a problem hiding this comment.
🤖 🕵️ [P2] Ordered-container comparisons now box each NavigableContainerInfo — 120 bytes per comparison at depth three, versus 0 with the previous representation.
open System
open System.Collections.Generic
open FSharp.Compiler.EditorServices
let chain file =
[1..3] |> List.fold (fun parent _ ->
NavigableContainer.Container {
ContainerType = NavigableContainerType.Module
NameParts = ["M"]
Parent = parent
}) (NavigableContainer.File file)
let a, b = chain "a.fs", chain "b.fs"
let comparer = Comparer<NavigableContainer>.Default
for _ in 1..10000 do comparer.Compare(a, b) |> ignore
let allocated =
let before = GC.GetAllocatedBytesForCurrentThread()
for _ in 1..100000 do comparer.Compare(a, b) |> ignore
GC.GetAllocatedBytesForCurrentThread() - before
printfn "%d bytes" allocated // 12000000 bytesThere was a problem hiding this comment.
The allocation is real and the cause is not the struct record itself: F#'s structural comparison for the union reaches its field through generic comparison, which boxes a struct payload. Implementing IComparable<NavigableContainerInfo> would not remove it either, since generic comparison boxes to IComparable before it ever sees a typed implementation.
What it costs in practice is nothing, because nothing orders these. NavigateTo.GetNavigableItems returns an array in traversal order; the editor reads a container only for FullName and Name, and the ordering Navigate To does is over the strings ComputeSecondarySort builds, never over items or containers. The 12 MB in the repro comes from calling Comparer<NavigableContainer>.Default directly, which no caller does — the comparison exists only because the type is structurally comparable by default.
That leaves two honest ways to make it zero again, and they trade against what this PR is for:
[<NoComparison>]on the container. Ordering containers is meaningless anyway, and this is the change that removes the cost outright — butCompareTois in the surface-area baseline today, so it is a breaking change for anyone who took the default.- Fields back on the case -
Container of containerType: NavigableContainerType * nameParts: string list * parent: NavigableContainer. One allocation, no boxing, fields still named in a match. What goes isinfo.NamePartsoutside a match, andNavigableContainerInfoas a type to pass around.
I would rather not leave a documented allocation in a published API on the strength of "no one calls it", so I will take one of the two. Leaning to 2, since it keeps the comparison the baseline already promises.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
bd019a9 to
2e4c723
Compare
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
|
🔍 Tooling Safety Check — Affects-Design-Time
|
`NavigateTo.GetNavigableItems` hands back items whose `Container` can be read but not constructed: the signature seals the type and hides the `File` and `Container` cases. That is enough to display a result and not enough to carry one across a process boundary, which is what caching navigable items on disk needs — the reader has to rebuild the container it deserialized. Publish both cases. While the shape is still private, replace `Container`'s three-element tuple with a named record so the parts have names at the point of use, and make it a struct: a struct record is laid out inside the case exactly as the tuple was, so this costs nothing. Building a file plus three nested containers a million times allocates 144 bytes per chain either way, where a reference record would take 216. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2e4c723 to
fe5c029
Compare
There was a problem hiding this comment.
Copilot review overview
🟡 Changes recommended
Remove the duplicate adjacent release-note entry.
Review effort: Lite
Findings: 1
Open (1)
What changed in this PR
Publishes NavigableContainer cases and introduces a named struct payload for reconstructing persisted Navigate To results.
Changes:
- Exposes
FileandContainercases. - Adds
NavigableContainerInfo. - Updates implementation, API baseline, and release notes.
| File | Description |
|---|---|
tests/FSharp.Compiler.Service.Tests/FSharp.Compiler.Service.SurfaceArea.netstandard20.bsl |
Updates the API baseline. |
src/Compiler/Service/ServiceNavigation.fsi |
Publishes the new API declarations. |
src/Compiler/Service/ServiceNavigation.fs |
Updates container representation and construction. |
docs/release-notes/.FSharp.Compiler.Service/11.0.100.md |
Documents the API change. |
| * `NavigableContainer` publishes its `File` and `Container` cases, so a consumer of `NavigateTo.GetNavigableItems` can take a container apart and rebuild one — needed to cache navigable items outside the process that produced them. `Container` now carries a `[<Struct>]` record, `NavigableContainerInfo`, in place of its three-element tuple: the fields gain names at no cost, since a struct record is laid out inside the case exactly as the tuple was. | ||
| * `NavigableContainer` publishes its `File` and `Container` cases, so a consumer of `NavigateTo.GetNavigableItems` can take a container apart and rebuild one — needed to cache navigable items outside the process that produced them. `Container` now carries a `[<Struct>]` record, `NavigableContainerInfo`, in place of its three-element tuple: the fields gain names at no cost, since a struct record is laid out inside the case exactly as the tuple was. ([PR #20529](https://github.com/dotnet/fsharp/pull/20529)) |
There was a problem hiding this comment.
Fixed in 5aaa231309: the unlinked copy is gone. Linking the note added a second line instead of editing the first.
The surviving entry also moved out of 11.0.100, which shipped while this PR was in review, into 11.0.200 — the version main now collects notes in.
Linking the note added a copy of it instead of editing the first, and `main` has since opened 11.0.200 for SDK 11.0.200, leaving 11.0.100 shipped. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

NavigateTo.GetNavigableItemsreturns items a consumer can read but not rebuild: the signature sealsNavigableContainerand hides itsFileandContainercases. That is enough to display a result, and notenough to carry one out of the process that produced it. Visual Studio's Navigate To is to keep each file's
navigable items on disk between sessions, as Roslyn keeps its syntax index, and reading them back means
constructing containers.
The change
NavigableContainerpublishesFile of fileName: stringandContainer of info: NavigableContainerInfo. Itsmembers
Type,FullNameandNameare unchanged.Containercarried a three-element tuple,containerType * nameParts * parent. Published as it was, the tuplewould be fixed into the API, so it becomes a record with named fields first:
NavigableContainerInfo { ContainerType; NameParts; Parent }.The record is a
[<Struct>]. A struct record lies inside the case exactly as the tuple's fields did — oneallocation per container — where a reference record adds an object per container and an indirection on every
access. Building a file and three nested containers a million times (x64, .NET 11,
fsi --optimize+):Containerpayload[<Struct>]recordA struct union is not an option:
Parentis recursive.The change is additive: the surface-area baseline only gains lines.
NavigableContainerkeeps its structuralequality and comparison, which the struct record derives as well.
SurfaceAreaTestpasses against the updatedFSharp.Compiler.Service.SurfaceArea.netstandard20.bsl. Theconsumer, a persistent cache for Navigate To in
FSharp.Editor, is a separate PR that builds on this one.🤖 Generated with Claude Code