Skip to content

Commit 4c68fe8

Browse files
authored
Fix excessive StackGuard thread jumps (#18971)
1 parent cdcd108 commit 4c68fe8

File tree

19 files changed

+83
-117
lines changed

19 files changed

+83
-117
lines changed

docs/large-inputs-and-stack-overflows.md

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -89,13 +89,12 @@ The previous example is considered incomplete, because arbitrary _combinations_
8989

9090
## Stack Guards
9191

92-
The `StackGuard` type is used to count synchronous recursive processing and move to a new thread if a limit is reached. Compilation globals are re-installed. Sample:
92+
The `StackGuard` type is used to move to a new thread if there is no sufficient stack space for a synchronous recursive call. Compilation globals are re-installed. Sample:
9393

9494
```fsharp
95-
let TcStackGuardDepth = StackGuard.GetDepthOption "Tc"
9695
9796
...
98-
stackGuard = StackGuard(TcMaxStackGuardDepth)
97+
stackGuard = StackGuard("TcExpr")
9998
10099
let rec ....
101100
@@ -108,10 +107,17 @@ and TcExpr cenv ty (env: TcEnv) tpenv (expr: SynExpr) =
108107
109108
```
110109

111-
Note stack guarding doesn't result in a tailcall so will appear in recursive stack frames, because a counter must be decremented after the call. This is used systematically for recursive processing of:
110+
Note stack guarding doesn't result in a tailcall so will appear in recursive stack frames, because a counter must be decremented after the call.
112111

113-
* SyntaxTree SynExpr
114-
* TypedTree Expr
112+
Compiling with `--times` option will show a summary if any thread switches were made due to stack guarding:
115113

116-
We don't use it for other inputs.
114+
115+
```
116+
StackGuard jumps:
117+
-----------------------------------------------------
118+
| caller | source | jumps | min depth |
119+
|--------|----------------------|-------|-----------|
120+
| exprF | TypedTreeOps.fs:7444 | 25 | 1601 |
121+
-----------------------------------------------------
122+
```
117123

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
### Fixed
2+
* Fix excessive StackGuard thread jumping ([PR #18971](https://github.com/dotnet/fsharp/pull/18971))
3+
4+
### Added
5+
6+
### Changed
7+
8+
### Breaking Changes

src/Compiler/AbstractIL/ilwritepdb.fs

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1023,11 +1023,7 @@ let rec pushShadowedLocals (stackGuard: StackGuard) (localsToPush: PdbLocalVar[]
10231023
// adding the text " (shadowed)" to the names of those with name conflicts.
10241024
let unshadowScopes rootScope =
10251025
// Avoid stack overflow when writing linearly nested scopes
1026-
let UnshadowScopesStackGuardDepth =
1027-
GetEnvInteger "FSHARP_ILPdb_UnshadowScopes_StackGuardDepth" 100
1028-
1029-
let stackGuard =
1030-
StackGuard(UnshadowScopesStackGuardDepth, "ILPdbWriter.unshadowScopes")
1026+
let stackGuard = StackGuard("ILPdbWriter.unshadowScopes")
10311027

10321028
let result, _ = pushShadowedLocals stackGuard [||] rootScope
10331029
result

src/Compiler/Checking/CheckBasics.fs

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -28,12 +28,6 @@ open FSharp.Compiler.TypedTreeOps
2828
open FSharp.Compiler.TypeProviders
2929
#endif
3030

31-
#if DEBUG
32-
let TcStackGuardDepth = GetEnvInteger "FSHARP_TcStackGuardDepth" 40
33-
#else
34-
let TcStackGuardDepth = GetEnvInteger "FSHARP_TcStackGuardDepth" 80
35-
#endif
36-
3731
/// The ValReprInfo for a value, except the number of typars is not yet inferred
3832
type PrelimValReprInfo =
3933
| PrelimValReprInfo of
@@ -353,7 +347,7 @@ type TcFileState =
353347
{ g = g
354348
amap = amap
355349
recUses = ValMultiMap<_>.Empty
356-
stackGuard = StackGuard(TcStackGuardDepth, "TcFileState")
350+
stackGuard = StackGuard("TcFileState")
357351
createsGeneratedProvidedTypes = false
358352
thisCcu = thisCcu
359353
isScript = isScript

src/Compiler/Checking/CheckIncrementalClasses.fs

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,6 @@ open FSharp.Compiler.TypeHierarchy
2828

2929
type cenv = TcFileState
3030

31-
let TcClassRewriteStackGuardDepth = StackGuard.GetDepthOption "TcClassRewrite"
32-
3331
exception ParameterlessStructCtor of range: range
3432

3533
/// Represents a single group of bindings in a class with an implicit constructor
@@ -579,7 +577,7 @@ type IncrClassReprInfo =
579577
PostTransform = (fun _ -> None)
580578
PreInterceptBinding = None
581579
RewriteQuotations = true
582-
StackGuard = StackGuard(TcClassRewriteStackGuardDepth, "FixupIncrClassExprPhase2C") } expr
580+
StackGuard = StackGuard("FixupIncrClassExprPhase2C") } expr
583581

584582
type IncrClassConstructionBindingsPhase2C =
585583
| Phase2CBindings of IncrClassBindingGroup list

src/Compiler/Checking/FindUnsolved.fs

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,6 @@ open FSharp.Compiler.TypeRelations
1717

1818
type env = | NoEnv
1919

20-
let FindUnsolvedStackGuardDepth = StackGuard.GetDepthOption "FindUnsolved"
21-
2220
/// The environment and collector
2321
type cenv =
2422
{ g: TcGlobals
@@ -318,7 +316,7 @@ let UnsolvedTyparsOfModuleDef g amap denv mdef extraAttribs =
318316
amap=amap
319317
denv=denv
320318
unsolved = []
321-
stackGuard = StackGuard(FindUnsolvedStackGuardDepth, "UnsolvedTyparsOfModuleDef") }
319+
stackGuard = StackGuard("UnsolvedTyparsOfModuleDef") }
322320
accModuleOrNamespaceDef cenv NoEnv mdef
323321
accAttribs cenv NoEnv extraAttribs
324322
List.rev cenv.unsolved

src/Compiler/Checking/PostInferenceChecks.fs

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -57,8 +57,6 @@ open Import
5757
// b) a lambda expression - rejected.
5858
// c) none of the above - rejected as when checking outmost expressions.
5959

60-
let PostInferenceChecksStackGuardDepth = GetEnvInteger "FSHARP_PostInferenceChecks" 50
61-
6260
//--------------------------------------------------------------------------
6361
// check environment
6462
//--------------------------------------------------------------------------
@@ -2691,7 +2689,7 @@ let CheckImplFile (g, amap, reportErrors, infoReader, internalsVisibleToPaths, v
26912689
reportErrors = reportErrors
26922690
boundVals = Dictionary<_, _>(100, HashIdentity.Structural)
26932691
limitVals = Dictionary<_, _>(100, HashIdentity.Structural)
2694-
stackGuard = StackGuard(PostInferenceChecksStackGuardDepth, "CheckImplFile")
2692+
stackGuard = StackGuard("CheckImplFile")
26952693
potentialUnboundUsesOfVals = Map.empty
26962694
anonRecdTypes = StampMap.Empty
26972695
usesQuotations = false

src/Compiler/Checking/TailCallChecks.fs

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,6 @@ open FSharp.Compiler.TypedTreeBasics
1616
open FSharp.Compiler.TypedTreeOps
1717
open FSharp.Compiler.TypeRelations
1818

19-
let PostInferenceChecksStackGuardDepth = GetEnvInteger "FSHARP_TailCallChecks" 50
20-
2119
[<return: Struct>]
2220
let (|ValUseAtApp|_|) e =
2321
match e with
@@ -887,7 +885,7 @@ let CheckImplFile (g: TcGlobals, amap, reportErrors, implFileContents) =
887885
let cenv =
888886
{
889887
g = g
890-
stackGuard = StackGuard(PostInferenceChecksStackGuardDepth, "CheckImplFile")
888+
stackGuard = StackGuard("CheckImplFile")
891889
amap = amap
892890
mustTailCall = Zset.empty valOrder
893891
hasPinnedLocals = false

src/Compiler/CodeGen/IlxGen.fs

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -45,10 +45,7 @@ open FSharp.Compiler.TypedTreeOps.DebugPrint
4545
open FSharp.Compiler.TypeHierarchy
4646
open FSharp.Compiler.TypeRelations
4747

48-
let IlxGenStackGuardDepth = StackGuard.GetDepthOption "IlxGen"
49-
50-
let getEmptyStackGuard () =
51-
StackGuard(IlxGenStackGuardDepth, "IlxAssemblyGenerator")
48+
let getEmptyStackGuard () = StackGuard("IlxAssemblyGenerator")
5249

5350
let IsNonErasedTypar (tp: Typar) = not tp.IsErased
5451

src/Compiler/Facilities/DiagnosticsLogger.fs

Lines changed: 38 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -879,17 +879,18 @@ module StackGuardMetrics =
879879
description = "Tracks the number of times the stack guard has jumped to a new thread"
880880
)
881881

882-
let countJump memberName location =
882+
let countJump memberName location depth =
883883
let tags =
884884
let mutable tags = TagList()
885885
tags.Add(Activity.Tags.callerMemberName, memberName)
886886
tags.Add("source", location)
887+
tags.Add("depth", depth)
887888
tags
888889

889890
jumpCounter.Add(1L, &tags)
890891

891892
// Used by the self-listener.
892-
let jumpsByFunctionName = ConcurrentDictionary<_, int64 ref>()
893+
let dataByFunctionName = ConcurrentDictionary<_, int64 ref * int ref>()
893894

894895
let Listen () =
895896
let listener = new Metrics.MeterListener()
@@ -899,20 +900,26 @@ module StackGuardMetrics =
899900
listener.SetMeasurementEventCallback(fun _ v tags _ ->
900901
let memberName = nonNull tags[0].Value :?> string
901902
let source = nonNull tags[1].Value :?> string
902-
let counter = jumpsByFunctionName.GetOrAdd((memberName, source), fun _ -> ref 0L)
903-
Interlocked.Add(counter, v) |> ignore)
903+
let depth = nonNull tags[2].Value :?> int
904+
905+
let counter, minDepth =
906+
dataByFunctionName.GetOrAdd((memberName, source), fun _ -> ref 0L, ref Int32.MaxValue)
907+
908+
counter.Value <- counter.Value + v
909+
minDepth.Value <- min depth minDepth.Value)
904910

905911
listener.Start()
906912
listener :> IDisposable
907913

908914
let StatsToString () =
909-
let headers = [ "caller"; "source"; "jumps" ]
915+
let headers = [ "caller"; "source"; "jumps"; "min depth" ]
910916

911917
let data =
912918
[
913-
for kvp in jumpsByFunctionName do
919+
for kvp in dataByFunctionName do
914920
let (memberName, source) = kvp.Key
915-
[ memberName; source; string kvp.Value.Value ]
921+
let jumps, depth = kvp.Value
922+
[ memberName; source; string jumps.Value; string depth.Value ]
916923
]
917924

918925
if List.isEmpty data then
@@ -930,9 +937,23 @@ module StackGuardMetrics =
930937
}
931938

932939
/// Guard against depth of expression nesting, by moving to new stack when a maximum depth is reached
933-
type StackGuard(maxDepth: int, name: string) =
940+
type StackGuard(name: string) =
941+
942+
let depth = new ThreadLocal<int>()
934943

935-
let mutable depth = 1
944+
static member inline IsStackSufficient() =
945+
946+
// We single out netstandard2.0 because it lacks the non-throwing TryEnsureSufficientExecutionStack
947+
// TODO: Get rid of this throwing version as soon as we can.
948+
#if NETSTANDARD2_0
949+
try
950+
RuntimeHelpers.EnsureSufficientExecutionStack()
951+
true
952+
with :? InsufficientExecutionStackException ->
953+
false
954+
#else
955+
RuntimeHelpers.TryEnsureSufficientExecutionStack()
956+
#endif
936957

937958
[<DebuggerHidden; DebuggerStepThrough>]
938959
member _.Guard
@@ -943,40 +964,30 @@ type StackGuard(maxDepth: int, name: string) =
943964
[<CallerLineNumber; Optional; DefaultParameterValue(0)>] line: int
944965
) =
945966

946-
depth <- depth + 1
967+
depth.Value <- depth.Value + 1
947968

948969
try
949-
if depth % maxDepth = 0 then
950-
970+
if StackGuard.IsStackSufficient() then
971+
f ()
972+
else
951973
let fileName = System.IO.Path.GetFileName(path)
974+
let depthWhenJump = depth.Value
952975

953-
StackGuardMetrics.countJump memberName $"{fileName}:{line}"
976+
StackGuardMetrics.countJump memberName $"{fileName}:{line}" depthWhenJump
954977

955978
async {
956979
do! Async.SwitchToNewThread()
957-
Thread.CurrentThread.Name <- $"F# Extra Compilation Thread for {name} (depth {depth})"
980+
Thread.CurrentThread.Name <- $"F# Extra Compilation Thread for {name} (depth {depthWhenJump})"
958981
return f ()
959982
}
960983
|> Async.RunImmediate
961-
else
962-
f ()
963984
finally
964-
depth <- depth - 1
985+
depth.Value <- depth.Value - 1
965986

966987
[<DebuggerHidden; DebuggerStepThrough>]
967988
member x.GuardCancellable(original: Cancellable<'T>) =
968989
Cancellable(fun ct -> x.Guard(fun () -> Cancellable.run ct original))
969990

970-
static member val DefaultDepth =
971-
#if DEBUG
972-
GetEnvInteger "FSHARP_DefaultStackGuardDepth" 50
973-
#else
974-
GetEnvInteger "FSHARP_DefaultStackGuardDepth" 100
975-
#endif
976-
977-
static member GetDepthOption(name: string) =
978-
GetEnvInteger ("FSHARP_" + name + "StackGuardDepth") StackGuard.DefaultDepth
979-
980991
// UseMultipleDiagnosticLoggers in ParseAndCheckProject.fs provides similar functionality.
981992
// We should probably adapt and reuse that code.
982993
module MultipleDiagnosticsLoggers =

0 commit comments

Comments
 (0)