Skip to content
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

.NET profiler improvements & test fixes #2968

Merged
merged 42 commits into from
Dec 22, 2023
Merged
Show file tree
Hide file tree
Changes from 24 commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
c0009f9
update profiler tests
vaind Dec 13, 2023
c39d96c
logging
vaind Dec 14, 2023
aaba231
improve profiler session startup
vaind Dec 14, 2023
00c75de
make profiling startup async
vaind Dec 15, 2023
c3d413a
introduce profiler startup timeout
vaind Dec 15, 2023
812874c
improve tests
vaind Dec 15, 2023
f7fbfc0
speedup tests
vaind Dec 15, 2023
47c826b
debug assert
vaind Dec 15, 2023
d86affa
Merge branch 'main' into chore/profiling-fixes
vaind Dec 15, 2023
0d3576b
improvements
vaind Dec 16, 2023
fb5c9fd
improvements
vaind Dec 17, 2023
5e99a61
add async startup test
vaind Dec 17, 2023
6c12446
improve tests
vaind Dec 17, 2023
2703013
Merge branch 'main' into chore/profiling-fixes
vaind Dec 18, 2023
c5e8513
deflake profiler tests
vaind Dec 18, 2023
ce98216
custom test params for CI
vaind Dec 18, 2023
7e87cb5
fixups
vaind Dec 18, 2023
85c4c33
Remove SparseScalarArray usage
vaind Dec 18, 2023
6d9e927
Merge branch 'main' into chore/profiling-fixes
vaind Dec 19, 2023
b4b913e
warn if profiling is misconfigured
vaind Dec 20, 2023
1924359
fixup
vaind Dec 20, 2023
ae3eeaf
cleanup CreateStackFrame()
vaind Dec 20, 2023
d84d25e
improve debug logs
vaind Dec 20, 2023
9367bfc
log changes
vaind Dec 20, 2023
c99df63
fix tests
vaind Dec 21, 2023
a929523
catch init exceptions
vaind Dec 21, 2023
03b586d
try to fix CI
vaind Dec 21, 2023
cb2bf9a
avoid logging errors multiple times
vaind Dec 21, 2023
7102696
review changes
vaind Dec 22, 2023
44fec5a
allow select profiler assertions to skip the test in CI.
vaind Dec 22, 2023
8a75ab3
cleanups
vaind Dec 22, 2023
a826783
Update src/Sentry.Profiling/SampleProfileBuilder.cs
vaind Dec 22, 2023
cc2a691
Default User.IpAddress to {{auto}} (#2981)
jamescrosswell Dec 20, 2023
853cf1f
Set in_foreground app context (#2983)
Giorgi Dec 22, 2023
1f244f4
MAUI screenshot (#2965)
Giorgi Dec 22, 2023
7b13d1d
chore(deps): update CLI to v2.23.1 (#2985)
github-actions[bot] Dec 22, 2023
91467a0
Remove Giorgi from CODEOWNERS (#2989)
Giorgi Dec 22, 2023
12971e3
chore: align LogWarning with LogError (#2987)
vaind Dec 22, 2023
f2c64b1
fixup LogWarning usages
vaind Dec 22, 2023
be2c18c
use Environment.ProcessId
vaind Dec 22, 2023
ba9a511
ci test fixups
vaind Dec 22, 2023
1290992
Merge remote-tracking branch 'origin/main' into chore/profiling-fixes
vaind Dec 22, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion benchmarks/Sentry.Benchmarks/ProfilingBenchmarks.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ public class ProfilingBenchmarks
[GlobalSetup(Targets = new string[] { nameof(Transaction), nameof(DoHardWorkWhileProfiling) })]
public void StartProfiler()
{
_factory = SamplingTransactionProfilerFactory.Create(new());
_factory = new SamplingTransactionProfilerFactory(new(), TimeSpan.FromSeconds(5));
}

[GlobalCleanup(Targets = new string[] { nameof(Transaction), nameof(DoHardWorkWhileProfiling) })]
Expand Down
5 changes: 4 additions & 1 deletion samples/Sentry.Samples.Console.Profiling/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,13 @@ private static void Main()
options.IsGlobalModeEnabled = true;
options.EnableTracing = true;
// Make sure to reduce the sampling rate in production.
options.ProfilesSampleRate = 1.0;
// Debugging
options.ShutdownTimeout = TimeSpan.FromMinutes(5);
options.AddIntegration(new ProfilingIntegration());
options.AddIntegration(new ProfilingIntegration(TimeSpan.FromMilliseconds(500)));
}))
{
var tx = SentrySdk.StartTransaction("app", "run");
Expand Down
3 changes: 1 addition & 2 deletions src/Sentry.Profiling/Downsampler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
internal class Downsampler
{
private static double _samplingRateMs = (double)1_000 / 101; // 101 Hz
private double _samplingGapMs = _samplingRateMs * 0.9;

// Maps from ThreadIndex to the last sample timestamp for that thread.
private GrowableArray<double> _prevThreadSamples = new(10);
Expand All @@ -17,7 +16,7 @@ public void NewThreadAdded(int threadIndex)
if (threadIndex >= _prevThreadSamples.Count)
{
_prevThreadSamples.Count = threadIndex + 1;
_prevThreadSamples[threadIndex] = Double.MinValue;
_prevThreadSamples[threadIndex] = double.MinValue;
}
}

Expand Down
22 changes: 21 additions & 1 deletion src/Sentry.Profiling/ProfilingIntegration.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,32 @@ namespace Sentry.Profiling;
/// </summary>
public class ProfilingIntegration : ISdkIntegration
{
private TimeSpan _startupTimeout;

/// <summary>
/// Initializes the profiling integration.
/// </summary>
/// <param name="startupTimeout">
/// If not given or TimeSpan.Zero, then the profiler initialization is asynchronous.
/// This is useful for applications that need to start quickly. The profiler will start in the background
/// and will be ready to capture transactions that have started after the profiler has started.
///
/// If given a non-zero timeout, profiling startup blocks up to the given amount of time. If the timeout is reached
/// and the profiler session hasn't started yet, the execution is unblocked and behaves as the async startup,
/// i.e. transactions will be profiled only after the session is eventually started.
/// </param>
public ProfilingIntegration(TimeSpan startupTimeout = default)
{
Debug.Assert(TimeSpan.Zero == default);
bruno-garcia marked this conversation as resolved.
Show resolved Hide resolved
_startupTimeout = startupTimeout;
}

/// <inheritdoc/>
public void Register(IHub hub, SentryOptions options)
{
if (options.IsProfilingEnabled)
{
options.TransactionProfilerFactory ??= SamplingTransactionProfilerFactory.Create(options);
options.TransactionProfilerFactory ??= new SamplingTransactionProfilerFactory(options, _startupTimeout);
}
}
}
33 changes: 18 additions & 15 deletions src/Sentry.Profiling/SampleProfileBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,15 +17,14 @@ internal class SampleProfileBuilder
// Output profile being built.
public readonly SampleProfile Profile = new();

// TODO reevaluate the use of SparseArray after setting up continous profiling. Dictionary might be better.
// A sparse array that maps from StackSourceFrameIndex to an index in the output Profile.frames.
private readonly SparseScalarArray<int> _frameIndexes = new(-1, 1000);
private readonly Dictionary<int, int> _frameIndexes = new();

// A dictionary from a CallStackIndex to an index in the output Profile.stacks.
private readonly SparseScalarArray<int> _stackIndexes = new(100);
private readonly Dictionary<int, int> _stackIndexes = new();

// A sparse array mapping from a ThreadIndex to an index in Profile.Threads.
private readonly SparseScalarArray<int> _threadIndexes = new(-1, 10);
private readonly Dictionary<int, int> _threadIndexes = new();

// TODO make downsampling conditional once this is available: https://github.com/dotnet/runtime/issues/82939
private readonly Downsampler _downsampler = new();
Expand All @@ -39,7 +38,7 @@ public SampleProfileBuilder(SentryOptions options, TraceLog traceLog)
internal void AddSample(TraceEvent data, double timestampMs)
{
var thread = data.Thread();
if (thread.ThreadIndex == ThreadIndex.Invalid)
if (thread is null || thread.ThreadIndex == ThreadIndex.Invalid)
{
_options.DiagnosticLogger?.LogDebug("Encountered a Profiler Sample without a correct thread. Skipping.");
return;
Expand Down Expand Up @@ -163,8 +162,7 @@ private SentryStackFrame CreateStackFrame(CodeAddressIndex codeAddressIndex)
{
frame.Function = method.FullMethodName;

TraceModuleFile moduleFile = method.MethodModuleFile;
if (moduleFile is not null)
if (method.MethodModuleFile is { } moduleFile)
{
frame.Module = moduleFile.Name;
}
Expand All @@ -173,15 +171,20 @@ private SentryStackFrame CreateStackFrame(CodeAddressIndex codeAddressIndex)
}
else
{
// native frame
frame.InApp = false;
}
// Fall back if the method info is unknown, see more info on Symbol resolution in
// https://github.com/microsoft/perfview/blob/031250ffb4f9fcadb9263525d6c9f274be19ca51/src/PerfView/SupportFiles/UsersGuide.htm#L7745-L7784
vaind marked this conversation as resolved.
Show resolved Hide resolved
frame.InstructionAddress = (long?)_traceLog.CodeAddresses.Address(codeAddressIndex);

// TODO enable this once we implement symbolication (we will need to send debug_meta too), see StackTraceFactory.
// if (_traceLog.CodeAddresses.Address(codeAddressIndex) is { } address)
// {
// frame.InstructionAddress = $"0x{address:x}";
// }
if (_traceLog.CodeAddresses.ModuleFile(codeAddressIndex) is { } moduleFile)
{
frame.Module = moduleFile.Name;
frame.ConfigureAppFrame(_options);
}
else
{
frame.InApp = false;
}
}

return frame;
}
Expand Down
43 changes: 36 additions & 7 deletions src/Sentry.Profiling/SampleProfilerSession.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System.Diagnostics.Tracing;
using Microsoft.Diagnostics.NETCore.Client;
using Microsoft.Diagnostics.Tracing;
using Microsoft.Diagnostics.Tracing.Etlx;
using Microsoft.Diagnostics.Tracing.EventPipe;
using Microsoft.Diagnostics.Tracing.Parsers;
Expand Down Expand Up @@ -52,15 +53,43 @@ private SampleProfilerSession(SentryStopwatch stopwatch, EventPipeSession sessio

public static SampleProfilerSession StartNew(IDiagnosticLogger? logger = null)
{
var client = new DiagnosticsClient(Process.GetCurrentProcess().Id);
var session = client.StartEventPipeSession(Providers, requestRundown: false, CircularBufferMB);
var stopWatch = SentryStopwatch.StartNew();
var eventSource = TraceLog.CreateFromEventPipeSession(session, TraceLog.EventPipeRundownConfiguration.Enable(client));
try
{
var client = new DiagnosticsClient(Process.GetCurrentProcess().Id);
vaind marked this conversation as resolved.
Show resolved Hide resolved

// Note: StartEventPipeSession() can time out after 30 seconds on resource constrained systems.
// See https://github.com/dotnet/diagnostics/blob/991c78895323a953008e15fe34b736c03706afda/src/Microsoft.Diagnostics.NETCore.Client/DiagnosticsIpc/IpcClient.cs#L40C52-L40C52
var session = client.StartEventPipeSession(Providers, requestRundown: false, CircularBufferMB);

// Process() blocks until the session is stopped so we need to run it on a separate thread.
Task.Factory.StartNew(eventSource.Process, TaskCreationOptions.LongRunning);
var stopWatch = SentryStopwatch.StartNew();
var eventSource = TraceLog.CreateFromEventPipeSession(session, TraceLog.EventPipeRundownConfiguration.Enable(client));

return new SampleProfilerSession(stopWatch, session, eventSource, logger);
// Process() blocks until the session is stopped so we need to run it on a separate thread.
Task.Factory.StartNew(eventSource.Process, TaskCreationOptions.LongRunning);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need to observe any exception of this and log? I think right this it'll end up on UnobservedExceptionHandler

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it's logged below on line 74

I've added a test case Profiler_ThrowingOnSessionStartup_DoesntBreakSentryInit to make sure an exception thrown inside profiler session init doesn't break Sentry init

Copy link
Member

@bruno-garcia bruno-garcia Dec 22, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry I don't get it (line 74 is above this). But I mean if the callback running concurrently throws (eventSource.Process, if that throws? We can ContinuesWith at the end of StartNew for example, or have a callback with a trycatch before we call eentSource.Process?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, I get what you mean now. I was confused at first because this bit of code hasn't actually changed so I thought you're talking about sth else.

In case Process() throws: yes, I think it'd get handled by UnobservedExceptionHandler.

I've added ContinueWith()


return new SampleProfilerSession(stopWatch, session, eventSource, logger);
}
catch (Exception ex)
{
logger?.LogWarning("Error during sampler profiler EventPipeSession startup.", ex);
vaind marked this conversation as resolved.
Show resolved Hide resolved
throw;
}
}

public async Task WaitForFirstEventAsync(CancellationToken cancellationToken = default)
{
var tcs = new TaskCompletionSource();
var cb = (TraceEvent _) => { tcs.TrySetResult(); };
_eventSource.AllEvents += cb;
try
{
// Wait for the first event to be processed.
await tcs.Task.WaitAsync(cancellationToken).ConfigureAwait(false);
}
finally
{
_eventSource.AllEvents -= cb;
}
}

public void Stop()
Expand Down
1 change: 1 addition & 0 deletions src/Sentry.Profiling/SamplingTransactionProfiler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ private void OnThreadSample(TraceEvent data)
}
else
{
_options.LogDebug("Profiler has been stopped and has received all the samples up to the end time.");
_session.SampleEventParser.ThreadSample -= OnThreadSample;
_completionSource.TrySetResult();
}
Expand Down
50 changes: 34 additions & 16 deletions src/Sentry.Profiling/SamplingTransactionProfilerFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,24 +15,28 @@ internal class SamplingTransactionProfilerFactory : IDisposable, ITransactionPro
private const int TIME_LIMIT_MS = 30_000;

private readonly SentryOptions _options;
private SampleProfilerSession _session;
internal Task<SampleProfilerSession> _sessionTask;

public static SamplingTransactionProfilerFactory Create(SentryOptions options)
public SamplingTransactionProfilerFactory(SentryOptions options, TimeSpan startupTimeout)
{
var session = SampleProfilerSession.StartNew(options.DiagnosticLogger);
return new SamplingTransactionProfilerFactory(options, session);
}
_options = options;

public static async Task<SamplingTransactionProfilerFactory> CreateAsync(SentryOptions options)
{
var session = await Task.Run(() => SampleProfilerSession.StartNew(options.DiagnosticLogger)).ConfigureAwait(false);
return new SamplingTransactionProfilerFactory(options, session);
}
_sessionTask = Task.Run(async () =>
{
// This can block up to 30 seconds. The timeout is out of our hands.
var session = SampleProfilerSession.StartNew(options.DiagnosticLogger);

private SamplingTransactionProfilerFactory(SentryOptions options, SampleProfilerSession session)
{
_options = options;
_session = session;
// This can block indefinitely.
await session.WaitForFirstEventAsync().ConfigureAwait(false);

return session;
});

Debug.Assert(TimeSpan.FromSeconds(0) == TimeSpan.Zero);
if (startupTimeout != TimeSpan.Zero && !_sessionTask.Wait(startupTimeout))
bruno-garcia marked this conversation as resolved.
Show resolved Hide resolved
{
options.LogWarning("Profiler session startup took longer then the given timeout {0:c}. Profilling will start once the first event is received.", startupTimeout);
}
}

/// <inheritdoc />
Expand All @@ -41,10 +45,24 @@ private SamplingTransactionProfilerFactory(SentryOptions options, SampleProfiler
// Start a profiler if one wasn't running yet.
if (Interlocked.Exchange(ref _inProgress, TRUE) == FALSE)
{
if (!_sessionTask.IsCompleted)
{
_options.LogWarning("Cannot start a sampling profiler, the session hasn't started yet.");
_inProgress = FALSE;
return null;
}

if (!_sessionTask.IsCompletedSuccessfully)
jamescrosswell marked this conversation as resolved.
Show resolved Hide resolved
{
_options.LogWarning("Cannot start a sampling profiler, the session startup has not been successful.");
_inProgress = FALSE;
return null;
}

_options.LogDebug("Starting a sampling profiler.");
try
{
return new SamplingTransactionProfiler(_options, _session, TIME_LIMIT_MS, cancellationToken)
return new SamplingTransactionProfiler(_options, _sessionTask.Result, TIME_LIMIT_MS, cancellationToken)
{
OnFinish = () => _inProgress = FALSE
};
Expand All @@ -60,6 +78,6 @@ private SamplingTransactionProfilerFactory(SentryOptions options, SampleProfiler

public void Dispose()
{
_session.Dispose();
_sessionTask.ContinueWith((_session) => _session.Dispose());
bruno-garcia marked this conversation as resolved.
Show resolved Hide resolved
}
}
41 changes: 0 additions & 41 deletions src/Sentry/Internal/SparseArray.cs

This file was deleted.

28 changes: 28 additions & 0 deletions src/Sentry/SentrySdk.cs
Original file line number Diff line number Diff line change
Expand Up @@ -72,9 +72,37 @@ internal static IHub InitHub(SentryOptions options)
}
options.PostInitCallbacks.Clear();

// Platform specific check for profiler misconfiguration.
#if __IOS__
// No user-facing warning necessary - the integration is part of InitSentryCocoaSdk().
Debug.Assert(options.IsProfilingEnabled == (options.TransactionProfilerFactory is not null));
#elif ANDROID
LogWarningIfProfilingMisconfigured(options, " on Android");
#else
#if NET8_0_OR_GREATER
if (AotHelper.IsNativeAot)
{
LogWarningIfProfilingMisconfigured(options, " for NativeAOT");
}
else
#endif
{
LogWarningIfProfilingMisconfigured(options, ", because ProfilingIntegration from package Sentry.Profiling" +
" hasn't been registered. You can do that by calling 'options.AddIntegration(new ProfilingIntegration())'");
}
#endif

return hub;
}

private static void LogWarningIfProfilingMisconfigured(SentryOptions options, string info)
{
if (options.IsProfilingEnabled && (options.TransactionProfilerFactory is null))
{
options.LogWarning("You've tried to enable profiling in options, but it is not available{0}.", info);
}
}

/// <summary>
/// Initializes the SDK while attempting to locate the DSN.
/// </summary>
Expand Down
2 changes: 2 additions & 0 deletions test/Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
<IsTestProject>true</IsTestProject>
<!-- Ignore warnings for JSON generated code -->
<NoWarn>$(NoWarn);SYSLIB0005;SYSLIB0012</NoWarn>
<!-- Ignore "Naming rule violation: Missing suffix: 'Async'" -->
<NoWarn>$(NoWarn);IDE1006</NoWarn>
</PropertyGroup>

<!--
Expand Down
Loading
Loading