-
Notifications
You must be signed in to change notification settings - Fork 802
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Use AsyncLocal
to keep diagnostics instead of ThreadStatic
#16602
Conversation
❗ Release notes required
|
Ok, I replaced it with |
I am not sure tbh. References: https://source.dot.net/#System.Private.CoreLib/src/libraries/System.Private.CoreLib/src/System/Threading/AsyncLocal.cs,ef9ce034697240ba |
This looks promising. Only one test fails. |
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:
|
@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). |
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. |
That one test fail is possibly something related to type providers in the checker. |
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. |
@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. |
@vzarytovskii the original test is fixed now, with the holder. A few others popped up. I'll take a closer look later. |
Currently failing tests indicate that we are not (re)storing logger correctly somewhere and it hides issues. |
Why does the ExecutionContext set null there? I'm also wondering, the ExecutionContext implementation changed quite a bit over the time: |
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 |
OK, now I'm stumped. /// Type holds thread-static globals for use by the compiler.
type DiagnosticsThreadStatics =
static let diagnosticsLogger = new AsyncLocal<DiagnosticsLogger option>()
static let buildPhase = new AsyncLocal<BuildPhase option>()
static do
diagnosticsLogger.Value <- Some AssertFalseDiagnosticsLogger
buildPhase.Value <- Some BuildPhase.DefaultPhase
static member BuildPhase
with get () = buildPhase.Value |> Option.get
and set v = buildPhase.Value <- Some v
static member DiagnosticsLogger
with get () = diagnosticsLogger.Value |> Option.get
and set v = diagnosticsLogger.Value <- Some v But it doesn't happen.😶 |
What happens instead? In the following code: open System.Threading
type DiagnosticsThreadStatics() =
static let diagnosticsLogger = new AsyncLocal<int option>()
static let buildPhase = new AsyncLocal<int option>()
static do
diagnosticsLogger.Value <- Some (1)
buildPhase.Value <- Some (2)
static member BuildPhase
with get () = buildPhase.Value |> Option.get
and set v = buildPhase.Value <- Some v
static member DiagnosticsLogger
with get () = diagnosticsLogger.Value |> Option.get
and set v = diagnosticsLogger.Value <- Some v Initialisation 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 |
Hm, just the debugger somehow does not break. I'm getting nulls just like you predicted so I was wondering what is going on. |
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. |
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. 🤞🤞 |
Nope, still bad. |
AsyncLocal
to keep diagnostics compile globals instead of ThreadStatic
. Removes NodeCode
AsyncLocal
to keep diagnostics compile globals instead of ThreadStatic
. Removes NodeCode
AsyncLocal
to keep diagnostics compile globals instead of ThreadStatic
AsyncLocal
to keep diagnostics compile globals instead of ThreadStatic
AsyncLocal
to keep diagnostics instead of ThreadStatic
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.
|
I did a quick experiment console app: open System.Threading
open System.Collections.Concurrent
type Holder() =
let data = ConcurrentQueue()
member _.Add v = data.Enqueue v
member _.GetAll = data |> List.ofSeq
module Locals =
let a = new AsyncLocal<_>()
let init () = a.Value <- Holder()
Locals.init()
let work tag =
async {
do! Async.Sleep 10
let a = Locals.a.Value
for i in 1 .. 5 do
a.Add $"{tag} {i}"
do! Async.Sleep 10
return a.GetAll
}
let result = ["A"; "B"; "C"] |> Seq.map work |> Async.Parallel |> Async.RunSynchronously
printfn "%A" Locals.a.Value.GetAll It seems to work correctly:
|
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. |
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. |
open System.Threading
open System.Threading.Tasks
open System.Collections.Concurrent
type Holder(name) =
let data = ConcurrentQueue()
member _.Add v = data.Enqueue v
member _.GetAll = name, data |> List.ofSeq
module Locals =
let a = new AsyncLocal<_>()
let init () = a.Value <- Holder("init")
Locals.init()
let t tag i =
task {
do! Task.Delay 10
Locals.a.Value.Add $"{tag} {i}"
}
let work tag =
async {
do! Async.Sleep 10
Locals.a.Value <- Holder(tag)
let a = Locals.a.Value
for i in 1 .. 5 do
do! Async.SwitchToNewThread()
do! t tag i |> Async.AwaitTask
do! Async.Sleep 10
return a.GetAll
}
let result = ["A"; "B"; "C"] |> Seq.map work |> Async.Parallel |> Async.RunSynchronously
printfn "%A" result
printfn "%A" Locals.a.Value.GetAll still works fine, even in FSI. |
I think I got it to pass CI: #16634 Is it ok to force push here? |
In the branch? Sure thing. |
OK, diff looks reasonably small, fingers crossed. |
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 |
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. |
I'm also working on removal of |
@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. |
|
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 customNodeCode
CE to flow it across threads during execution.AsyncLocal
is a BCL class that provides exactly that functionality: flowing state along the async execution path.This replaces the
ThreadStatic
diagnostics state forAsyncLocal
.NodeCode<'T>
is no longer needed. For now we make it into an alias forAsync<'T>
, and thenode
CE is an alias forasync
builder. To be decided if we want to remove it in this or a separate PR.