dotnet / fsharp

The F# compiler, F# core library, F# language service, and F# tooling integration for Visual Studio
https://dotnet.microsoft.com/languages/fsharp
MIT License
3.83k stars 773 forks source link

Improve async stack traces #2741

Open vasily-kirichenko opened 7 years ago

vasily-kirichenko commented 7 years ago

C#

image

F#

image

  1. The stack trace is completely lost between main and bazz.
  2. ConsoleApplication1.exe!Program.bazz@6-1.Invoke(Microsoft.FSharp.Core.Unit unitVar = null)

Why not ConsoleApplication1.exe!Program.bazz@6-1()? Why that unit and Invoke noise? Is this something we can fix on VFT side?

3.

FSharp.Core.dll!Microsoft.FSharp.Control.AsyncBuilderImpl.callA@841<int, System.__Canon>.Invoke(Microsoft.FSharp.Control.AsyncParams<int> args = {Microsoft.FSharp.Control.AsyncParams<int>})   Unknown
FSharp.Core.dll!<StartupCode$FSharp-Core>.$Control.loop@427-51(Microsoft.FSharp.Control.Trampoline this = {Microsoft.FSharp.Control.Trampoline}, Microsoft.FSharp.Core.FSharpFunc<Microsoft.FSharp.Core.Unit, Microsoft.FSharp.Control.FakeUnitValue> action)   Unknown
FSharp.Core.dll!Microsoft.FSharp.Control.Trampoline.ExecuteAction(Microsoft.FSharp.Core.FSharpFunc<Microsoft.FSharp.Core.Unit, Microsoft.FSharp.Control.FakeUnitValue> firstAction) Unknown

Why it's shown at all? Can we hide it, always?

4.

FSharp.Core.dll!Microsoft.FSharp.Control.CancellationTokenOps.RunSynchronouslyInCurrentThread<int>(System.Threading.CancellationToken token, Microsoft.FSharp.Control.FSharpAsync<int> computation) Unknown
FSharp.Core.dll!Microsoft.FSharp.Control.CancellationTokenOps.RunSynchronously<int>(System.Threading.CancellationToken token, Microsoft.FSharp.Control.FSharpAsync<int> computation, Microsoft.FSharp.Core.FSharpOption<int> timeout)   Unknown
FSharp.Core.dll!Microsoft.FSharp.Control.FSharpAsync.RunSynchronously<int>(Microsoft.FSharp.Control.FSharpAsync<int> computation, Microsoft.FSharp.Core.FSharpOption<int> timeout, Microsoft.FSharp.Core.FSharpOption<System.Threading.CancellationToken> cancellationToken)    Unknown
ConsoleApplication1.exe!Program.main(string[] argv = {string[0]}) Line 23   F#

Looks OK, but too verbose.

vasily-kirichenko commented 4 years ago

@abelbraaksma as you wish, but the stack traces as useless as before.

abelbraaksma commented 4 years ago

@vasily-kirichenko, it's not my wish, I'm just not certain of the state of the improvements. If they aren't useful still, we should keep this open.

NinoFloris commented 4 years ago

@vasily-kirichenko Task is not so different in this regard

Try the following example based on yours. You need <PackageReference Include="Ply" Version="0.1.8" /> for the task CE.

open System
open System.Threading.Tasks
open FSharp.Control.Tasks.Builders

let executeReader() =
    task {
        do! Task.Yield()
        printfn "%s" (System.Diagnostics.StackTrace(true).ToString())
        return Ok [1..10]
    }

let exec (count: int) =
    task {
        return! executeReader()
    }

let getTasks() =
    task {
        return! exec 1
    }

let loadTasksAsync() =
    task {
        return! getTasks()
    }

[<EntryPoint>]
let main _ =
    (task {
        let! _ = loadTasksAsync()
        return ()
    }).Result

    Console.ReadLine() |> ignore
    0

output

   at Program.executeReader@72-1.Invoke(Unit _arg1) in /Users/nfloris/Projects/rep/Program.fs:line 72
   at Program.executeReader@71-2.Invoke(Unit x) in /Users/nfloris/Projects/rep/Program.fs:line 71
   at Ply.TplPrimitives.ContinuationStateMachine`1.System-Runtime-CompilerServices-IAsyncStateMachine-MoveNext()
   at System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1.AsyncStateMachineBox`1.ExecutionContextCallback(Object s)
   at System.Threading.ExecutionContext.RunFromThreadPoolDispatchLoop(Thread threadPoolThread, ExecutionContext executionContext, ContextCallback callback, Object state)
   at System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1.AsyncStateMachineBox`1.MoveNext(Thread threadPoolThread)
   at System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1.AsyncStateMachineBox`1.ExecuteFromThreadPool(Thread threadPoolThread)
   at System.Threading.ThreadPoolWorkQueue.Dispatch()
   at System.Threading._ThreadPoolWaitCallback.PerformWaitCallback()

No trace of exec, getTasks, or loadTasksAsync for Tasks either. However if you remove do! Task.Yield() from executeReader the stacktrace will contain all information. As Task runs binds inline if it can by removing the forced yield causes the execution to never unwind back up to the trampoline (Ply.TplPrimitives.ContinuationStateMachine here). There was nothing intrinsically async to do. Permitting inline binds is a fundamental difference between Tasks and Async today, Async could definitely start doing this within safe limits.

However the real thing to understand here is the difference between StackTrace and how async exceptions build a stacktrace. Task and as far as I know Async (EDIT: nope it does not at all) both handle exception traces very well and will return the full trace, this comes down to a difference in how the trace is constructed.

StackTrace only has the native frames to inspect, for an async function that yields by unwinding its (native) stack completely this will be fairly useless. Regardless of Async or Task allowing continuations to run inline, a yield will destroy the native frames. For exceptions this path is a bit different; to understand it you have to think of the async operations representing a logical stack, one that after yielding never exists in full as a native stack again, but we can reify it with cooperation of the async primitive!

I will use Task as an example as I'm a bit hazy on the details for Async but the general concept should transfer well. The difference between Async and Task here is that instead of handing off the result or exception to the continuation like Async does (more or less), Task instead carries any result or exception around as a value on itself, just as it also carries any continuations it is expected to run once completed, it is a mutable thing.

So as our Task is done it calls into the continuation we registered earlier as part of yielding — Note, in the case of async await, the continuation always ends up in the same method that was initially responsible for calling the method that produced the Task that it then awaited on, meaning we can use this to reconstruct our lost stack! Exceptions to this are cases like Tasks that were started by one method, stored in a field, and awaited by another, fairly uncommon cases. Though the general rule is important for what comes next. — we are expected to inspect the Task for a result (if you are somewhat familiar this is Awaiter.GetResult()). Right at this moment the failed Task has the opportunity to help reify a bit of the stack frames that were lost, it does this by throwing an exception via ExceptionDispatchInfo.Throw this serves to keep the original trace intact instead of replacing all frames like raise ex would.

As our continuation method is itself a Task returning method (virality of async) we are again expected to store any exceptions, we call GetResult on the Task, catch any exception and this cause our current frame to be appended before we store this on the Task. As it is stored it completes the Task causing it to call its own continuation which catches the same exception and adds another frame, stores it, and so on and so forth. As the logical stack is unwound forward through the continuations the trace accumulates all frames, et voilà you have a full trace again.

EDIT: This is however distinctly different from how Async works today, I'm looking into ways to get closer to what Task does but it may not be backwards compatible, to be continued.

Happy to answer any questions if something is unclear!

natalie-o-perret commented 4 years ago

@vasily-kirichenko, it's not my wish, I'm just not certain of the state of the improvements. If they aren't useful still, we should keep this open.

let rec odd (n : int) =
    async {
        if n = 0 then return false
        else
            return! even (n - 1)
    }
and even (n : int) =
    async {
        if n = 0 then return failwith "bug!"
        else
            return! odd (n - 1)
    }
Unhandled exception. System.Exception: bug!
   at Program.even@10.Invoke(Unit unitVar) in C:\Users\Michelle\Desktop\Work\FSharpPlayground\FSharpPlayground\Program.fs:line 10
   at Microsoft.FSharp.Control.AsyncPrimitives.CallThenInvoke[T,TResult](AsyncActivation`1 ctxt, TResult result1, FSharpFunc`2 part2) in E:\A\_work\130\s\src\
fsharp\FSharp.Core\async.fs:line 398
   at Microsoft.FSharp.Control.Trampoline.Execute(FSharpFunc`2 firstAction) in E:\A\_work\130\s\src\fsharp\FSharp.Core\async.fs:line 109
--- End of stack trace from previous location where exception was thrown ---
   at Microsoft.FSharp.Control.AsyncResult`1.Commit() in E:\A\_work\130\s\src\fsharp\FSharp.Core\async.fs:line 350
   at Microsoft.FSharp.Control.AsyncPrimitives.RunSynchronouslyInCurrentThread[a](CancellationToken cancellationToken, FSharpAsync`1 computation) in E:\A\_wor
k\130\s\src\fsharp\FSharp.Core\async.fs:line 882
   at Microsoft.FSharp.Control.AsyncPrimitives.RunSynchronously[T](CancellationToken cancellationToken, FSharpAsync`1 computation, FSharpOption`1 timeout) in
E:\A\_work\130\s\src\fsharp\FSharp.Core\async.fs:line 890
   at Microsoft.FSharp.Control.FSharpAsync.RunSynchronously[T](FSharpAsync`1 computation, FSharpOption`1 timeout, FSharpOption`1 cancellationToken) in E:\A\_w
ork\130\s\src\fsharp\FSharp.Core\async.fs:line 1154
   at Program.main(String[] _arg1) in C:\Users\Michelle\Desktop\Work\FSharpPlayground\FSharpPlayground\Program.fs:line 17

I think that as long as that simple use case (along with any other relevant cases) is not covered we can consider that we should keep that issue opened.

NinoFloris commented 3 years ago

I've just fixed the last remaining piece of this issue in Ply, meaning code like this:

module PlyProgram

open System.Threading.Tasks
open FSharp.Control.Tasks
open System

let findDingbobbleById(x: int) = vtask {
    let! x = Task.Yield() // Force the Task to unwind the native stack
    return invalidOp "Oh no"
}

let dingbobbleFeature() = vtask {
    return! findDingbobbleById 1
}

let dingbobbleRoute() = vtask {
    return! dingbobbleFeature()
}

[<EntryPoint>]
let main argv =
    try dingbobbleRoute().GetAwaiter().GetResult() |> ignore
    with ex -> Console.WriteLine(ex)
    0

will show a trace like:

System.InvalidOperationException: Oh no
   at PlyProgram.findDingbobbleById@9-1.Invoke(FSharpResult`2 r)
   at Ply.TplPrimitives.TplAwaitable`4.GetNext() in /Users/nfloris/Projects/Crowded/Ply/Ply.fs:line 158
   at Ply.TplPrimitives.ContinuationStateMachine`1.System-Runtime-CompilerServices-IAsyncStateMachine-MoveNext() in /Users/nfloris/Projects/Crowded/Ply/Ply.fs:line 113
   at Ply.TplPrimitives.ValueTaskAwaiterMethods`1.Ply-TplPrimitives-IAwaiterMethods`2-GetResult(ValueTaskAwaiter`1& awt) in /Users/nfloris/Projects/Crowded/Ply/Ply.fs:line 392
   at Ply.TplPrimitives.TplAwaitable`4.GetNext() in /Users/nfloris/Projects/Crowded/Ply/Ply.fs:line 155
--- End of stack trace from previous location where exception was thrown ---
   at PlyProgram.dingbobbleFeature@14-1.Invoke(FSharpResult`2 r)
   at Ply.TplPrimitives.TplAwaitable`4.GetNext() in /Users/nfloris/Projects/Crowded/Ply/Ply.fs:line 158
   at Ply.TplPrimitives.ContinuationStateMachine`1.System-Runtime-CompilerServices-IAsyncStateMachine-MoveNext() in /Users/nfloris/Projects/Crowded/Ply/Ply.fs:line 113
   at Ply.TplPrimitives.ValueTaskAwaiterMethods`1.Ply-TplPrimitives-IAwaiterMethods`2-GetResult(ValueTaskAwaiter`1& awt) in /Users/nfloris/Projects/Crowded/Ply/Ply.fs:line 392
   at Ply.TplPrimitives.TplAwaitable`4.GetNext() in /Users/nfloris/Projects/Crowded/Ply/Ply.fs:line 155
--- End of stack trace from previous location where exception was thrown ---
   at PlyProgram.dingbobbleRoute@18-1.Invoke(FSharpResult`2 r)
   at Ply.TplPrimitives.TplAwaitable`4.GetNext() in /Users/nfloris/Projects/Crowded/Ply/Ply.fs:line 158
   at Ply.TplPrimitives.ContinuationStateMachine`1.System-Runtime-CompilerServices-IAsyncStateMachine-MoveNext() in /Users/nfloris/Projects/Crowded/Ply/Ply.fs:line 113
   at PlyProgram.main(String[] argv) in /Users/nfloris/Projects/Crowded/Ply/Program.fs:line 90

The changes to do this were fairly small (though Async is probably quite a bit more involved) https://github.com/crowded/ply/commit/eae21dbebd0f6d3ebd9021ad2ca31c017831faab

When I have some spare time I'm going to prototype this approach for Async as well.

dsyme commented 3 years ago

I checked some of the cases on this thread

asyncResult

For the case here: https://github.com/dotnet/fsharp/issues/2741#issuecomment-387796159, the example CE code needs two changes to activate marginal improvements which at least mean the stack shows causality

  1. The Bind method should be inlined

  2. "Tailcalls" on ReturnFrom should be disabled in DEBUG mode, e.g.

    member inline __.ReturnFrom (asyncResult : Async<Result<'T, 'Error>>) =
#if DEBUG
        async { let! res = asyncResult in return res }
#else
        asyncResult
#endif

This then gives marginally improved stack traces that at least show the correct causal trace in terms of lines of code:

image

However the variables available in stack frames do not show values correctly (because async is fundamentally two-phase execution - create then phase - and the closures generated in the "create" phase lose the debug environment association in the "run" phase).

On discussion with @TIHan believe we should add a general feature for CEs authors where they can request that the debug environment be fully captured by within crucial closures that represent the execution stacks. To give an example from the F# compiler:

    let inline bind f comp1 = 
       Cancellable (fun ct -> 
            FSharp.Core.CompilerServices.RuntimeHelpers.InlineDebbugerEnvironment();
            match run ct comp1 with 
            | ValueOrCancelled.Value v1 -> run ct (f v1) 
            | ValueOrCancelled.Cancelled err1 -> ValueOrCancelled.Cancelled err1)

This mechanism would inject fake bindings in debug mode to rebind the full captured environment at the point of final inlining.

async add/even

The case here https://github.com/dotnet/fsharp/issues/2741#issuecomment-635055634 involves tailcalling asyncs. In DEBUG mode the F# async apparatus still takes tailcalls. This can be manually disabled through the same technique:

let rec odd (n : int) =
    async {
        if n = 0 then return false
        else
            let! res = even (n - 1)
            return res
    }
and even (n : int) =
    async {
        if n = 0 then return failwith "bug!"
        else
            let! res = odd (n - 1)
            return res
    }

even 100 |> Async.RunSynchronously

giving a long stack trace showing causality

Unhandled Exception: System.TypeInitializationException: The type initializer for '<StartupCode$a>.$A' threw an exception. ---> System.Exception: bug!
   at A.even@106.Invoke(Unit unitVar) in C:\GitHub\dsyme\fsharp\a.fs:line 106
   at Microsoft.FSharp.Control.AsyncPrimitives.CallThenInvoke[T,TResult](AsyncActivation`1 ctxt, TResult result1, FSharpFunc`2 part2)
   at A.odd@101-4.Invoke(AsyncActivation`1 ctxt) in C:\GitHub\dsyme\fsharp\a.fs:line 101
   at A.even@108-4.Invoke(AsyncActivation`1 ctxt) in C:\GitHub\dsyme\fsharp\a.fs:line 108
   at A.odd@101-4.Invoke(AsyncActivation`1 ctxt) in C:\GitHub\dsyme\fsharp\a.fs:line 101
   at A.even@108-4.Invoke(AsyncActivation`1 ctxt) in C:\GitHub\dsyme\fsharp\a.fs:line 108
   at A.odd@101-4.Invoke(AsyncActivation`1 ctxt) in C:\GitHub\dsyme\fsharp\a.fs:line 101
   at A.even@108-4.Invoke(AsyncActivation`1 ctxt) in C:\GitHub\dsyme\fsharp\a.fs:line 108
   at A.odd@101-4.Invoke(AsyncActivation`1 ctxt) in C:\GitHub\dsyme\fsharp\a.fs:line 101
   at A.even@108-4.Invoke(AsyncActivation`1 ctxt) in C:\GitHub\dsyme\fsharp\a.fs:line 108
   at A.odd@101-4.Invoke(AsyncActivation`1 ctxt) in C:\GitHub\dsyme\fsharp\a.fs:line 101
   at A.even@108-4.Invoke(AsyncActivation`1 ctxt) in C:\GitHub\dsyme\fsharp\a.fs:line 108
   at A.odd@101-4.Invoke(AsyncActivation`1 ctxt) in C:\GitHub\dsyme\fsharp\a.fs:line 101
   at A.even@108-4.Invoke(AsyncActivation`1 ctxt) in C:\GitHub\dsyme\fsharp\a.fs:line 108
   at A.odd@101-4.Invoke(AsyncActivation`1 ctxt) in C:\GitHub\dsyme\fsharp\a.fs:line 101
   at A.even@108-4.Invoke(AsyncActivation`1 ctxt) in C:\GitHub\dsyme\fsharp\a.fs:line 108
   at A.odd@101-4.Invoke(AsyncActivation`1 ctxt) in C:\GitHub\dsyme\fsharp\a.fs:line 101
   at A.even@108-4.Invoke(AsyncActivation`1 ctxt) in C:\GitHub\dsyme\fsharp\a.fs:line 108
   at A.odd@101-4.Invoke(AsyncActivation`1 ctxt) in C:\GitHub\dsyme\fsharp\a.fs:line 101
   at A.even@108-4.Invoke(AsyncActivation`1 ctxt) in C:\GitHub\dsyme\fsharp\a.fs:line 108
   at A.odd@101-4.Invoke(AsyncActivation`1 ctxt) in C:\GitHub\dsyme\fsharp\a.fs:line 101
   at A.even@108-4.Invoke(AsyncActivation`1 ctxt) in C:\GitHub\dsyme\fsharp\a.fs:line 108

There is as yet no way to disable async tailcalls across your codebase in debug mode without making these intrusive code changes.

petrkoutnycz commented 2 years ago

Such call stacks are real PITA, I'm trying to push F# more in my company but things like this are really holding me back :-(

cartermp commented 2 years ago

@petrkoutnycz one purported benefit of the F# 6 task { } work is that stack traces are better than with async { }. Have you been able to adopt them?

petrkoutnycz commented 2 years ago

@cartermp thanks for the tip, I'll give it a shot

NinoFloris commented 2 years ago

Also if for some reason you cannot use F# 6, Ply's stacktraces are also really good for a 'classic' computation expression.

jwosty commented 7 months ago

Here's an interesting compounding factor. Take this program, which attempts to manually improve stack traces by using a helper function to rethrow things:

open System
open System.Diagnostics
open System.IO
open System.Runtime.ExceptionServices

module ThirdPartyLibrary =
    // Represents some low-level async "primitive" which is used in many many places and which may fail. This type of
    // method/function is not typically what you're interested in when examining stacktraces.
    // Example: a database query function from a thid-party library
    let doWorkAsync crash = ExtraTopLevelOperators.async {
        do! Async.Sleep 100
        if crash then
            raise (IOException("Oh no!"))
    }

// Helper function to improve stacktraces. This is the recommended way to augment (not replace) exception stacktraces
// with the caller's stack frame information
// See: https://stackoverflow.com/a/17091351/1231925
let inline rethrowAsync (computation: Async<'a>) = async {
    try
        return! computation
    with e ->
        // aids in debugging - you wouldn't include this in a real implementation
        let s = StackTrace()
        Debug.WriteLine $"StackTrace: %O{s}"
        Debug.WriteLine $"Rethrowing: %s{e.StackTrace}"
        Debug.WriteLine ""

        ExceptionDispatchInfo.Throw e
        return Unchecked.defaultof<_> // unreachable, but the compiler doesn't realize that
}

// Application function. We need this to show up in stack traces
let doLoopAsync (n: int) = async {
    for n in n .. -1 .. 0 do
        printfn "n = %d" n
        let crash = n = 6
        do! rethrowAsync (ThirdPartyLibrary.doWorkAsync crash)
}

// Higher level application function. We also need this to show up in stacktraces
let mainAsync () = async {
    let n = 10
    printfn "Starting with iterations: %d" n
    do! rethrowAsync (doLoopAsync n)
    printfn "Done."
}

[<EntryPoint>]
let main _ =
    Async.RunSynchronously (mainAsync ())
    0

Even with the explicit rethrows, we still somehow lose all the intermediate functions:

Unhandled exception. System.IO.IOException: Oh no!                                                                                                                                                                        
   at Program.ThirdPartyLibrary.doWorkAsync@12-1.Invoke(Unit _arg1) in C:\Users\jwostenberg\Documents\Code\FSharpSandbox\ConsoleApp2\Program.fs:line 13                                                                   
   at Microsoft.FSharp.Control.AsyncPrimitives.CallThenInvokeNoHijackCheck[a,b](AsyncActivation`1 ctxt, b result1, FSharpFunc`2 userCode) in D:\a\_work\1\s\src\FSharp.Core\async.fs:line 528                             
   at Microsoft.FSharp.Control.Trampoline.Execute(FSharpFunc`2 firstAction) in D:\a\_work\1\s\src\FSharp.Core\async.fs:line 112                                                                                           
--- End of stack trace from previous location ---                                                                                                                                                                         
   at Microsoft.FSharp.Control.AsyncResult`1.Commit() in D:\a\_work\1\s\src\FSharp.Core\async.fs:line 454                                                                                                                 
   at Microsoft.FSharp.Control.AsyncPrimitives.QueueAsyncAndWaitForResultSynchronously[a](CancellationToken token, FSharpAsync`1 computation, FSharpOption`1 timeout) in D:\a\_work\1\s\src\FSharp.Core\async.fs:line 1140
   at Microsoft.FSharp.Control.AsyncPrimitives.RunSynchronously[T](CancellationToken cancellationToken, FSharpAsync`1 computation, FSharpOption`1 timeout) in D:\a\_work\1\s\src\FSharp.Core\async.fs:line 1167           
   at Microsoft.FSharp.Control.FSharpAsync.RunSynchronously[T](FSharpAsync`1 computation, FSharpOption`1 timeout, FSharpOption`1 cancellationToken) in D:\a\_work\1\s\src\FSharp.Core\async.fs:line 1511                  
   at Program.main(String[] _arg1) in C:\Users\jwostenberg\Documents\Code\FSharpSandbox\ConsoleApp2\Program.fs:line 49

I'm fairly certain that this worked at some point in the past.

Interestingly, if you "transfer" the exception across the RunSynchronously call:

// ... same from before

type Async =
    static member RunSynchronouslyRethrow (computation, ?timeout, ?cancellationToken) =
        let result = Async.RunSynchronously (Async.Catch computation, ?timeout = timeout, ?cancellationToken = cancellationToken)
        match result with
        | Choice1Of2 x -> x
        | Choice2Of2 exn ->
            ExceptionDispatchInfo.Throw(exn)
            raise (UnreachableException())

[<EntryPoint>]
let main _ =
    Async.RunSynchronouslyRethrow (mainAsync ())

you get better stack traces again:

Unhandled exception. System.IO.IOException: Oh no!                                                                                                                                                        
   at Program.ThirdPartyLibrary.doWorkAsync@12-1.Invoke(Unit _arg1) in C:\Users\jwostenberg\Documents\Code\FSharpSandbox\ConsoleApp2\Program.fs:line 13                                                   
   at Microsoft.FSharp.Control.AsyncPrimitives.CallThenInvokeNoHijackCheck[a,b](AsyncActivation`1 ctxt, b result1, FSharpFunc`2 userCode) in D:\a\_work\1\s\src\FSharp.Core\async.fs:line 528             
   at Microsoft.FSharp.Control.Trampoline.Execute(FSharpFunc`2 firstAction) in D:\a\_work\1\s\src\FSharp.Core\async.fs:line 112                                                                           
--- End of stack trace from previous location ---                                                                                                                                                         
   at Program.doLoopAsync@36-4.Invoke(Exception _arg1)                                                                                                                                                    
   at Program.doLoopAsync@36-7.Invoke(Exception exn)                                                                                                                                                      
   at Microsoft.FSharp.Control.AsyncPrimitives.CallFilterThenInvoke[T](AsyncActivation`1 ctxt, FSharpFunc`2 filterFunction, ExceptionDispatchInfo edi) in D:\a\_work\1\s\src\FSharp.Core\async.fs:line 547
   at Microsoft.FSharp.Control.Trampoline.Execute(FSharpFunc`2 firstAction) in D:\a\_work\1\s\src\FSharp.Core\async.fs:line 112                                                                           
--- End of stack trace from previous location ---                                                                                                                                                         
   at Program.mainAsync@43-3.Invoke(Exception _arg1)                                                                                                                                                      
   at Program.mainAsync@43-6.Invoke(Exception exn)                                                                                                                                                        
   at Microsoft.FSharp.Control.AsyncPrimitives.CallFilterThenInvoke[T](AsyncActivation`1 ctxt, FSharpFunc`2 filterFunction, ExceptionDispatchInfo edi) in D:\a\_work\1\s\src\FSharp.Core\async.fs:line 547
   at Microsoft.FSharp.Control.Trampoline.Execute(FSharpFunc`2 firstAction) in D:\a\_work\1\s\src\FSharp.Core\async.fs:line 112
--- End of stack trace from previous location ---
   at Program.Async.RunSynchronouslyRethrow[a](FSharpAsync`1 computation, FSharpOption`1 timeout, FSharpOption`1 cancellationToken) in C:\Users\jwostenberg\Documents\Code\FSharpSandbox\ConsoleApp2\Program.fs:line 53
   at Program.main(String[] _arg1) in C:\Users\jwostenberg\Documents\Code\FSharpSandbox\ConsoleApp2\Program.fs:line 58

Does anybody have any clue what the culprit is? Is it the thread pool itself losing the information, or is Async.RunSynchronously? Where should the bug report go? I cannot find any prior art whatsoever about this particular tidbit.