Skip to content

Profiler: a CPU sample in hidden F# match code keeps its method's file - #19

Merged
MelbourneDeveloper merged 1 commit into
mainfrom
fix/18-hidden-sequence-points
Oct 20, 2026
Merged

MelbourneDeveloper merged 1 commit into
mainfrom
fix/18-hidden-sequence-points

Conversation

@MelbourneDeveloper

Copy link
Copy Markdown
Collaborator

TLDR

A CPU sample in code the compiler hid, such as an F# match's decision tree, now keeps its method's file and takes the visible line before it. A function no longer splits into a located entry and an unlocated twin (#18).

Details

  • Symbolizer.cs: FrameTable.Located keeps TraceEvent's file:line when the line is visible. It asks the PDB instead when TraceEvent reports line 0, SequencePoint.HiddenLine, or no source at all (a prolog, IL −1). The answer is the method's nearest visible sequence point at or before the IL offset, or its first visible point when none comes before. A method with no visible point, such as a compiler-generated closure or a union constructor, stays unlocated as before. Symbolizer and FrameTable now own one SourceLocator for the whole recording, and CpuCommand disposes it.
  • SourceLocator.cs: the new Line(module, methodToken, ilOffset) reuses the per-module PDB cache that memory locations already use. MethodPoints returns nothing for a token that names no method in the PDB.
  • Fixture: HotLeakAppFs/Matching.fs adds Matching.classify, a non-inlined match over a struct active pattern whose tests compile to hidden sequence points. The line protocol gains classify <count>. Program.fs keeps its line numbers, so every existing line constant still holds.
  • Spec and plan: [PROF-DOTNET-CPU-SYMBOLS] now states the rule. The plan's "a hidden sequence point is reported without a location" now points to this defect, which is ticked.

How Do The Automated Tests Prove It Works?

  • cpu_samples_in_fsharp_match_code_keep_their_file (F# e2e) profiles three classify 50000000 rounds.
    • Before the fix it failed: HotLeakAppFs.Matching.classify(int32) came back twice. One entry held 948 samples (63.4%) in Matching.fs; the other was an unlocated twin of 179 samples (12.0%), all self time in the decision tree.
    • After the fix it passes, unchanged: one entry, in Matching.fs, with its line and a hot line inside the match (lines 17–22).
  • Against SharpLsp's FsPricingEngine (run parse, 12 s), Csv.parseLine went from 1592 located and 2507 unlocated samples to 4708 located and none unlocated. Under run mixed, every named function is located. That split is what failed SharpLsp's Windows profiler suite on Razor review fixes (#399–#407) and Blazor and Razor docs, film and post SharpLsp#406 (run 39364686929).
  • Local CI passes:
    • make fmt CHECK=1
    • profiler UI build and test:paths
    • make lint
    • make test: 413/413 tests; lsp-profiler at 93.29% and lsp-profiler-dotnet at 92.60% against their 90% thresholds
    • make build

Closes #18.

@MelbourneDeveloper
MelbourneDeveloper merged commit 3cd5896 into main Oct 20, 2026
3 checks passed
@MelbourneDeveloper
MelbourneDeveloper deleted the fix/18-hidden-sequence-points branch October 20, 2026 04:48
MelbourneDeveloper added a commit to Nimblesite/SharpLsp that referenced this pull request Oct 20, 2026
…op is sampled where it runs (#409)

## TLDR
Fixes two defects that made the F# profiler suites flaky (#408). An F#
CPU sample in a `match`'s decision tree now keeps its method's file, and
FsOrderLedger's burn loop is now sampled where it actually runs.

## Details
- **`lspkit` e6e4172 → 3cd5896 (Nimblesite/lspkit#19, fixes
lspkit#18):**
- The .NET engine's `Symbolizer` gives a frame on a hidden sequence
point its method's nearest visible sequence point, or the method's
first, read from the PDB through `SourceLocator`. A method with no
visible point, such as a compiler-generated closure, stays unlocated.
- Before this, such frames lost their file, so a function split into a
located hot function and an unlocated twin. That was 61% of
FsPricingEngine's `Csv.parseLine`, and it failed the Windows profiler
chunk on #406 (run 39364686929).
  - lspkit's spec ([PROF-DOTNET-CPU-SYMBOLS]) and plan state the rule.
- Its fixture `HotLeakAppFs` gains `Matching.fs` and a `classify`
command. The Program.fs lines SharpLsp's `profiler-fsharp-e2e` pins (up
to 97) are unchanged.
- **`FsOrderLedger` `Gate.fs` and `Program.fs`:** the burn tick no
longer takes the process-wide gate.
  - It only reads (`priceAll` snapshots under the ledger's own lock).
- EventPipe's sampler stops a thread at its next safe point, so the
uncontended lock call took 98% of the burn thread's samples.
dotnet-counters showed 0 lock contentions per second.
- That left `priceAll` at the 2% hot-function threshold, and "the burn
loop is hot" failed two runs in three on macOS.
- Line counts are unchanged, so `PRICE_ALL` (Ledger.fs 63–70) still
holds.
- **`docs/plans/PROFILER-PLAN.md`:** both defects join the F#
real-program list.

## How Do The Automated Tests Prove It Works?
- **lspkit's `cpu_samples_in_fsharp_match_code_keep_their_file`:**
before the fix, `Matching.classify` came back twice, with 948 samples
located and 179 unlocated. After the fix it passes unchanged. lspkit CI
is green on the PR (run 39371497743) and on main (run 39371510065).
- **FsPricingEngine through this branch's published engine (`run
parse`):** `Csv.parseLine` went from 1592 located and 2507 unlocated
samples to 3271 located and none unlocated.
- **"the burn loop is the hotspot while memory tracks the same
program"** (`profiler-fsharp-soak-cpu-e2e`):
- Unchanged test. It failed two of three local runs before the fixture
fix, both on "the burn loop is hot".
  - It passes three of three after it.
- A direct engine trace puts `priceAll` on 100% of the burn thread's
samples, up from 1.6%.
- **The full `profiler` VS Code chunk passes locally on macOS against
this branch: 323/323.** This PR's pipeline re-checks it on Linux and
Windows, where the F# CPU and soak suites assert each named function's
file.

Closes #408.
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.

CPU profiler drops a frame's file when its IL offset is on a hidden sequence point (F# match code)

1 participant