Skip to content

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

Closed

Description

@MelbourneDeveloper

Bug

FrameTable.Describe in dotnet/LspProfiler.DotNet.Engine/Symbolizer.cs keeps a managed frame's file only when TraceEvent reports a line above 0. F# compiles a match decision tree (active patterns, union cases) to hidden sequence points, and TraceEvent 3.2.6 reports those as line 0. Every sample in that code therefore arrives without a file, and FunctionKey(file, name) splits one function into a filed entry and a fileless one. This breaks [PROF-DOTNET-CPU-SYMBOLS], which resolves each managed frame to its PDB file:line.

Red reproduction

This runs the real engine (cpu --pid) against SharpLsp's src/sharplsp/tests/fixtures/fsharp-profiling/FsPricingEngine (Release, run parse) for 12 s on macOS arm64. The columns are samples whose stacks hold the function:

Function With file Without file
Csv.parseLine 1592 2507 (61%)
Csv.|Money|_| 1587 34
Csv.|SkuCode|_| 220 32
Csv.|Int|_| 401 30

parseLine (token 0x060000C5) has these sequence points in its portable PDB: 0:L31, hidden at 18, 20, 23, 29, 32, 33, 34, 49, 65, 82 and 86, then 129:L33, 150:L34 and a hidden point at 152. The sampled IL offsets 19, 46, 62, 78, 89 and 112 fall in that hidden run, and TraceEvent returns line 0 for each of them.

Effect

SharpLsp's Windows profiler chunk failed on Nimblesite/SharpLsp#406 (run 39364686929) with "the snapshot already names parseLine in Csv.fs", "parseAll: sampled 7.0%, measured 24.6%" and "shortestPath is hot in Routing.fs". The same job passed unchanged on the next run, so the split share decides pass or fail by timing.

Fix, prototyped outside the tree

When TraceEvent's line is 0, SequencePoint.HiddenLine or missing, take the line from the PDB instead:

  • the last visible sequence point at or before the IL offset;
  • the method's first visible point when there is none before it, as for a prolog (IL −1).

SourceLocator's ModuleSymbols cache already reads these PDBs, so the frame table can reuse it for the whole recording.

With the fix, parseLine has 3882 samples with its file and none without. Its hidden offsets resolve to line 31, the match. Methods that carry no debug information at all stay fileless: the closure Csv+parseAll@39.Invoke and Csv.get_invariant, whose MethodDebugInformation has no document.

Test

Put a hot F# match in HotLeakAppFs. Then assert in the e2e suite that every sampled frame of a method with visible sequence points carries its file and a line of its own.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions