Repository navigation
Trace async bodies with a debug hook #369
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
Changes from all commits
dad5d58
2ee6979
8068b3e
87ea5cb
7ece84a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,213 @@ | ||
| // Licensed to the .NET Foundation under one or more agreements. | ||
| // The .NET Foundation licenses this file to you under the Apache 2.0 License. | ||
| // See the LICENSE file in the project root for more information. | ||
|
|
||
| #nullable enable | ||
|
|
||
| using System; | ||
| using System.Collections.Generic; | ||
| using System.Globalization; | ||
| using System.Linq.Expressions; | ||
| using System.Reflection; | ||
| using System.Runtime.CompilerServices; | ||
|
|
||
| using Microsoft.Scripting.Debugging; | ||
| using Microsoft.Scripting.Debugging.CompilerServices; | ||
|
|
||
| namespace Microsoft.Scripting.Ast { | ||
| /// <summary> | ||
| /// Compiles an async body for tracing. | ||
| /// </summary> | ||
| /// <remarks> | ||
| /// Used by <see cref="AsyncRewriter"/> and <c>AsyncEnumerableRewriter</c> when the async node carries a debug hook (set by the host | ||
| /// only when it builds the tracing variant of the code, e.g. when <c>sys.settrace</c> is active). In two steps: | ||
| /// <list type="number"> | ||
| /// <item><see cref="InjectDebugPoints"/> instruments the body before it is sliced into a state machine, so that the debug points | ||
| /// end up in the same state machine as the awaits and yields.</item> | ||
| /// <item><see cref="TraceStateMachine"/> wraps the resulting state machine in a <see cref="TracingStateMachine"/>, which dispatches | ||
| /// the trace events while the driver pumps it.</item> | ||
| /// </list> | ||
| /// | ||
| /// <para>The body's locals are not lifted, so its frame reports its location but no locals; the scope map is therefore uniform | ||
| /// (every sequence point maps to no locals).</para> | ||
| /// </remarks> | ||
| internal sealed class AsyncDebugSupport { | ||
| private static readonly ConstructorInfo s_tracingStateMachineCtor | ||
| = typeof(TracingStateMachine).GetConstructor( | ||
| BindingFlags.Instance | BindingFlags.NonPublic, binder: null, | ||
| [typeof(IEnumerator<object?>), typeof(DebugContext), typeof(FunctionInfo), typeof(StrongBox<object?>), typeof(StrongBox<Exception?>), typeof(bool)], | ||
| modifiers: null)!; | ||
| private static readonly MethodInfo s_onExceptionCaughtMethod | ||
| = typeof(TracingStateMachine).GetMethod(nameof(TracingStateMachine.OnExceptionCaught), BindingFlags.Instance | BindingFlags.NonPublic)!; | ||
|
|
||
| private readonly DebugContext _debugContext; | ||
| private readonly DebugLambdaInfo _lambdaInfo; | ||
| private readonly string? _name; | ||
| private readonly ParameterExpression _exceptionSlot; | ||
|
|
||
| // Holds the TracingStateMachine, so that the body's exception handlers can report to it. | ||
| private readonly ParameterExpression _tracer = Expression.Variable(typeof(TracingStateMachine), "$tracer"); | ||
|
|
||
| // Null until the body is instrumented, and also if it has no sequence points (then there is nothing to trace). | ||
| private FunctionInfo? _functionInfo; | ||
|
|
||
| /// <param name="exceptionSlot">The body's exception slot, which the body rethrows from at each resume point.</param> | ||
| internal AsyncDebugSupport(DebugContext debugContext, DebugLambdaInfo lambdaInfo, string? name, ParameterExpression exceptionSlot) { | ||
| _debugContext = debugContext; | ||
| _lambdaInfo = lambdaInfo; | ||
| _name = name; | ||
| _exceptionSlot = exceptionSlot; | ||
| } | ||
|
|
||
|
|
||
| /// <summary> | ||
| /// Instruments <paramref name="body"/> to report its sequence points and the exceptions it handles. | ||
| /// </summary> | ||
| /// <remarks> | ||
| /// Each <see cref="DebugInfoExpression"/> becomes <c>{ yield DebugPoint(cookie); rethrow-if-pending }</c> on | ||
| /// <paramref name="yieldLabel"/>, the label the body's awaits and yields are sliced on, and each exception handler first reports | ||
| /// the exception it handles. The rethrow lets an exception raised by the trace callback surface inside the body. | ||
| /// </remarks> | ||
| internal Expression InjectDebugPoints(Expression body, LabelTarget yieldLabel) { | ||
| var injector = new DebugPointInjector(_debugContext, yieldLabel, _exceptionSlot, _tracer); | ||
| Expression instrumentedBody = injector.Visit(body); | ||
| if (injector.SequencePoints.Length == 0) { | ||
| return body; | ||
| } | ||
|
|
||
| _functionInfo = DebugContext.CreateFunctionInfo( | ||
| generatorFactory: null, // the frame cannot be remapped to a generator, so it cannot change its location either | ||
| _name ?? "<async>", | ||
| injector.SequencePoints, | ||
| injector.VariableScopeMap, | ||
| Array.Empty<VariableInfo>(), | ||
| _lambdaInfo.CustomPayload); | ||
| return instrumentedBody; | ||
| } | ||
|
|
||
|
|
||
| /// <summary> | ||
| /// Wraps <paramref name="stateMachine"/>, the state machine of the instrumented body, in a <see cref="TracingStateMachine"/>. | ||
| /// </summary> | ||
| /// <param name="resultSlot">Where the body leaves its return value, or null if it has none.</param> | ||
| /// <param name="isAsyncEnumerable">Whether the state machine is driven by <c>AsyncHelpers.DriveAsyncEnumerable</c>.</param> | ||
| internal Expression TraceStateMachine(Expression stateMachine, ParameterExpression? resultSlot, bool isAsyncEnumerable) { | ||
| if (_functionInfo is null) { | ||
| return stateMachine; | ||
| } | ||
|
|
||
| return Expression.Block( | ||
| typeof(TracingStateMachine), | ||
| [_tracer], | ||
| Expression.Assign( | ||
| _tracer, | ||
| Expression.New( | ||
| s_tracingStateMachineCtor, | ||
| stateMachine, | ||
| Expression.Constant(_debugContext), | ||
| Expression.Constant(_functionInfo), | ||
| (Expression?)resultSlot ?? Expression.Constant(null, typeof(StrongBox<object?>)), | ||
| _exceptionSlot, | ||
| Expression.Constant(isAsyncEnumerable)))); | ||
| } | ||
|
|
||
|
|
||
| /// <summary> | ||
| /// Rewrites the debug info and the exception handlers of an async body for <see cref="TracingStateMachine"/>. | ||
| /// </summary> | ||
| /// <remarks> | ||
| /// Each <see cref="DebugInfoExpression"/> becomes a yield of a <see cref="DebugPoint"/> on the body's own yield label, recording | ||
| /// the source span per cookie, and each catch block first reports the exception to the tracer. This mirrors what | ||
| /// <c>DebugInfoRewriter</c> does to a synchronous body. | ||
| /// </remarks> | ||
| private sealed class DebugPointInjector : ExpressionVisitor { | ||
| private readonly DebugContext _debugContext; | ||
| private readonly LabelTarget _yieldLabel; | ||
| private readonly ParameterExpression _exceptionSlot; | ||
| private readonly ParameterExpression _tracer; | ||
| private readonly List<DebugSourceSpan> _spans = new(); | ||
|
|
||
| internal DebugPointInjector(DebugContext debugContext, LabelTarget yieldLabel, ParameterExpression exceptionSlot, ParameterExpression tracer) { | ||
| _debugContext = debugContext; | ||
| _yieldLabel = yieldLabel; | ||
| _exceptionSlot = exceptionSlot; | ||
| _tracer = tracer; | ||
| } | ||
|
|
||
| internal DebugSourceSpan[] SequencePoints => _spans.ToArray(); | ||
|
|
||
| internal IList<VariableInfo>[] VariableScopeMap { | ||
| get { | ||
| var map = new IList<VariableInfo>[_spans.Count]; | ||
| for (int i = 0; i < map.Length; i++) { | ||
| map[i] = Array.Empty<VariableInfo>(); | ||
| } | ||
| return map; | ||
| } | ||
| } | ||
|
|
||
| protected override Expression VisitDebugInfo(DebugInfoExpression node) { | ||
| if (node.IsClear) { | ||
| return Expression.Empty(); | ||
| } | ||
|
|
||
| if (node.Document is null) { | ||
| throw new InvalidOperationException( | ||
| string.Format(CultureInfo.CurrentCulture, "DebugInfoExpression at sequence point {0} has no SymbolDocumentInfo", _spans.Count)); | ||
| } | ||
|
|
||
| DebugSourceFile sourceFile = _debugContext.GetDebugSourceFile( | ||
| string.IsNullOrEmpty(node.Document.FileName) ? "<compile>" : node.Document.FileName); | ||
|
|
||
| int cookie = _spans.Count; | ||
| _spans.Add(new DebugSourceSpan(sourceFile, node.StartLine, node.StartColumn, node.EndLine, node.EndColumn)); | ||
|
|
||
| return Expression.Block( | ||
| typeof(void), | ||
| Utils.YieldReturn(_yieldLabel, Expression.Constant(new DebugPoint(cookie))), | ||
| AsyncRewriter.RethrowIfPending(_exceptionSlot)); | ||
| } | ||
|
|
||
| protected override Expression VisitTry(TryExpression node) { | ||
| var visited = (TryExpression)base.VisitTry(node); | ||
|
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Resolved in 7ece84a: The variables are now reset on each entry into the try. A resumption into the handler is routed past the reset, so the exception still survives the handler's own yields. This change to the DLR by itself does not affect IronPython. IronPython has its own copy of |
||
| if (visited.Handlers.Count == 0) { | ||
| return visited; | ||
| } | ||
|
|
||
| var handlers = new List<CatchBlock>(visited.Handlers.Count); | ||
| foreach (CatchBlock handler in visited.Handlers) { | ||
| ParameterExpression variable = handler.Variable ?? Expression.Variable(handler.Test, null); | ||
| Expression exception = variable.Type == typeof(Exception) ? variable : Expression.Convert(variable, typeof(Exception)); | ||
| handlers.Add(Expression.MakeCatchBlock( | ||
| handler.Test, | ||
| variable, | ||
| Expression.Block(Expression.Call(_tracer, s_onExceptionCaughtMethod, exception), handler.Body), | ||
| handler.Filter)); | ||
| } | ||
| return Expression.MakeTry(visited.Type, visited.Body, visited.Finally, visited.Fault, handlers); | ||
| } | ||
|
|
||
| protected override Expression VisitLambda<T>(Expression<T> node) { | ||
| // Don't descend into nested lambdas — they have their own debug context. | ||
| return node; | ||
| } | ||
|
|
||
| protected override Expression VisitExtension(Expression node) { | ||
| // A nested async node is sliced into its own state machine against its own label; leave it opaque. | ||
| if (node is AsyncExpression or GeneratorExpression | ||
| #if NET | ||
| or AsyncEnumerableExpression | ||
| #endif | ||
| ) { | ||
| return node; | ||
| } | ||
| if (node is YieldExpression { Value: not null } yield && yield.Target == _yieldLabel) { | ||
| return Expression.Block( | ||
| base.VisitExtension(node), | ||
| AsyncRewriter.RethrowIfPending(_exceptionSlot)); | ||
| } | ||
| return base.VisitExtension(node); | ||
| } | ||
| } | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The observation is correct but not applicable for this PR. Async tracing deliberately behaves like the DLR's sync tracing here, and this limitation is DLR-wide:
DebugContext.TransformLambdaturns every sequence point into a yield in its debug generator, andGeneratorRewriterdoesn't allow yields in catch filters or fault blocks. So a lambda with a sequence point inside a filter or fault block already fails to transform for sync tracing (yield in filter is not allowed/yield in fault block is not supported). Filters without sequence points trace fine.LightCompilerthrows NRE for any filter, traced or not. So a language that compiles its exception tests into .NET filters is already limited to compiled code.exceptclauses are tested inside a plaincatch (Exception)body. IronPython has no filters to step through. A Python except clause is compiled as ordinary code inside a plaincatch (Exception)block: an if/else chain that callsCheckExceptionwith each clause's expression in turn, and rethrows if none matches. So everything Python considers part of theexcepttest runs in the catch body, where sequence points are allowed (in async bodies, debug-point yields there are supported, through the deferred-handler lowering). Perhaps other languages do something similar to sidestep the DLR limitation.If sequence points in filters and fault blocks need supporting (e.g. for a language with C#-like
catch … when), it should be done for sync and async tracing together, for example by emitting no debug point there, or a non-yielding one.