Uh oh!
There was an error while loading. Please reload this page.
Use AsyncLocal to keep diagnostics instead of ThreadStatic - #16602
Use AsyncLocal to keep diagnostics instead of ThreadStatic#16602majocha wants to merge 5 commits into
AsyncLocal to keep diagnostics instead of ThreadStatic#16602Conversation
❗ Release notes required
|
Uh oh!
There was an error while loading. Please reload this page.
majocha
commented
Jan 29, 2024
Ok, I replaced it with |
vzarytovskii
commented
Jan 29, 2024
I am not sure tbh. References: https://source.dot.net/#System.Private.CoreLib/src/libraries/System.Private.CoreLib/src/System/Threading/AsyncLocal.cs,ef9ce034697240ba |
majocha
commented
Jan 29, 2024
This looks promising. Only one test fails. |
0101
commented
Jan 29, 2024
I will be amazed if this works. 🤞 |
That one error indicates a case where the AsyncLocal fails us where the normal ThreadStatic worked. It's worth it to dive to the bottom of this: |
vzarytovskii
commented
Jan 29, 2024
@majocha getting rid of additional builder abstractions which are essentially just It will make it even easier to move to some state-machines based ones in future (or at least experiment with those). |
0101
commented
Jan 29, 2024
Definitely no objections. The whole point of it was just managing the thread statics. If we can solve it without the extra builder it will be a great improvement. |
majocha
commented
Jan 29, 2024
That one test fail is possibly something related to type providers in the checker. |
majocha
commented
Jan 29, 2024
Comparing the test in question with main, there are two diagnostics missing that should be there:
and
That's some starting point for figuring this out. |
0101
commented
Jan 29, 2024
Uh oh!
There was an error while loading. Please reload this page.
vzarytovskii
commented
Jan 29, 2024
@majocha If you know which test are failing (or were failing rather) specifically, I can run them tomorrow on my Mac in the loop, they're quite fast here. |
majocha
commented
Jan 29, 2024
@vzarytovskii the original test is fixed now, with the holder. A few others popped up. I'll take a closer look later. |
vzarytovskii
commented
Jan 30, 2024
Currently failing tests indicate that we are not (re)storing logger correctly somewhere and it hides issues. |
majocha
commented
Jan 30, 2024
Why does the ExecutionContext set null there? I'm also wondering, the ExecutionContext implementation changed quite a bit over the time: |
vzarytovskii
commented
Jan 30, 2024
The one from the target runtime. In VS it would be the one from .NET 4.7.2, in tests - either the same or 8.0.101 |
majocha
commented
Jan 30, 2024
OK, now I'm stumped. /// Type holds thread-static globals for use by the compiler.typeDiagnosticsThreadStatics=static letdiagnosticsLogger=new AsyncLocal<DiagnosticsLogger option>()static letbuildPhase=new AsyncLocal<BuildPhase option>()
static do
diagnosticsLogger.Value <- Some AssertFalseDiagnosticsLogger
buildPhase.Value <- Some BuildPhase.DefaultPhase
static memberBuildPhasewith get ()= buildPhase.Value |> Option.get
andset v = buildPhase.Value <- Some v
static memberDiagnosticsLoggerwith get ()= diagnosticsLogger.Value |> Option.get
andset v = diagnosticsLogger.Value <- Some vBut it doesn't happen.😶 |
What happens instead? In the following code: openSystem.ThreadingtypeDiagnosticsThreadStatics()=static letdiagnosticsLogger=new AsyncLocal<int option>()static letbuildPhase=new AsyncLocal<int option>()
static do
diagnosticsLogger.Value <- Some (1)
buildPhase.Value <- Some (2)static memberBuildPhasewith get ()= buildPhase.Value |> Option.get
andset v = buildPhase.Value <- Some v
static memberDiagnosticsLoggerwith get ()= diagnosticsLogger.Value |> Option.get
andset v = diagnosticsLogger.Value <- Some vInitialisation will roughly happens in this order (via external (likely) static initializer): DiagnosticsThreadStatics.diagnosticsLogger =new AsyncLocal<FSharpOption<int>>();
DiagnosticsThreadStatics.buildPhase =new AsyncLocal<FSharpOption<int>>();
DiagnosticsThreadStatics.diagnosticsLogger.Value = FSharpOption<int>.Some(1);
DiagnosticsThreadStatics.buildPhase.Value = FSharpOption<int>.Some(2);Both getter and setter for properties will also have an intrinsic initialisation check, which will throw in case if initialization hasn't been done. Keep in mind, that it will be executed once in the async context, static constructor is "running". So if you first accessed |
majocha
commented
Jan 30, 2024
Hm, just the debugger somehow does not break. I'm getting nulls just like you predicted so I was wondering what is going on. |
vzarytovskii
commented
Jan 30, 2024
Since it's kinda "external" init code, I'm not sure we emit any debug points for it. Can be verified by looking at the PDB info, I think. |
majocha
commented
Jan 30, 2024
Thanks @vzarytovskii, I think you clarified it to me now. I've been running in circles about it. This is now green locally, let's see what the CI says. 🤞🤞 |
majocha
commented
Jan 30, 2024
Nope, still bad. |
AsyncLocal to keep diagnostics compile globals instead of ThreadStatic. Removes NodeCodeAsyncLocal to keep diagnostics compile globals instead of ThreadStatic. Removes NodeCodeAsyncLocal to keep diagnostics compile globals instead of ThreadStaticAsyncLocal to keep diagnostics compile globals instead of ThreadStaticAsyncLocal to keep diagnostics instead of ThreadStaticmajocha
commented
Jan 31, 2024
Hard to tell, not noticeable, but I admit I run just the FCS usually, or a subset of failing ones, not the full suite. I'll try to run everything later when I'm on a desktop PC. |
Changes look sane, but I have few concerns/questions.
|
majocha
commented
Jan 31, 2024
I did a quick experiment console app: openSystem.ThreadingopenSystem.Collections.ConcurrenttypeHolder()=letdata= ConcurrentQueue()member_.Add v = data.Enqueue v
member_.GetAll= data |> List.ofSeq
moduleLocals =leta=new AsyncLocal<_>()letinit()= a.Value <- Holder()
Locals.init()letwork tag =async{do! Async.Sleep 10leta= Locals.a.Value
for i in1..5do
a.Add $"{tag} {i}"do! Async.Sleep 10return a.GetAll
}letresult=["A";"B";"C"]|> Seq.map work |> Async.Parallel |> Async.RunSynchronously
printfn "%A" Locals.a.Value.GetAllIt seems to work correctly: |
majocha
commented
Jan 31, 2024
My understanding it that ExecutionContext operates on ThreadPool level, and does not depend on tasks in any way. |
Yeah, that's what I was trying to understand. Regarding the example above, what if you will mix in We use the former a lot in stack guards, which can lead to interesting consequences. |
majocha
commented
Jan 31, 2024
OK, my plan to deal with this is to reset everything, leave NodeCodeBuilder in place and only do the minimal changes required, i.e. DiagnosticsLogger.fs, the test fix in CompilerImports.fs. |
majocha
commented
Jan 31, 2024
openSystem.ThreadingopenSystem.Threading.TasksopenSystem.Collections.ConcurrenttypeHolder(name)=letdata= ConcurrentQueue()member_.Add v = data.Enqueue v
member_.GetAll= name, data |> List.ofSeq
moduleLocals =leta=new AsyncLocal<_>()letinit()= a.Value <- Holder("init")
Locals.init()lett tag i =task{do! Task.Delay 10
Locals.a.Value.Add $"{tag} {i}"}letwork tag =async{do! Async.Sleep 10
Locals.a.Value <- Holder(tag)leta= Locals.a.Value
for i in1..5dodo! Async.SwitchToNewThread()do! t tag i |> Async.AwaitTask
do! Async.Sleep 10return a.GetAll
}letresult=["A";"B";"C"]|> Seq.map work |> Async.Parallel |> Async.RunSynchronously
printfn "%A" result
printfn "%A" Locals.a.Value.GetAllstill works fine, even in FSI. |
majocha
commented
Jan 31, 2024
I think I got it to pass CI: #16634 Is it ok to force push here? |
vzarytovskii
commented
Jan 31, 2024
In the branch? Sure thing. |
majocha
commented
Jan 31, 2024
OK, diff looks reasonably small, fingers crossed. |
0101
commented
Feb 1, 2024
This looks really good! I installed it now and will be trying it out. Wonder if we could come up with some meaningful tests for it. The |
majocha
commented
Feb 1, 2024
NodeCode test we have does it to some extent. There are also some AsyncMemoize tests and BuildGraph tests using DiagnosticsThreadStatics, but a dedicated test would be good. |
majocha
commented
Feb 1, 2024
I'm also working on removal of |
vzarytovskii
commented
Feb 2, 2024
@0101 does this test failure ring a bell by any chance? I recall seeing something similar. If it's just flaky, then we have to either figure out what's going on with it or disable it. |
0101
commented
Feb 2, 2024
|
majocha
commented
Feb 4, 2024
T-Gro
commented
Feb 8, 2024
I propose to close this PR and continue with #16645 only. |




Introduction of Transparent Compiler revealed some instability w.r.t.
DiagnosticsThreadStatics:#16576 (comment)
#16576
#16589
Turns out we used
[<ThreadStatic>]to keep a compilation-globalDiagnosticsLogger, and a customNodeCodeCE to flow it across threads during execution.AsyncLocalis a BCL class that provides exactly that functionality: flowing state along the async execution path.This replaces the
ThreadStaticdiagnostics state forAsyncLocal.NodeCode<'T>is no longer needed. For now we make it into an alias forAsync<'T>, and thenodeCE is an alias forasyncbuilder. To be decided if we want to remove it in this or a separate PR.