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

iOS native profiling #2930

Merged
merged 46 commits into from
Dec 12, 2023
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
3f8f036
wip: cocoa profiling
vaind Nov 30, 2023
cd43974
use ISerializable in ITransactionProfiler
vaind Dec 1, 2023
fad8eff
fixup profile content
vaind Dec 1, 2023
c2adb5a
dispose stream
vaind Dec 1, 2023
0aa3bf1
fixup sentry.profiling
vaind Dec 1, 2023
ba4982a
build fixes
vaind Dec 1, 2023
fb13b03
Merge branch 'main' into feat/ios-native-profiling
vaind Dec 4, 2023
efa4552
feat: add ProfilesSampleRate
vaind Dec 4, 2023
4533cd9
Apply suggestions from code review
vaind Dec 5, 2023
4e18597
fixup naming
vaind Dec 5, 2023
1727f44
update verifier tests
vaind Dec 5, 2023
bf8e17d
update profiler tests
vaind Dec 5, 2023
e5383e2
build(deps): bump actions/setup-java from 3 to 4 (#2942)
dependabot[bot] Dec 4, 2023
221439c
workaround .net nativeAOT crash (#2943)
vaind Dec 4, 2023
cf734bd
release: 4.0.0-beta.4
getsentry-bot Dec 5, 2023
5bfcdf0
fix: gcp link (#2944)
bruno-garcia Dec 5, 2023
6d4f658
update sample
vaind Dec 5, 2023
8246932
chore: update changelog
vaind Dec 5, 2023
5d12ed0
release: 4.0.0-beta.4
getsentry-bot Dec 5, 2023
67c54fa
Merge remote-tracking branch 'origin/main' into feat/ios-native-profi…
vaind Dec 5, 2023
7fce737
fixup changelog
vaind Dec 5, 2023
f91846a
profiler hub tests
vaind Dec 8, 2023
5acfdc2
improve profiler collect() error handling
vaind Dec 8, 2023
e728541
Merge remote-tracking branch 'origin/main' into feat/ios-native-profi…
vaind Dec 8, 2023
c56da69
fixup device test script
vaind Dec 8, 2023
4a92fac
fix: hub tests
vaind Dec 8, 2023
6772bc1
Update CHANGELOG.md
vaind Dec 8, 2023
07eb383
Update src/Sentry.Profiling/ProfilingIntegration.cs
vaind Dec 8, 2023
37ca589
Update src/Sentry/Platforms/Cocoa/CocoaProfiler.cs
vaind Dec 8, 2023
6364fd0
Update src/Sentry/Platforms/Cocoa/SentrySdk.cs
vaind Dec 8, 2023
20807ab
review changes
vaind Dec 8, 2023
539cc8a
review changes
vaind Dec 8, 2023
1f93d19
profiler integration test
vaind Dec 8, 2023
bf11364
Update ProfilerTests.cs
vaind Dec 8, 2023
a17b3a0
test cleanup
vaind Dec 8, 2023
0c4ebc3
only set profiler factory if not previously set
vaind Dec 10, 2023
45617e7
trying to find stuck test
vaind Dec 10, 2023
f030857
improve logs
vaind Dec 10, 2023
24d1656
fixup device tests
vaind Dec 10, 2023
c2e63ec
include cocoa in device test app
vaind Dec 10, 2023
1dddef1
fix profiler test
vaind Dec 10, 2023
fb339d9
try disabling hub tests
vaind Dec 10, 2023
25099ba
try enabling a potential flaky test
vaind Dec 11, 2023
2b78e3b
try to enable a test case
vaind Dec 12, 2023
e5a069a
try enabling a test case
vaind Dec 12, 2023
ed6dd63
restore CI
vaind Dec 12, 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
38 changes: 38 additions & 0 deletions samples/Sentry.Samples.Ios/AppDelegate.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ public override bool FinishedLaunching(UIApplication application, NSDictionary l
{
o.Debug = true;
o.Dsn = "https://eb18e953812b41c3aeb042e666fd3b5c@o447951.ingest.sentry.io/5428537";
o.TracesSampleRate = 1.0;
});

// create a new window instance based on the screen size
Expand Down Expand Up @@ -50,6 +51,43 @@ public override bool FinishedLaunching(UIApplication application, NSDictionary l
// throw new Exception("Test Unhandled Managed Exception");
// SentrySdk.CauseCrash(CrashType.Native);

{
var tx = SentrySdk.StartTransaction("app", "run");
var count = 10;
for (var i = 0; i < count; i++)
{
FindPrimeNumber(100000);
}

tx.Finish();
}

return true;
}

private static long FindPrimeNumber(int n)
{
int count = 0;
long a = 2;
while (count < n)
{
long b = 2;
int prime = 1;// to check if found a prime
while (b * b <= a)
{
if (a % b == 0)
{
prime = 0;
break;
}
b++;
}
if (prime > 0)
{
count++;
}
a++;
}
return (--a);
}
}
4 changes: 2 additions & 2 deletions src/Sentry.Profiling/SamplingTransactionProfiler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -96,11 +96,11 @@
}

/// <inheritdoc />
public async Task<ProfileInfo> CollectAsync(Transaction transaction)
public async object Collect(Transaction transaction)

Check failure on line 99 in src/Sentry.Profiling/SamplingTransactionProfiler.cs

View workflow job for this annotation

GitHub Actions / Analyze

The return type of an async method must be void, Task, Task<T>, a task-like type, IAsyncEnumerable<T>, or IAsyncEnumerator<T>

Check failure on line 99 in src/Sentry.Profiling/SamplingTransactionProfiler.cs

View workflow job for this annotation

GitHub Actions / Analyze

The return type of an async method must be void, Task, Task<T>, a task-like type, IAsyncEnumerable<T>, or IAsyncEnumerator<T>

Check failure on line 99 in src/Sentry.Profiling/SamplingTransactionProfiler.cs

View workflow job for this annotation

GitHub Actions / Analyze

The return type of an async method must be void, Task, Task<T>, a task-like type, IAsyncEnumerable<T>, or IAsyncEnumerator<T>

Check failure on line 99 in src/Sentry.Profiling/SamplingTransactionProfiler.cs

View workflow job for this annotation

GitHub Actions / Analyze

The return type of an async method must be void, Task, Task<T>, a task-like type, IAsyncEnumerable<T>, or IAsyncEnumerator<T>

Check failure on line 99 in src/Sentry.Profiling/SamplingTransactionProfiler.cs

View workflow job for this annotation

GitHub Actions / .NET (ubuntu-latest)

The return type of an async method must be void, Task, Task<T>, a task-like type, IAsyncEnumerable<T>, or IAsyncEnumerator<T>

Check failure on line 99 in src/Sentry.Profiling/SamplingTransactionProfiler.cs

View workflow job for this annotation

GitHub Actions / .NET (ubuntu-latest)

The return type of an async method must be void, Task, Task<T>, a task-like type, IAsyncEnumerable<T>, or IAsyncEnumerator<T>

Check failure on line 99 in src/Sentry.Profiling/SamplingTransactionProfiler.cs

View workflow job for this annotation

GitHub Actions / .NET (ubuntu-latest)

The return type of an async method must be void, Task, Task<T>, a task-like type, IAsyncEnumerable<T>, or IAsyncEnumerator<T>

Check failure on line 99 in src/Sentry.Profiling/SamplingTransactionProfiler.cs

View workflow job for this annotation

GitHub Actions / .NET (ubuntu-latest)

The return type of an async method must be void, Task, Task<T>, a task-like type, IAsyncEnumerable<T>, or IAsyncEnumerator<T>

Check failure on line 99 in src/Sentry.Profiling/SamplingTransactionProfiler.cs

View workflow job for this annotation

GitHub Actions / .NET (windows-latest)

The return type of an async method must be void, Task, Task<T>, a task-like type, IAsyncEnumerable<T>, or IAsyncEnumerator<T>

Check failure on line 99 in src/Sentry.Profiling/SamplingTransactionProfiler.cs

View workflow job for this annotation

GitHub Actions / .NET (windows-latest)

The return type of an async method must be void, Task, Task<T>, a task-like type, IAsyncEnumerable<T>, or IAsyncEnumerator<T>

Check failure on line 99 in src/Sentry.Profiling/SamplingTransactionProfiler.cs

View workflow job for this annotation

GitHub Actions / .NET (windows-latest)

The return type of an async method must be void, Task, Task<T>, a task-like type, IAsyncEnumerable<T>, or IAsyncEnumerator<T>

Check failure on line 99 in src/Sentry.Profiling/SamplingTransactionProfiler.cs

View workflow job for this annotation

GitHub Actions / .NET (windows-latest)

The return type of an async method must be void, Task, Task<T>, a task-like type, IAsyncEnumerable<T>, or IAsyncEnumerator<T>

Check failure on line 99 in src/Sentry.Profiling/SamplingTransactionProfiler.cs

View workflow job for this annotation

GitHub Actions / .NET (macos-13)

The return type of an async method must be void, Task, Task<T>, a task-like type, IAsyncEnumerable<T>, or IAsyncEnumerator<T>

Check failure on line 99 in src/Sentry.Profiling/SamplingTransactionProfiler.cs

View workflow job for this annotation

GitHub Actions / .NET (macos-13)

The return type of an async method must be void, Task, Task<T>, a task-like type, IAsyncEnumerable<T>, or IAsyncEnumerator<T>

Check failure on line 99 in src/Sentry.Profiling/SamplingTransactionProfiler.cs

View workflow job for this annotation

GitHub Actions / .NET (macos-13)

The return type of an async method must be void, Task, Task<T>, a task-like type, IAsyncEnumerable<T>, or IAsyncEnumerator<T>

Check failure on line 99 in src/Sentry.Profiling/SamplingTransactionProfiler.cs

View workflow job for this annotation

GitHub Actions / .NET (macos-13)

The return type of an async method must be void, Task, Task<T>, a task-like type, IAsyncEnumerable<T>, or IAsyncEnumerator<T>
vaind marked this conversation as resolved.
Show resolved Hide resolved
{
if (!_stopped)
{
throw new InvalidOperationException("Profiler.CollectAsync() called before Finish()");
throw new InvalidOperationException("Profiler.Collect() called before Finish()");
vaind marked this conversation as resolved.
Show resolved Hide resolved
}

// Wait for the last sample (<= _endTimeMs), or at most 1 second. The timeout shouldn't happen because
Expand Down
3 changes: 2 additions & 1 deletion src/Sentry/Internal/Hub.cs
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,7 @@ internal ITransactionTracer StartTransaction(
transaction.SampleRate = sampleRate;
}

// TODO profileSampleRate
if (transaction.IsSampled is true && _options.TransactionProfilerFactory is { } profilerFactory)
{
// TODO cancellation token based on Hub being closed?
Expand Down Expand Up @@ -200,7 +201,7 @@ public SentryTraceHeader GetTraceHeader()

public BaggageHeader GetBaggage()
{
if (GetSpan() is TransactionTracer { DynamicSamplingContext: { IsEmpty: false } dsc } )
if (GetSpan() is TransactionTracer { DynamicSamplingContext: { IsEmpty: false } dsc })
{
return dsc.ToBaggageHeader();
}
Expand Down
3 changes: 2 additions & 1 deletion src/Sentry/Internal/ITransactionProfiler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,5 +26,6 @@ internal interface ITransactionProfiler
/// <summary>
/// Process and collect the profile.
/// </summary>
Task<ProfileInfo> CollectAsync(Transaction transaction);
/// <returns>The collected profile. See EnvelopeItem.FromProfileInfo() for supported return types.</returns>
object Collect(Transaction transaction);
}
16 changes: 9 additions & 7 deletions src/Sentry/Internal/SentryStopwatch.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ internal struct SentryStopwatch
{
private static readonly double StopwatchTicksPerTimeSpanTick =
(double)Stopwatch.Frequency / TimeSpan.TicksPerSecond;
private static readonly double StopwatchTicksPerNs = (double)Stopwatch.Frequency / 1000000000.0;

private long _startTimestamp;
private DateTimeOffset _startDateTimeOffset;
Expand All @@ -21,14 +22,15 @@ internal struct SentryStopwatch
public DateTimeOffset StartDateTimeOffset => _startDateTimeOffset;
public DateTimeOffset CurrentDateTimeOffset => _startDateTimeOffset + Elapsed;

private long Diff() => Stopwatch.GetTimestamp() - _startTimestamp;

public TimeSpan Elapsed
{
get
{
var now = Stopwatch.GetTimestamp();
var diff = now - _startTimestamp;
var ticks = (long)(diff / StopwatchTicksPerTimeSpanTick);
return TimeSpan.FromTicks(ticks);
}
get => TimeSpan.FromTicks((long)(Diff() / StopwatchTicksPerTimeSpanTick));
}

public ulong ElapsedNanoseconds
{
get => (ulong)(Diff() / StopwatchTicksPerNs);
}
}
45 changes: 45 additions & 0 deletions src/Sentry/Platforms/iOS/CocoaProfiler.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
using Sentry.Extensibility;
using Sentry.Internal;
using Sentry.Protocol;
using Sentry.iOS.Extensions;
using Sentry.iOS.Facades;

namespace Sentry.iOS;

internal class CocoaProfiler : ITransactionProfiler
{
private readonly SentryOptions _options;
private readonly SentryId _traceId;
private readonly CocoaSdk.SentryId _cocoaTraceId;
private readonly ulong _starTimeNs;
private ulong _endTimeNs;
private readonly SentryStopwatch _stopwatch = SentryStopwatch.StartNew();

public CocoaProfiler(SentryOptions options, ulong starTimeNs, SentryId traceId, CocoaSdk.SentryId cocoaTraceId)
{
_options = options;
_starTimeNs = starTimeNs;
_traceId = traceId;
_cocoaTraceId = cocoaTraceId;
_options.LogDebug("Trace {0} profile start timestamp: {1} ns", _traceId, _starTimeNs);
}

/// <inheritdoc />
public void Finish()
{
if (_endTimeNs == 0)
{
_endTimeNs = _starTimeNs + (ulong)_stopwatch.ElapsedNanoseconds;
_options.LogDebug("Trace {0} profile end timestamp: {1} ns", _traceId, _endTimeNs);
}
}

public object Collect(Transaction transaction)
{
var payload = SentryCocoaHybridSdk.CollectProfileBetween(_starTimeNs, _endTimeNs, _cocoaTraceId);
ArgumentNullException.ThrowIfNull(payload, "profile payload");

_options.LogDebug("Trace {0} profile payload collected", _traceId);
return new SerializableNSObject(payload);
}
}
26 changes: 26 additions & 0 deletions src/Sentry/Platforms/iOS/CocoaProfilerFactory.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
using Sentry.Internal;
using Sentry.iOS.Extensions;

namespace Sentry.iOS;

internal class CocoaProfilerFactory : ITransactionProfilerFactory
{
private readonly SentryOptions _options;

internal CocoaProfilerFactory(SentryOptions options)
{
_options = options;
}

/// <inheritdoc />
public ITransactionProfiler? Start(ITransactionTracer tracer, CancellationToken cancellationToken)
{
var traceId = tracer.TraceId.ToCocoaSentryId();
var startTime = SentryCocoaHybridSdk.StartProfilerForTrace(traceId);
if (startTime == 0)
{
return null;
}
return new CocoaProfiler(_options, startTime, tracer.TraceId, traceId);
}
}
33 changes: 33 additions & 0 deletions src/Sentry/Platforms/iOS/Facades/SerializableNSObject.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
using Sentry.Extensibility;
using Sentry.Internal;
using ISerializable = Sentry.Protocol.Envelopes.ISerializable;

namespace Sentry.iOS.Facades;

internal class SerializableNSObject : ISerializable
{
private readonly NSObject _value;

public SerializableNSObject(NSObject value)
{
_value = value;
}

public Task SerializeAsync(Stream stream, IDiagnosticLogger? logger, CancellationToken cancellationToken = default)
=> Task.Run(() => Serialize(stream, logger)); // TODO do we need a better implementation?

public void Serialize(Stream stream, IDiagnosticLogger? logger) {
// For types that implement Sentry Cocoa's SentrySerializable protocol (interface),
// We should call that first, and then serialize the result to JSON later.
var obj = _value is CocoaSdk.ISentrySerializable serializable
? serializable.Serialize()
: _value;

// Now we will use Apple's JSON Serialization functions.
// See https://developer.apple.com/documentation/foundation/nsjsonserialization
// TODO can we pipe NSOutputStream directly? It can be passed as a second argument
// TODO how do we check if the error happened? Is it non-null? Then we can rethrow as NSErrorException?
var data = NSJsonSerialization.Serialize(obj, 0, out NSError error);
data.AsStream().CopyTo(stream);
}
}
1 change: 1 addition & 0 deletions src/Sentry/Platforms/iOS/SentrySdk.cs
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,7 @@ private static void InitSentryCocoaSdk(SentryOptions options)
options.CrashedLastRun = () => SentryCocoaSdk.CrashedLastRun;
options.EnableScopeSync = true;
options.ScopeObserver = new IosScopeObserver(options);
options.TransactionProfilerFactory = new CocoaProfilerFactory(options);

// TODO: Pause/Resume
}
Expand Down
2 changes: 1 addition & 1 deletion src/Sentry/Protocol/Envelopes/Envelope.cs
Original file line number Diff line number Diff line change
Expand Up @@ -314,7 +314,7 @@ public static Envelope FromTransaction(Transaction transaction)
if (transaction.TransactionProfiler is { } profiler)
{
// Profiler.CollectAsync() may throw in which case the EnvelopeItem won't serialize.
items.Add(EnvelopeItem.FromProfileInfo(profiler.CollectAsync(transaction)));
items.Add(EnvelopeItem.FromProfileInfo(profiler.Collect(transaction)));
}

return new Envelope(eventId, header, items);
Expand Down
11 changes: 9 additions & 2 deletions src/Sentry/Protocol/Envelopes/EnvelopeItem.cs
Original file line number Diff line number Diff line change
Expand Up @@ -224,14 +224,21 @@ public static EnvelopeItem FromTransaction(Transaction transaction)
/// <summary>
/// Creates an <see cref="EnvelopeItem"/> from <paramref name="source"/>.
/// </summary>
internal static EnvelopeItem FromProfileInfo(Task<ProfileInfo> source)
internal static EnvelopeItem FromProfileInfo(object source)
{
var header = new Dictionary<string, object?>(1, StringComparer.Ordinal)
{
[TypeKey] = TypeValueProfile
};

return new EnvelopeItem(header, AsyncJsonSerializable.CreateFrom(source));
ISerializable payload = source switch
{
ISerializable serializable => serializable,
Task<IJsonSerializable> task => AsyncJsonSerializable.CreateFrom(task),
_ => throw new ArgumentException("Unsupported profile info source type.", nameof(source))
};

return new EnvelopeItem(header, payload);
}

/// <summary>
Expand Down
Loading