Skip to content

test(profiling): fix broken tests for new API #4998

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

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
10 changes: 3 additions & 7 deletions Sources/Sentry/Profiling/SentryContinuousProfiler.mm
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@

# pragma mark - Private

NSTimeInterval kSentryProfilerChunkExpirationInterval = 60;

namespace {
/** @warning: Must be used from a synchronized context. */
std::mutex _threadUnsafe_gContinuousProfilerLock;
Expand Down Expand Up @@ -152,8 +154,6 @@ + (void)stop
return;
}

SENTRY_LOG_DEBUG(@"Stopping continuous profiler after current chunk completes.");

# if defined(SENTRY_TEST) || defined(SENTRY_TEST_CI)
// we want to allow immediately stopping a continuous profile for a UI test, since those
// currently only test launch profiles, and there is no reliable way to make the UI test
Expand All @@ -168,11 +168,7 @@ + (void)stop
}
# endif // defined(SENTRY_TEST) || defined(SENTRY_TEST_CI)

if (![_threadUnsafe_gContinuousCurrentProfiler isRunning]) {
SENTRY_LOG_DEBUG(@"No continuous profiler is currently running.");
return;
}

SENTRY_LOG_DEBUG(@"Stopping continuous profiler after current chunk completes.");
_stopCalled = YES;
}

Expand Down
15 changes: 11 additions & 4 deletions Sources/Sentry/Profiling/SentryProfiledTracerConcurrency.mm
Original file line number Diff line number Diff line change
Expand Up @@ -80,12 +80,17 @@
void
_unsafe_cleanUpContinuousTraceProfiler()
{
if (SENTRY_CASSERT_RETURN(_gInFlightRootSpans > 0,
@"Attempted to decrement count of root spans to less than zero.")) {
_gInFlightRootSpans -= 1;
_gInFlightRootSpans -= 1;

if (_gInFlightRootSpans < 0) {
# if SENTRY_TEST || SENTRY_TEST_CI

Check warning on line 86 in Sources/Sentry/Profiling/SentryProfiledTracerConcurrency.mm

View check run for this annotation

Codecov / codecov/patch

Sources/Sentry/Profiling/SentryProfiledTracerConcurrency.mm#L86

Added line #L86 was not covered by tests
SENTRY_CASSERT(NO, @"Attempted to decrement count of root spans to less than zero.");
# else
SENTRY_LOG_DEBUG(@"Attempted to decrement count of root spans to less than zero.");
# endif // SENTRY_TEST || SENTRY_TEST_CI
}

if (_gInFlightRootSpans == 0) {
if (_gInFlightRootSpans <= 0) {
SENTRY_LOG_DEBUG(@"Last root span ended, stopping profiler.");
[SentryContinuousProfiler stop];
} else {
Expand Down Expand Up @@ -157,6 +162,8 @@
return;
}
if (!traceSampled) {
SENTRY_LOG_DEBUG(@"The trace associated with the profiler was not sampled, so the "
@"profiler was never started and there is nothing to discard.");
return;
}
_unsafe_cleanUpContinuousTraceProfiler();
Expand Down
4 changes: 2 additions & 2 deletions Sources/Sentry/Profiling/SentryProfilerDefines.h
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@ typedef NS_ENUM(NSUInteger, SentryProfilerTruncationReason) {
SentryProfilerTruncationReasonAppMovedToBackground,
};

static NSTimeInterval kSentryProfilerChunkExpirationInterval = 60;
static NSTimeInterval kSentryProfilerTimeoutInterval = 30;
SENTRY_EXTERN NSTimeInterval kSentryProfilerChunkExpirationInterval;
SENTRY_EXTERN NSTimeInterval kSentryProfilerTimeoutInterval;

NS_ASSUME_NONNULL_BEGIN

Expand Down
1 change: 1 addition & 0 deletions Sources/Sentry/Profiling/SentryTraceProfiler.mm
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
# pragma mark - Private

NSTimer *_Nullable _sentry_threadUnsafe_traceProfileTimeoutTimer;
NSTimeInterval kSentryProfilerTimeoutInterval = 30;

namespace {
/** @warning: Must be used from a synchronized context. */
Expand Down
8 changes: 7 additions & 1 deletion Sources/Sentry/SentryProfiler.mm
Original file line number Diff line number Diff line change
Expand Up @@ -49,13 +49,19 @@
return;
}

sentry_profilerSessionSampleDecision = sentry_sampleProfileSession(options);
sentry_reevaluateSessionSampleRate(options.profiling.sessionSampleRate);
}

} // namespace

# pragma mark - Public

void
sentry_reevaluateSessionSampleRate(float sessionSampleRate)
{
sentry_profilerSessionSampleDecision = sentry_sampleProfileSession(sessionSampleRate);
}

void
sentry_sdkInitProfilerTasks(SentryOptions *options, SentryHub *hub)
{
Expand Down
4 changes: 2 additions & 2 deletions Sources/Sentry/SentrySampling.m
Original file line number Diff line number Diff line change
Expand Up @@ -120,9 +120,9 @@
}

SentrySamplerDecision *
sentry_sampleProfileSession(SentryOptions *options)
sentry_sampleProfileSession(float sessionSampleRate)
{
return _sentry_calcSampleFromNumericalRate(@(options.profiling.sessionSampleRate));
return _sentry_calcSampleFromNumericalRate(@(sessionSampleRate));
}

#endif // SENTRY_TARGET_PROFILING_SUPPORTED
Expand Down
11 changes: 11 additions & 0 deletions Sources/Sentry/SentrySessionTracker.m
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,11 @@
#import "SentrySDK+Private.h"
#import "SentrySwift.h"

#import "SentryProfilingConditionals.h"
#if SENTRY_TARGET_PROFILING_SUPPORTED
# import "SentryProfiler+Private.h"
#endif // SENTRY_TARGET_PROFILING_SUPPORTED

#if SENTRY_TARGET_MACOS_HAS_UI
# import <Cocoa/Cocoa.h>
#endif
Expand Down Expand Up @@ -174,6 +179,12 @@ - (void)didBecomeActive
}
[[[hub getClient] fileManager] deleteTimestampLastInForeground];
self.lastInForeground = nil;

#if SENTRY_TARGET_PROFILING_SUPPORTED
if (hub.client.options.profiling != nil) {
sentry_reevaluateSessionSampleRate(hub.client.options.profiling.sessionSampleRate);
}
#endif // SENTRY_TARGET_PROFILING_SUPPORTED
}

/**
Expand Down
1 change: 1 addition & 0 deletions Sources/Sentry/include/SentryPrivate.h
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
#import "SentryLevelHelper.h"
#import "SentryLogC.h"
#import "SentryMeta.h"
#import "SentryProfiler+Private.h"
#import "SentryRandom.h"
#import "SentryScreenshot.h"
#import "SentrySdkInfo.h"
Expand Down
4 changes: 4 additions & 0 deletions Sources/Sentry/include/SentryProfiledTracerConcurrency.h
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,10 @@ void sentry_captureTransactionWithProfile(SentryHub *hub, SentryDispatchQueueWra
SentryId *_Nullable sentry_startProfiler(SentryTracerConfiguration *configuration, SentryHub *hub,
SentryTransactionContext *transactionContext);

/**
* @note Only called for transaction-based profiling or continuous profiling V2 with trace lifecycle
* option configured.
*/
SENTRY_EXTERN void sentry_stopProfilerDueToFinishedTransaction(
SentryHub *hub, SentryDispatchQueueWrapper *dispatchQueue, SentryTransaction *transaction,
BOOL isProfiling, NSDate *traceStartTimestamp, uint64_t startSystemTime
Expand Down
2 changes: 2 additions & 0 deletions Sources/Sentry/include/SentryProfiler+Private.h
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ SENTRY_EXTERN void sentry_sdkInitProfilerTasks(SentryOptions *options, SentryHub
*/
SENTRY_EXTERN SentrySamplerDecision *_Nullable sentry_profilerSessionSampleDecision;

SENTRY_EXTERN void sentry_reevaluateSessionSampleRate(float sessionSampleRate);

/**
* A wrapper around the low-level components used to gather sampled backtrace profiles.
* @warning A main assumption is that profile start/stop must be contained within range of time of
Expand Down
2 changes: 1 addition & 1 deletion Sources/Sentry/include/SentrySampling.h
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ SENTRY_EXTERN SentrySamplerDecision *sentry_sampleTrace(
SENTRY_EXTERN SentrySamplerDecision *sentry_sampleTraceProfile(SentrySamplingContext *context,
SentrySamplerDecision *tracesSamplerDecision, SentryOptions *options);

SENTRY_EXTERN SentrySamplerDecision *sentry_sampleProfileSession(SentryOptions *options);
SENTRY_EXTERN SentrySamplerDecision *sentry_sampleProfileSession(float sessionSampleRate);
#endif // SENTRY_TARGET_PROFILING_SUPPORTED

NS_ASSUME_NONNULL_END
25 changes: 16 additions & 9 deletions Sources/Swift/Integrations/Performance/SentryProfileOptions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,18 @@ import Foundation

/// An object containing configuration for the Sentry profiler.
/// - warning: Continuous profiling is an experimental feature and may still contain bugs.
/// - note: If either `SentryOptions.profilesSampleRate` or `SentryOptions.profilesSampler` are
/// set to a non-nil value such that transaction-based profiling is being used, these settings
/// will have no effect, nor will `SentrySDK.startProfileSession()` or
/// `SentrySDK.stopProfileSession()`.
/// - note: Profiling is automatically disabled if a thread sanitizer is attached.
@objcMembers
public class SentryProfileOptions: NSObject {
/// Different modes for starting and stopping the profiler.
@objc public enum SentryProfileLifecycle: Int {
/// Profiling is controlled manually, and is independent of transactions & spans. Developers
/// must `SentrySDK.startProfileSession()` and `SentrySDK.stopProfileSession()` to control
/// the lifecycle of the profiler. If the session is sampled,
/// must use`SentrySDK.startProfileSession()` and `SentrySDK.stopProfileSession()` to
/// manage the profile session. If the session is sampled,
/// `SentrySDK.startProfileSession()` will always start profiling.
/// - warning: Continuous profiling is an experimental feature and may still contain bugs.
/// - note: Profiling is automatically disabled if a thread sanitizer is attached.
Expand All @@ -27,6 +31,10 @@ public class SentryProfileOptions: NSObject {
/// - note: If there are multiple overlapping root spans, where some are sampled and some or
/// not, profiling will continue until the end of the last sampled root span. Profiling data
/// will not be linked with spans that are not sampled.
/// - note: When the last root span finishes, the profiler will continue running until the
/// end of the current timed interval. If a new root span starts before this interval
/// completes, the profiler will instead continue running until the next root span stops, at
/// which time it will attempt to stop again in the same way.
/// - note: Profiling is automatically disabled if a thread sanitizer is attached.
case trace
}
Expand All @@ -38,13 +46,12 @@ public class SentryProfileOptions: NSObject {
public var lifecycle: SentryProfileLifecycle = .manual

/// The % of user sessions in which to enable profiling.
/// - warning: Continuous profiling is an experimental feature and may still contain bugs.
/// - note: Whether or not the session is sampled is determined once, when the SDK is initially
/// configured.
/// - note: If either `SentryOptions.profilesSampleRate` or `SentryOptions.profilesSampler` are
/// set to a non-nil value such that transaction-based profiling is being used, then setting
/// this property has no effect, and neither do `SentrySDK.startProfileSession()` or
/// `SentrySDK.stopProfileSession()`.
/// - warning: Continuous profiling is an experimental feature and may still contain bugs.
/// - note: The decision whether or not to sample profiles is computed using this sample rate
/// when the SDK is started, and applies to any requests to start the profiler–regardless of
/// `lifecycle`– until the app resigns its active status. It is then reevaluated on subsequent
/// foreground events. The duration of time that a sample decision prevails between
/// launch/foreground and background is referred to as a profile session.
/// - note: Backgrounding and foregrounding the app starts a new user session and sampling is
/// re-evaluated. If there is no active trace when the app is backgrounded, profiling stops
/// before the app backgrounds. If there is an active trace and profiling is in-flight when the
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,14 @@
SentrySDK.close()
try assertContinuousProfileStoppage()
}


func testBackgroundNotificationStopsProfile() {
SentryContinuousProfiler.start()
XCTAssert(SentryContinuousProfiler.isCurrentlyProfiling())
fixture.notificationCenter.post(Notification(name: UIApplication.willResignActiveNotification))

Check failure on line 112 in Tests/SentryProfilerTests/SentryContinuousProfilerTests.swift

View workflow job for this annotation

GitHub Actions / Unit macOS - Xcode 16.2 - OS latest Sentry

cannot find 'UIApplication' in scope

Check failure on line 112 in Tests/SentryProfilerTests/SentryContinuousProfilerTests.swift

View workflow job for this annotation

GitHub Actions / Unit macOS - Xcode 15.4 - OS latest Sentry

cannot find 'UIApplication' in scope
XCTAssertFalse(SentryContinuousProfiler.isCurrentlyProfiling())
}

func testStartingAPerformanceTransactionDoesNotStartProfiler() throws {
let manualSpan = try fixture.newTransaction()
XCTAssertFalse(SentryContinuousProfiler.isCurrentlyProfiling())
Expand Down
64 changes: 55 additions & 9 deletions Tests/SentryProfilerTests/SentryProfilingPublicAPITests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,19 @@
return scope
}()

let random = TestRandom(value: 0.5)
var sessionTracker: SessionTracker?

var _random: TestRandom = TestRandom(value: 0.5)
var random: TestRandom {
get {
_random
}

Check warning on line 29 in Tests/SentryProfilerTests/SentryProfilingPublicAPITests.swift

View check run for this annotation

Codecov / codecov/patch

Tests/SentryProfilerTests/SentryProfilingPublicAPITests.swift#L28-L29

Added lines #L28 - L29 were not covered by tests
set(newValue) {
_random = newValue
SentryDependencyContainer.sharedInstance().random = newValue
}
}

let currentDate = TestCurrentDateProvider()
lazy var timerFactory = TestSentryNSTimerFactory(currentDateProvider: currentDate)
lazy var client = TestClient(options: options)!
Expand All @@ -31,7 +43,6 @@

override func setUp() {
super.setUp()
SentryDependencyContainer.sharedInstance().random = fixture.random
SentryDependencyContainer.sharedInstance().timerFactory = fixture.timerFactory
SentryDependencyContainer.sharedInstance().dateProvider = fixture.currentDate
}
Expand All @@ -51,8 +62,9 @@
}
}

// MARK: continuous profiling v1
extension SentryProfilingPublicAPITests {
func testStartingContinuousProfilerWithSampleRateZero() throws {
func testStartingContinuousProfilerV1WithSampleRateZero() throws {
givenSdkWithHub()

fixture.options.profilesSampleRate = 0
Expand All @@ -63,7 +75,7 @@
XCTAssertFalse(SentryContinuousProfiler.isCurrentlyProfiling())
}

func testStartingContinuousProfilerWithSampleRateNil() throws {
func testStartingContinuousProfilerV1WithSampleRateNil() throws {
givenSdkWithHub()

// nil is the default initial value for profilesSampleRate, so we don't have to explicitly set it on the fixture
Expand All @@ -75,7 +87,7 @@
try stopProfiler()
}

func testNotStartingContinuousProfilerWithSampleRateBlock() throws {
func testNotStartingContinuousProfilerV1WithSampleRateBlock() throws {
givenSdkWithHub()

fixture.options.profilesSampler = { _ in 0 }
Expand All @@ -84,7 +96,7 @@
XCTAssertFalse(SentryContinuousProfiler.isCurrentlyProfiling())
}

func testNotStartingContinuousProfilerWithSampleRateNonZero() throws {
func testNotStartingContinuousProfilerV1WithSampleRateNonZero() throws {
givenSdkWithHub()

fixture.options.profilesSampleRate = 1
Expand All @@ -93,7 +105,7 @@
XCTAssertFalse(SentryContinuousProfiler.isCurrentlyProfiling())
}

func testStartingAndStoppingContinuousProfiler() throws {
func testStartingAndStoppingContinuousProfilerV1() throws {
givenSdkWithHub()
SentrySDK.startProfiler()
XCTAssert(SentryContinuousProfiler.isCurrentlyProfiling())
Expand All @@ -103,18 +115,21 @@
XCTAssertFalse(SentryContinuousProfiler.isCurrentlyProfiling())
}

func testStartingContinuousProfilerBeforeStartingSDK() {
func testStartingContinuousProfilerV1BeforeStartingSDK() {
SentrySDK.startProfiler()
XCTAssertFalse(SentryContinuousProfiler.isCurrentlyProfiling())
}

func testStartingContinuousProfilerAfterStoppingSDK() {
func testStartingContinuousProfilerV1AfterStoppingSDK() {
givenSdkWithHub()
SentrySDK.close()
SentrySDK.startProfiler()
XCTAssertFalse(SentryContinuousProfiler.isCurrentlyProfiling())
}
}

// MARK: continuous profiling v2
extension SentryProfilingPublicAPITests {
func testManuallyStartingAndStoppingContinuousProfilerV2Sampled() throws {
// Arrange
fixture.options.profiling.sessionSampleRate = 1
Expand Down Expand Up @@ -401,6 +416,37 @@
// Assert
XCTAssertFalse(SentryContinuousProfiler.isCurrentlyProfiling())
}

func testSessionSampleRateReevaluationOnAppBecomingActive() {
// Arrange
fixture.options.profiling.sessionSampleRate = 0.5
fixture.options.profiling.lifecycle = .manual
fixture.random = TestRandom(value: 0)
let nc = SentryDependencyContainer.sharedInstance().notificationCenterWrapper
fixture.sessionTracker = SessionTracker(options: fixture.options, notificationCenter: nc)
fixture.sessionTracker?.start()
givenSdkWithHub()

// Act
SentrySDK.startProfileSession()

// Assert
XCTAssert(SentryContinuousProfiler.isCurrentlyProfiling())

// Act
nc.post(Notification(name: UIApplication.willResignActiveNotification))
XCTAssertFalse(SentryContinuousProfiler.isCurrentlyProfiling())

// Arrange
fixture.random = TestRandom(value: 1)

// Act
nc.post(Notification(name: UIApplication.didBecomeActiveNotification))
SentrySDK.startProfileSession()

// Assert
XCTAssertFalse(SentryContinuousProfiler.isCurrentlyProfiling())
}
}

private extension SentryProfilingPublicAPITests {
Expand Down
Loading