Skip to content

Commit 3fc4206

Browse files
Merged PR 674879: Exclude non essential methods from exception's stack trace
This PR updates `Ben.Demystifier` logic that is embeded into `BuildXL.Utilities` to support the latest language changes. The PR also adds support for respecting `StackTraceHiddenAttribute` for async methods and adds tests to check that the methods marked with `StackTraceHiddenAttribute` are not part of the error output. Related work items: #1979115
1 parent a6be68a commit 3fc4206

18 files changed

+904
-437
lines changed

Public/Src/Cache/ContentStore/Distributed/NuCache/EventStreaming/EventHubContentLocationEventStore.cs

+2-1
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
using BuildXL.Cache.ContentStore.Tracing.Internal;
1919
using BuildXL.Cache.ContentStore.Utils;
2020
using BuildXL.Native.IO;
21+
using BuildXL.Utilities;
2122
using BuildXL.Utilities.ParallelAlgorithms;
2223
using BuildXL.Utilities.Tasks;
2324
using BuildXL.Utilities.Tracing;
@@ -207,7 +208,7 @@ await _extraEventHubClientRetryPolicy.ExecuteAsync(async () =>
207208
// If the error is not retryable, then the entire operation will fail and we don't need to double trace the error.
208209
if (TransientEventHubErrorDetectionStrategy.IsRetryable(e))
209210
{
210-
Tracer.Debug(context, $"{Tracer.Name}.{nameof(SendEventsCoreAsync)} failed with retryable error=[{e}]");
211+
Tracer.Debug(context, $"{Tracer.Name}.{nameof(SendEventsCoreAsync)} failed with retryable error=[{e.ToStringDemystified()}]");
211212
}
212213

213214
throw;

Public/Src/Cache/ContentStore/Library/Stores/FileSystemContentStoreInternalChecker.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -366,7 +366,7 @@ void traceProgressIfNeeded(ContentHash currentHash, int currentHashIndex)
366366
}
367367
catch (Exception e)
368368
{
369-
_tracer.Warning(context, $"SelfCheck: Content hash is invalid. Hash={expectedHash.ToShortString()}, Error={e}");
369+
_tracer.Warning(context, $"SelfCheck: Content hash is invalid. Hash={expectedHash.ToShortString()}, Error={e.DemystifyToString()}");
370370

371371
return (isValid: true, error: string.Empty);
372372
}

Public/Src/Cache/ContentStore/Library/Tracing/OperationContextExtensions.cs

+9
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// Licensed under the MIT License.
33

44
using System;
5+
using System.Diagnostics;
56
using System.Runtime.CompilerServices;
67
using System.Threading;
78
using System.Threading.Tasks;
@@ -260,6 +261,7 @@ protected void TracePendingOperation(StopwatchSlim stopwatch)
260261
}
261262

262263
/// <nodoc />
264+
[StackTraceHidden]
263265
protected T RunOperationAndConvertExceptionToError<T>(Func<T> operation)
264266
where T : ResultBase
265267
{
@@ -276,6 +278,7 @@ protected T RunOperationAndConvertExceptionToError<T>(Func<T> operation)
276278
}
277279

278280
/// <nodoc />
281+
[StackTraceHidden]
279282
protected async Task<T> RunOperationAndConvertExceptionToErrorAsync<T>(Func<Task<T>> operation, StopwatchSlim stopwatch)
280283
where T : ResultBase
281284
{
@@ -347,6 +350,7 @@ public PerformAsyncOperationNonResultBuilder(OperationContext context, Tracer tr
347350
}
348351

349352
/// <nodoc />
353+
[StackTraceHidden]
350354
public async Task<TResult> RunAsync([CallerMemberName] string? caller = null)
351355
{
352356
// If the caller was not set by 'WithOptions' call, setting it here.
@@ -429,6 +433,7 @@ public PerformAsyncOperationBuilder(OperationContext context, Tracer tracer, Fun
429433
}
430434

431435
/// <nodoc />
436+
[StackTraceHidden]
432437
public virtual async Task<TResult> RunAsync([CallerMemberName] string? caller = null)
433438
{
434439
// If the caller was not set by 'WithOptions' call, setting it here.
@@ -470,13 +475,15 @@ public PerformAsyncOperationWithTimeoutBuilder(OperationContext context, Tracer
470475
}
471476

472477
/// <nodoc />
478+
[StackTraceHidden]
473479
public PerformAsyncOperationWithTimeoutBuilder<TResult> WithTimeout(TimeSpan? timeout)
474480
{
475481
_timeout = timeout;
476482
return this;
477483
}
478484

479485
/// <nodoc />
486+
[StackTraceHidden]
480487
public virtual async Task<TResult> RunAsync([CallerMemberName] string? caller = null)
481488
{
482489
// If the caller was not set by 'WithOptions' call, setting it here.
@@ -496,6 +503,7 @@ public virtual async Task<TResult> RunAsync([CallerMemberName] string? caller =
496503
}
497504

498505
/// <nodoc />
506+
[StackTraceHidden]
499507
protected async Task<T> RunOperationAndConvertExceptionToErrorAsync<T>(Func<OperationContext, Task<T>> operation, StopwatchSlim stopwatch)
500508
where T : ResultBase
501509
{
@@ -515,6 +523,7 @@ protected async Task<T> RunOperationAndConvertExceptionToErrorAsync<T>(Func<Oper
515523
}
516524

517525
/// <nodoc />
526+
[StackTraceHidden]
518527
public static async Task<T> WithOptionalTimeoutAsync<T>(Func<OperationContext, Task<T>> operation, TimeSpan? timeout, OperationContext context, [CallerMemberName] string? caller = null) where T : ResultBase
519528
{
520529
if (timeout == null || timeout.Value == TimeSpan.MaxValue)

Public/Src/Cache/ContentStore/Library/Utils/BandwidthChecker.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -163,7 +163,7 @@ void traceCopyTaskFailures()
163163
{
164164
if (!(t.Exception?.InnerException is ObjectDisposedException))
165165
{
166-
context.TracingContext.Debug($"Checked copy failed. {t.Exception}", component: nameof(BandwidthChecker), operation: nameof(CheckBandwidthAtIntervalAsync));
166+
context.TracingContext.Debug($"Checked copy failed. {t.Exception?.DemystifyToString()}", component: nameof(BandwidthChecker), operation: nameof(CheckBandwidthAtIntervalAsync));
167167
}
168168
}
169169
});

Public/Src/Cache/ContentStore/Test/Tracing/PerformOperationTests.cs

+56-4
Original file line numberDiff line numberDiff line change
@@ -25,12 +25,9 @@ namespace BuildXL.Cache.ContentStore.Test.Tracing
2525
{
2626
public class PerformOperationTests : TestWithOutput
2727
{
28-
private readonly ITestOutputHelper _testOutputHelper;
29-
30-
public PerformOperationTests(ITestOutputHelper output, ITestOutputHelper testOutputHelper)
28+
public PerformOperationTests(ITestOutputHelper output)
3129
: base(output)
3230
{
33-
_testOutputHelper = testOutputHelper;
3431
}
3532

3633
public enum AsyncOperationKind
@@ -146,6 +143,61 @@ Exception exceptionWithStackTrace()
146143

147144
// The final result should contain an original method that threw the error.
148145
finalErrorResult.Should().Contain("nested");
146+
finalErrorResult.Should().NotContain(nameof(Interfaces.Results.ResultsExtensions.ThrowIfFailure));
147+
148+
// Both the full string and the error message should have the original error message
149+
finalErrorResult.Should().Contain(errorMessage);
150+
r3.ErrorMessage.Should().Contain(errorMessage);
151+
152+
// The error message should not be repeated multiple times.
153+
finalErrorResult.Split(new string[] { errorMessage }, StringSplitOptions.RemoveEmptyEntries).Length.Should().Be(2);
154+
}
155+
156+
[Fact]
157+
public async Task TestResultPropagationExceptionInAsyncMethod()
158+
{
159+
const string errorMessage = "Invalid operation error";
160+
var tracer = new Tracer("MyTracer");
161+
var context = new OperationContext(new Context(TestGlobal.Logger));
162+
163+
var r1 = context.PerformOperationAsync(
164+
tracer,
165+
() =>
166+
{
167+
new BoolResult(exceptionWithStackTrace()).ThrowIfFailure();
168+
return BoolResult.SuccessTask;
169+
170+
Exception exceptionWithStackTrace()
171+
{
172+
try
173+
{
174+
nested();
175+
throw new InvalidOperationException("Should not get here!");
176+
}
177+
catch (Exception e)
178+
{
179+
return e;
180+
}
181+
}
182+
183+
void nested() => throw new InvalidOperationException(errorMessage);
184+
});
185+
186+
var r3 = await context.PerformOperationAsync(
187+
tracer,
188+
async () =>
189+
{
190+
await r1.ThrowIfFailureAsync();
191+
return BoolResult.Success;
192+
}, extraStartMessage: "Final operation");
193+
194+
var finalErrorResult = r3.ToString();
195+
196+
Output.WriteLine("R1: " + (await r1));
197+
198+
// The final result should contain an original method that threw the error.
199+
finalErrorResult.Should().Contain("nested");
200+
finalErrorResult.Should().NotContain(nameof(Interfaces.Results.ResultsExtensions.ThrowIfFailureAsync));
149201

150202
// Both the full string and the error message should have the original error message
151203
finalErrorResult.Should().Contain(errorMessage);

Public/Src/Utilities/Utilities/Ben.Demystifier/EnhancedStackFrame.cs

+23-9
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,30 @@
1-
// Copyright (c) Microsoft Corporation.
2-
// Licensed under the MIT License.
1+
// Copyright (c) Ben A Adams. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
33

44
using System.Reflection;
55

6+
#nullable enable
7+
68
namespace System.Diagnostics
79
{
810
/// <nodoc />
9-
public class EnhancedStackFrame : StackFrame
11+
internal class EnhancedStackFrame : StackFrame
1012
{
11-
private readonly string _fileName;
13+
private readonly string? _fileName;
1214
private readonly int _lineNumber;
1315
private readonly int _colNumber;
1416

15-
/// <nodoc />
1617
public StackFrame StackFrame { get; }
1718

18-
/// <nodoc />
19+
public bool IsRecursive
20+
{
21+
get => MethodInfo.RecurseCount > 0;
22+
internal set => MethodInfo.RecurseCount++;
23+
}
24+
1925
public ResolvedMethod MethodInfo { get; }
2026

21-
internal EnhancedStackFrame(StackFrame stackFrame, ResolvedMethod methodInfo, string fileName, int lineNumber, int colNumber)
27+
internal EnhancedStackFrame(StackFrame stackFrame, ResolvedMethod methodInfo, string? fileName, int lineNumber, int colNumber)
2228
: base(fileName, lineNumber, colNumber)
2329
{
2430
StackFrame = stackFrame;
@@ -29,6 +35,14 @@ internal EnhancedStackFrame(StackFrame stackFrame, ResolvedMethod methodInfo, st
2935
_colNumber = colNumber;
3036
}
3137

38+
internal bool IsEquivalent(ResolvedMethod methodInfo, string? fileName, int lineNumber, int colNumber)
39+
{
40+
return _lineNumber == lineNumber &&
41+
_colNumber == colNumber &&
42+
_fileName == fileName &&
43+
MethodInfo.IsSequentialEquivalent(methodInfo);
44+
}
45+
3246
/// <summary>
3347
/// Gets the column number in the file that contains the code that is executing.
3448
/// This information is typically extracted from the debugging symbols for the executable.
@@ -48,7 +62,7 @@ internal EnhancedStackFrame(StackFrame stackFrame, ResolvedMethod methodInfo, st
4862
/// This information is typically extracted from the debugging symbols for the executable.
4963
/// </summary>
5064
/// <returns>The file name, or null if the file name cannot be determined.</returns>
51-
public override string GetFileName() => _fileName;
65+
public override string? GetFileName() => _fileName;
5266

5367
/// <summary>
5468
/// Gets the offset from the start of the Microsoft intermediate language (MSIL)
@@ -63,7 +77,7 @@ internal EnhancedStackFrame(StackFrame stackFrame, ResolvedMethod methodInfo, st
6377
/// Gets the method in which the frame is executing.
6478
/// </summary>
6579
/// <returns>The method in which the frame is executing.</returns>
66-
public override MethodBase GetMethod() => StackFrame.GetMethod();
80+
public override MethodBase? GetMethod() => StackFrame.GetMethod();
6781

6882
/// <summary>
6983
/// Gets the offset from the start of the native just-in-time (JIT)-compiled code

0 commit comments

Comments
 (0)