diff --git a/CHANGELOG.md b/CHANGELOG.md index 38051d2126..60b44b6169 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +## Unreleased + +### Fixes + +- Unknown stack frames in profiles on .NET 8+ ([#3967](https://github.com/getsentry/sentry-dotnet/pull/3967)) +- Deduplicate profiling stack frames ([#3969](https://github.com/getsentry/sentry-dotnet/pull/3969)) + ## 5.1.1 ### Fixes @@ -10,7 +17,6 @@ - Fixed envelopes with oversized attachments getting stuck in __processing ([#3938](https://github.com/getsentry/sentry-dotnet/pull/3938)) - OperatingSystem will now return macOS as OS name instead of 'Darwin' as well as the proper version. ([#2710](https://github.com/getsentry/sentry-dotnet/pull/3956)) - Ignore null value on CocoaScopeObserver.SetTag ([#3948](https://github.com/getsentry/sentry-dotnet/pull/3948)) -- Deduplicate profiling stack frames ([#3969](https://github.com/getsentry/sentry-dotnet/pull/3969)) ## 5.1.0 diff --git a/src/Sentry.Profiling/SampleProfilerSession.cs b/src/Sentry.Profiling/SampleProfilerSession.cs index e54cf62020..9b5e2756b1 100644 --- a/src/Sentry.Profiling/SampleProfilerSession.cs +++ b/src/Sentry.Profiling/SampleProfilerSession.cs @@ -12,7 +12,6 @@ namespace Sentry.Profiling; internal class SampleProfilerSession : IDisposable { private readonly EventPipeSession _session; - private readonly TraceLogEventSource _eventSource; private readonly SampleProfilerTraceEventParser _sampleEventParser; private readonly IDiagnosticLogger? _logger; private readonly SentryStopwatch _stopwatch; @@ -23,8 +22,8 @@ private SampleProfilerSession(SentryStopwatch stopwatch, EventPipeSession sessio { _session = session; _logger = logger; - _eventSource = eventSource; - _sampleEventParser = new SampleProfilerTraceEventParser(_eventSource); + EventSource = eventSource; + _sampleEventParser = new SampleProfilerTraceEventParser(EventSource); _stopwatch = stopwatch; _processing = processing; } @@ -38,7 +37,7 @@ private SampleProfilerSession(SentryStopwatch stopwatch, EventPipeSession sessio // Default = GC | Type | GCHeapSurvivalAndMovement | Binder | Loader | Jit | NGen | SupressNGen // | StopEnumeration | Security | AppDomainResourceManagement | Exception | Threading | Contention | Stack | JittedMethodILToNativeMap // | ThreadTransfer | GCHeapAndTypeNames | Codesymbols | Compilation, - new EventPipeProvider(ClrTraceEventParser.ProviderName, EventLevel.Informational, (long) ClrTraceEventParser.Keywords.Default), + new EventPipeProvider(ClrTraceEventParser.ProviderName, EventLevel.Verbose, (long) ClrTraceEventParser.Keywords.Default), new EventPipeProvider(SampleProfilerTraceEventParser.ProviderName, EventLevel.Informational), // new EventPipeProvider(TplEtwProviderTraceEventParser.ProviderName, EventLevel.Informational, (long) TplEtwProviderTraceEventParser.Keywords.Default) }; @@ -48,11 +47,14 @@ private SampleProfilerSession(SentryStopwatch stopwatch, EventPipeSession sessio // need a large buffer if we're connecting righ away. Leaving it too large increases app memory usage. internal static int CircularBufferMB = 16; + // Exposed for tests + internal TraceLogEventSource EventSource { get; } + public SampleProfilerTraceEventParser SampleEventParser => _sampleEventParser; public TimeSpan Elapsed => _stopwatch.Elapsed; - public TraceLog TraceLog => _eventSource.TraceLog; + public TraceLog TraceLog => EventSource.TraceLog; // default is false, set 1 for true. private static int _throwOnNextStartupForTests = 0; @@ -110,7 +112,7 @@ public async Task WaitForFirstEventAsync(CancellationToken cancellationToken = d { var tcs = new TaskCompletionSource(); var cb = (TraceEvent _) => { tcs.TrySetResult(); }; - _eventSource.AllEvents += cb; + EventSource.AllEvents += cb; try { // Wait for the first event to be processed. @@ -118,7 +120,7 @@ public async Task WaitForFirstEventAsync(CancellationToken cancellationToken = d } finally { - _eventSource.AllEvents -= cb; + EventSource.AllEvents -= cb; } } @@ -132,7 +134,7 @@ public void Stop() _session.Stop(); _processing.Wait(); _session.Dispose(); - _eventSource.Dispose(); + EventSource.Dispose(); } catch (Exception ex) { diff --git a/test/Sentry.Profiling.Tests/SamplingTransactionProfilerTests.cs b/test/Sentry.Profiling.Tests/SamplingTransactionProfilerTests.cs index 12bdcc5995..af5914011d 100644 --- a/test/Sentry.Profiling.Tests/SamplingTransactionProfilerTests.cs +++ b/test/Sentry.Profiling.Tests/SamplingTransactionProfilerTests.cs @@ -1,4 +1,5 @@ -using System.IO.Abstractions.TestingHelpers; +using Microsoft.Diagnostics.Tracing; +using Microsoft.Diagnostics.Tracing.Parsers.Clr; using Sentry.Internal.Http; namespace Sentry.Profiling.Tests; @@ -180,6 +181,46 @@ public async Task Profiler_AfterTimeout_Stops() } } + + /// + /// Guards regression of https://github.com/microsoft/perfview/issues/2155 + /// + [SkippableFact] + public async Task EventPipeSession_ReceivesExpectedCLREvents() + { + SampleProfilerSession? session = null; + SkipIfFailsInCI(() => session = SampleProfilerSession.StartNew(_testOutputLogger)); + using (session) + { + var eventsReceived = new HashSet(); + session!.EventSource.Clr.MethodLoadVerbose += (_) => eventsReceived.Add("Method/LoadVerbose"); + session!.EventSource.Clr.MethodILToNativeMap += (_) => eventsReceived.Add("Method/ILToNativeMap"); + + await session.WaitForFirstEventAsync(CancellationToken.None); + var tries = 0; + while (eventsReceived.Count < 2 && tries++ < 10) + { + if (tries > 1) + { + await Task.Delay(100); + } + var limitMs = 50; + var sut = new SamplingTransactionProfiler(_testSentryOptions, session, limitMs, CancellationToken.None); + MethodToBeLoaded(100); + RunForMs(limitMs); + sut.Finish(); + } + + Assert.Contains("Method/LoadVerbose", eventsReceived); + Assert.Contains("Method/ILToNativeMap", eventsReceived); + } + } + + private static long MethodToBeLoaded(int n) + { + return -n; + } + [SkippableTheory] [InlineData(true)] [InlineData(false)]