Я реализовал атрибут [<Trace>]
для некоторых наших более крупных решений .NET, который позволит легко добавлять настраиваемую аналитику к любым функциям / методам, которые считаются важными. Я использую Fody и MethodBoundaryAspect , чтобы перехватывать вход и выход каждой функции и записывать метрики. Это хорошо работает для синхронных функций, а для методов, которые возвращают Task
, существует работоспособное решение с Task.ContinueWith
, но для функций F # с асинхронным возвратом OnExit
из MethodBoundaryAspect запускается, как только возвращается Async (скорее чем когда асинхронное выполнение фактически выполняется).
Чтобы получить правильные метрики для функций, возвращающих F # Async, я пытался найти эквивалентное решение для использования Task.ContinueWith
, но самое близкое, что я мог придумать, - это создать новый Async, который связывает первый один, запускает функции захвата метрики, а затем возвращает исходный результат. Это еще более усложняется тем фактом, что возвращаемое значение F # Async, которое я перехватываю, представляется только как obj
, и после этого мне нужно все делать рефлексивно, поскольку не существует неуниверсальной версии Async
, как с Task
, который я могу использовать, не зная точного типа возврата.
Мое лучшее решение на данный момент выглядит примерно так:
open System
open System.Diagnostics
open FSharp.Reflection
open MethodBoundaryAspect.Fody.Attributes
[<AllowNullLiteral>]
[<AttributeUsage(AttributeTargets.Method ||| AttributeTargets.Property, AllowMultiple = false)>]
type TraceAttribute () =
inherit OnMethodBoundaryAspect()
let traceEvent (args: MethodExecutionArgs) (timestamp: int64) =
// Capture metrics here
()
override __.OnEntry (args) =
Stopwatch.GetTimestamp() |> traceEvent args
override __.OnExit (args) =
let exit () = Stopwatch.GetTimestamp() |> traceEvent args
match args.ReturnValue with
| :? System.Threading.Tasks.Task as task ->
task.ContinueWith(fun _ -> exit()) |> ignore
| other -> // Here's where I could use some help
let clrType = other.GetType()
if clrType.IsGenericType && clrType.GetGenericTypeDefinition() = typedefof<Async<_>> then
// If the return type is an F# Async, replace it with a new Async that calls exit after the original return value is computed
let returnType = clrType.GetGenericArguments().[0]
let functionType = FSharpType.MakeFunctionType(returnType, typedefof<Async<_>>.MakeGenericType([| returnType |]))
let f = FSharpValue.MakeFunction(functionType, (fun _ -> exit(); other))
let result = typeof<AsyncBuilder>.GetMethod("Bind").MakeGenericMethod([|returnType; returnType|]).Invoke(async, [|other; f|])
args.ReturnValue <- result
else
exit()
К сожалению, это решение не только довольно грязное, но я считаю, что рефлексивная конструкция асинхронных вычислений добавляет нетривиальные издержки, особенно когда я пытаюсь отследить функции, которые вызываются в цикле или имеют глубоко вложенные асинхронные вызовы. Есть ли лучший способ достичь того же результата при запуске данной функции сразу после фактической оценки асинхронных вычислений?